diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 2143461c8edc..ea89cb27b240 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,4 +1,4 @@ -name: "🐛 Bug report" +name: "🐛 Bug Report" description: Something on Artemis is not working as expected? Create a report to help us improve. labels: [bug] body: @@ -8,7 +8,7 @@ body: - type: textarea attributes: label: Describe the bug - description: A clear and concise description of what the bug is. + description: A clear and concise description of what the bug is. placeholder: What happened? Also tell us, what did you expect to happen? validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature-proposal--developer-.md b/.github/ISSUE_TEMPLATE/feature-proposal--developer-.md index 7728ab5150ea..e08b8bbf1c37 100644 --- a/.github/ISSUE_TEMPLATE/feature-proposal--developer-.md +++ b/.github/ISSUE_TEMPLATE/feature-proposal--developer-.md @@ -1,5 +1,5 @@ --- -name: Feature Proposal (Developer) +name: 📝 Feature Proposal (Developer) about: Software Engineering Process for a new feature title: "[Feature Proposal]" labels: feature-proposal @@ -7,7 +7,10 @@ assignees: '' --- -> Feature Template Spec Version 0.1 + + +# Feature Proposal +> Spec Version 0.2.0 ## Context @@ -15,9 +18,8 @@ assignees: '' > Describe the problem that is tackled in this issue ### Motivation -> Describe the motivation WHY the problem needs solving. Include the affected users/roles here. +> Describe the motivation WHY the problem needs solving. Specify the affected users/roles. ---- ## Requirements Engineering ### Existing (Problematic) Solution / System @@ -25,7 +27,7 @@ assignees: '' > You may include a UML Model here ### Proposed System -> How should the perfect solution look like? +> What would the ideal solution look like? ### Requirements > Describe the Functional and Non-Functional Requirements of the feature. Stick to the INVEST methodology! @@ -33,7 +35,6 @@ assignees: '' > > 1. NFR: : : <Description> ---- ## Analysis ### Analysis Object Model @@ -43,24 +44,22 @@ assignees: '' > Include dynamic models (Activity Diagram, State Chart Diagram, Communication Diagram) here to outline the dynamic nature of the PROBLEM ---- -## System Design +## System Architecture ### Subsystem Decomposition > Show the involved subsystems and their interfaces. Make sure to describe the APIs that you add/change in detail. Model the DTOs you intend to (re)use or change! ### Persistent Data Management > Describe the Database changes you intend to make. -> Outline new config options you will add. -> Describe all other data persistency mechanisms you may use. +> Outline new configuration options you plan to introduce +> Describe all other data persistence mechanisms you may use. ### Access Control / Security Aspects > Describe the access control considerations for your feature ### Other Design Decisions -> Potential candidates to discuss here: Websockets, Test strategy +> Potential topics to discuss here include: WebSockets, testing strategies. ---- -## UI / UX -> Describe the user flow (references to dynamic model). -> Screenshots of the final UI mockup +## UI/UX Design +> Screenshots of the final UI mockups (mandatory): Please include screenshots to provide a clear and persistent visual reference of the design. +> Link to the design mockup (optional): Additionally, you may include a link to the live design mockup (e.g., Figma, Sketch) for a more interactive view. Note that this link is supplementary and should not replace the required screenshots. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index db83f3d94b38..76b9187dd7d7 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,11 +1,11 @@ -name: "🚀 Feature request" +name: "🚀 Feature Request" description: Suggest an idea for this project labels: [feature] body: - type: markdown attributes: value: | - Thanks for suggesting new features or pointing our missing functionality. + Thanks for suggesting new features or pointing our missing functionality. Please describe your request in detail so we can understand your ideas. Feel free to upload additional material such as mockups, diagrams, or sketches - type: textarea attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 987634ddf123..8fdf21f8fe51 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,11 +5,11 @@ #### General <!-- Remove tasks that are not applicable for your PR. Please only put the PR into ready for review, if all relevant tasks are checked! --> <!-- You only need to choose one of the first two check items: Generally, test on the test servers. --> -<!-- If it's only a small change, testing it locally is acceptable and you may remove the first checkmark. If you are unsure, please test on the test servers. --> +<!-- If it's only a small change, testing it locally is acceptable, and you may remove the first checkmark. If you are unsure, please test on the test servers. --> - [ ] I tested **all** changes and their related features with **all** corresponding user types on a test server. - [ ] This is a small issue that I tested locally and was confirmed by another developer on a test server. - [ ] Language: I followed the [guidelines for inclusive, diversity-sensitive, and appreciative language](https://docs.artemis.cit.tum.de/dev/guidelines/language-guidelines/). -- [ ] I chose a title conforming to the [naming conventions for pull requests](https://docs.artemis.cit.tum.de/dev/development-process/#naming-conventions-for-github-pull-requests). +- [ ] I chose a title conforming to the [naming conventions for pull requests](https://docs.artemis.cit.tum.de/dev/development-process/development-process.html#naming-conventions-for-github-pull-requests). #### Server diff --git a/.github/workflows/append_feature_proposal.yml b/.github/workflows/append_feature_proposal.yml new file mode 100644 index 000000000000..ad99a6abe54c --- /dev/null +++ b/.github/workflows/append_feature_proposal.yml @@ -0,0 +1,169 @@ +name: Append Feature Proposal to Issue Description + +on: + issues: + types: [assigned, unassigned, labeled] + +jobs: + check-labels: + runs-on: ubuntu-latest + outputs: + status: ${{ steps.feature-proposal-tag-check.outputs.status }} + steps: + - id: feature-proposal-tag-check + name: Check if feature proposal tag added + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const issueNumber = context.payload.issue.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + const hasFeatureProposalLabel = issue.data.labels.some(label => label.name === 'needs-feature-proposal'); + if (hasFeatureProposalLabel) { + console.log('Feature Proposal label added. Proceeding...'); + core.setOutput('status', 'success'); + } else { + console.log('Feature Proposal label not added. Skipping action...'); + core.setOutput('status', 'failure'); + } + + manage-feature-proposal: + needs: check-labels + if: needs.check-labels.outputs.status == 'success' + runs-on: ubuntu-latest + steps: + - name: Check if feature proposal tag added + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const issueNumber = context.payload.issue.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + const hasFeatureProposalLabel = issue.data.labels.some(label => label.name === 'needs-feature-proposal'); + if (hasFeatureProposalLabel) { + console.log('Feature Proposal label added. Proceeding...'); + } else { + console.log('Feature Proposal label not added. Skipping action...'); + process.exit(0); + } + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Append Feature Proposal Template to Issue Description + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const issueNumber = context.payload.issue.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + // Check if the issue has a 'bug' label + const hasBugLabel = issue.data.labels.some(label => label.name === 'bug'); + if (hasBugLabel) { + console.log("Issue is labeled as 'bug'. Skipping..."); + return; // Exit the script if 'bug' label is found + } + + const featureProposalMarker = '<!-- Feature Proposal Marker -->'; + if (!issue.data.body.includes(featureProposalMarker)) { + const templateContent = await github.rest.repos.getContent({ + owner, + repo, + path: '.github/ISSUE_TEMPLATE/feature-proposal--developer-.md' + }); + let templateText = Buffer.from(templateContent.data.content, 'base64').toString(); + + // Add separator line and remove metadata section + templateText = '---\n' + templateText.split('---').slice(2).join('---').trim(); + + const updatedBody = issue.data.body + "\n" + templateText; + + await github.rest.issues.update({ + owner, + repo, + issue_number: issueNumber, + body: updatedBody, + }); + } + + - name: Update or Post instructions comment + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const issueNumber = context.payload.issue.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber, + }); + + // Check if the issue has any assignees + if (issue.data.assignees.length === 0) { + console.log("No assignees for this issue. Skipping..."); + return; // Exit the script if no assignees are found + } + + // Check if the issue has a 'bug' label + const hasBugLabel = issue.data.labels.some(label => label.name === 'bug'); + if (hasBugLabel) { + console.log("Issue is labeled as 'bug'. Skipping..."); + return; // Exit the script if 'bug' label is found + } + + const assignees = issue.data.assignees.map(assignee => '@' + assignee.login).join(', '); + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + }); + const instructionCommentMarker = '<!-- Instruction Comment Marker -->'; + let instructionCommentId = null; + + for (const comment of comments.data) { + if (comment.body.includes(instructionCommentMarker)) { + instructionCommentId = comment.id; + break; + } + } + + const commentBody = `Hello ${assignees},\n\nThank you for taking on this issue.\n\nTo ensure the Feature Proposal is accurately filled out, we kindly ask you to follow the structure provided.\n\n**For detailed instructions and best practices**, please refer to our **[Development Process Guidelines](https://docs.artemis.cit.tum.de/dev/development-process.html)**.\n\n${instructionCommentMarker}`; + + if (instructionCommentId) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: instructionCommentId, + body: commentBody, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: commentBody, + }); + } diff --git a/.idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml b/.idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml new file mode 100644 index 000000000000..6c1043ef9f79 --- /dev/null +++ b/.idea/runConfigurations/Artemis__Server__Jenkins___LocalVC_.xml @@ -0,0 +1,13 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Artemis (Server, Jenkins & LocalVC)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot"> + <option name="ACTIVE_PROFILES" value="dev,jenkins,localvc,artemis,scheduling,core,local" /> + <module name="Artemis.main" /> + <option name="SHORTEN_COMMAND_LINE" value="MANIFEST" /> + <option name="SPRING_BOOT_MAIN_CLASS" value="de.tum.in.www1.artemis.ArtemisApp" /> + <option name="VM_PARAMETERS" value="-XX:+ShowCodeDetailsInExceptionMessages -Duser.country=US -Duser.language=en" /> + <option name="ALTERNATIVE_JRE_PATH" /> + <method v="2"> + <option name="Gradle.BeforeRunTask" enabled="false" tasks="build" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="-x webapp -x test -x jacocoTestCoverageVerification -x spotlessCheck -x checkstyleMain -x checkstyleTest" /> + </method> + </configuration> +</component> diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04b02781b0cb..ea3d5c107479 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,110 +1,6 @@ # Contributing Guide for Artemis -Please read this guide before creating a pull request, otherwise your contribution might not be approved. +Read the [setup guide](https://docs.artemis.cit.tum.de/dev/setup.html) on how to set up your local development environment. -## Branch Organization +Before creating a pull request, please read the [guidelines to the development process](https://docs.artemis.cit.tum.de/dev/development-process/development-process.html) as well as the [coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines.html). -All pull request branches are created from develop. - -We use the following structure for branch names: - -\<type\>/\<area\>/\<short-description\> - -Possible types are: - -- feature -- enhancement -- bugfix -- hotfix - -The pull request template will provide additional information on the requirement for the integration of changes into Artemis. -Once the changes in your pull request are approved by one of our reviewers, they can be merged into develop. - -## Pull request (PR) guidelines: - -- **Merge fast**: PRs should only be open for a couple of days. -- **Small packages**: PRs should be as small as possible and ideally concentrate on a single topic. Features should be split up into multiple PRs if it makes sense. -- **Until the PR is _ready-for-review_, the PR should be a [Draft PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests)** -- **Definition of done**: Before requesting a code review make sure that the PR is _ready-for-review_: - - The PR template is filled out completely, containing as much information as needed to understand the feature. - - All tasks from the template checklist are done and checked off (writing tests, adding screenshots, etc.). - - The branch of the PR is up-to-date with develop. - - The last build of the PR is successful. - -## Code review guidelines - -- **Check out the code and test it**: Testing the feature/enhancement/bugfix helps to understand the code. -- **Respect the PR scope**: Bugfixes, enhancements or implementations that are unrelated to the PRs topic should not be enforced in a code review. -In this case the reviewer or PR maintainer needs to make sure to create an issue for this topic on GitHub or the internal task tracking tool so it is not lost. -- **Code style is not part of a code review**: Code style and linting issues are not part of the review process. If issues in code style or linting arise, the linters and auto formatters used in our CI tools need to be updated. -- **Enforce guidelines**: Enforcing technical & design guidelines is an integral part of the code review (e.g. consistent REST urls). -- **Mark optional items**: Review items that are optional from the reviewers' perspective should be marked as such (e.g. "Optional: You could also do this with...") -- **Explain your rational**: If the reviewer requests a change, the reasoning behind the change should be explained (e.g. not "Please change X to Y", but "Please change X to Y, because this would improve Z") - -## Development Workflow - -Find here [a guide](docs/dev/setup.rst) on how to setup your local development environment. - -## Route Naming Conventions - -- Always use **kebab-case** (e.g. "/exampleAssessment" → "/example-assessment") -- The routes should follow the general structure entity > entityId > sub-entity ... (e.g. "/exercises/{exerciseId}/participations") -- Use **plural for server route's** entities and **singular for client route's** entities -- Specify the key entity at the end of the route (e.g. "text-editor/participations/{participationId}" should be changed to "participations/{participationId}/text-editor") -- Never specify an id that is used only for consistency and not used in the code (e.g. GET "/courses/{courseId}/exercises/{exerciseId}/participations/{participationId}/submissions/{submissionId}" can be simplified to GET "/submissions/{submissionId}" because all other entities than the submission are either not needed or can be loaded without the need to specify the id) - -## CSS Guidelines - -We are using [Scss](https://sass-lang.com) to write modular, reusable css. - -We have a couple of global scss files in `webapp/content` but encourage [component dependent css with angular's styleUrls](https://angular.io/guide/component-styles). - -From a methodology viewpoint we encourage the use of [BEM](http://getbem.com/introduction/). -```scss -.my-container { - // container styles - &__content { - // content styles - &--modifier { - // modifier styles - } - } -} -``` - -Within the component html files, we encourage the use of [bootstrap css](https://getbootstrap.com/). - -Encouraged html styling: -`<div class="d-flex ms-2">some content</div>` - -## Testing - -We create unit & integration tests for the Artemis server and client. -Adding tests is an integral part of any pull request - please be aware that your pull request will not be approved until you provide automated tests for your implementation! -Our goal is to keep the test coverage above 80%. - -### Server Testing - -We use the [Spring Boot testing utilities](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html) for server side testing. - -Location of test files: `src/test/java` - -Execution command: `./gradlew test` - -### Client Testing - -We use [Jest](https://jestjs.io/) for client side testing. - -For convenience purposes we have [Sinon](https://sinonjs.org/) and [Chai](https://www.chaijs.com/) as dependencies, so that easy stubbing/mocking is possible ([sinon-chai](https://github.com/domenic/sinon-chai)). - -Location of test files: `src/test/javascript` - -Execution command: `npm run test` - -The folder structure is further divided into: - -- component -- integration -- service - -The tests located in the folder `/app` are not working at the moment and are not included in the test runs. diff --git a/README.md b/README.md index 3ad3b4f25acb..395055ec1c64 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,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.0.3.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.1.0.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index d713f51dc37f..41cc4db0a54c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ plugins { id "idea" id "jacoco" id "org.springframework.boot" version "${spring_boot_version}" - id "io.spring.dependency-management" version "1.1.4" + id "io.spring.dependency-management" version "1.1.5" id "com.google.cloud.tools.jib" version "3.4.2" id "com.github.node-gradle.node" version "${gradle_node_plugin_version}" id "com.diffplug.spotless" version "6.25.0" @@ -32,7 +32,7 @@ plugins { } group = "de.tum.in.www1.artemis" -version = "7.0.3" +version = "7.1.0" description = "Interactive Learning with Individual Feedback" java { @@ -157,12 +157,14 @@ jar { private excludedClassFilesForReport(classDirectories) { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, - exclude: [ - "**/de/tum/in/www1/artemis/domain/**/*_*", - "**/de/tum/in/www1/artemis/config/migration/entries/**", - "**/org/eclipse/jgit/**", - "**/gradle-wrapper.jar/**" - ] + exclude: [ + "**/de/tum/in/www1/artemis/domain/**/*_*", + "**/de/tum/in/www1/artemis/config/migration/entries/**", + "**/de/tum/in/www1/artemis/service/connectors/pyris/dto/**", + "**/de/tum/in/www1/artemis/web/rest/iris/dto/**", + "**/org/eclipse/jgit/**", + "**/gradle-wrapper.jar/**" + ] ) })) } @@ -184,13 +186,13 @@ jacocoTestCoverageVerification { counter = "INSTRUCTION" value = "COVEREDRATIO" // TODO: in the future the following value should become higher than 0.92 - minimum = 0.899 + minimum = 0.898 } limit { counter = "CLASS" value = "MISSEDCOUNT" // TODO: in the future the following value should become less than 10 - maximum = 26 + maximum = 29 } } } @@ -385,7 +387,7 @@ dependencies { implementation "org.springframework.security:spring-security-oauth2-core:${spring_security_version}" implementation "org.springframework.security:spring-security-oauth2-client:${spring_security_version}" // use newest version of nimbus-jose-jwt to avoid security issues through outdated dependencies - implementation "com.nimbusds:nimbus-jose-jwt:9.37.3" + implementation "com.nimbusds:nimbus-jose-jwt:9.38" implementation "org.springframework.security:spring-security-oauth2-jose:${spring_security_version}" implementation "org.springframework.security:spring-security-crypto:${spring_security_version}" implementation "org.springframework.security:spring-security-web:${spring_security_version}" @@ -420,7 +422,7 @@ dependencies { implementation "org.apache.maven:maven-model:3.9.6" implementation "org.apache.pdfbox:pdfbox:3.0.2" implementation "com.google.protobuf:protobuf-java:4.26.1" - implementation "org.apache.commons:commons-csv:1.10.0" + implementation "org.apache.commons:commons-csv:1.11.0" implementation "org.commonmark:commonmark:0.22.0" implementation "commons-fileupload:commons-fileupload:1.5" implementation "net.lingala.zip4j:zip4j:2.11.5" @@ -475,7 +477,7 @@ dependencies { } testImplementation("net.bytebuddy:byte-buddy") { version { - strictly "1.14.14" + strictly "1.14.15" } } testImplementation "io.github.classgraph:classgraph:4.8.172" diff --git a/docker/artemis/config/node1.env b/docker/artemis/config/node1.env index 5db1cae1da99..65d16156ff29 100644 --- a/docker/artemis/config/node1.env +++ b/docker/artemis/config/node1.env @@ -1,4 +1,4 @@ -SPRING_PROFILES_ACTIVE='prod,localvc,localci,buildagent,core,scheduling,docker' +SPRING_PROFILES_ACTIVE='prod,localvc,localci,core,scheduling,docker' EUREKA_INSTANCE_INSTANCEID='Artemis:1' EUREKA_INSTANCE_HOSTNAME='artemis-app-node-1' SPRING_HAZELCAST_INTERFACE='artemis-app-node-1' diff --git a/docker/artemis/config/node2.env b/docker/artemis/config/node2.env index 8fe579d9d0d8..c8438fadc51f 100644 --- a/docker/artemis/config/node2.env +++ b/docker/artemis/config/node2.env @@ -2,3 +2,6 @@ SPRING_PROFILES_ACTIVE='prod,localvc,localci,buildagent,core,docker' EUREKA_INSTANCE_INSTANCEID='Artemis:2' EUREKA_INSTANCE_HOSTNAME='artemis-app-node-2' SPRING_HAZELCAST_INTERFACE='artemis-app-node-2' + +ARTEMIS_VERSIONCONTROL_USER='artemis_admin' +ARTEMIS_VERSIONCONTROL_PASSWORD='artemis_admin' diff --git a/docker/artemis/config/node3.env b/docker/artemis/config/node3.env index feeec81050dc..f0b2937cac32 100644 --- a/docker/artemis/config/node3.env +++ b/docker/artemis/config/node3.env @@ -1,4 +1,7 @@ -SPRING_PROFILES_ACTIVE='prod,localvc,localci,buildagent,core,docker' +SPRING_PROFILES_ACTIVE='prod,buildagent' EUREKA_INSTANCE_INSTANCEID='Artemis:3' EUREKA_INSTANCE_HOSTNAME='artemis-app-node-3' SPRING_HAZELCAST_INTERFACE='artemis-app-node-3' + +ARTEMIS_VERSIONCONTROL_USER='artemis_admin' +ARTEMIS_VERSIONCONTROL_PASSWORD='artemis_admin' diff --git a/docker/artemis/config/prod-multinode.env b/docker/artemis/config/prod-multinode.env index 4ca433fa7fd8..5965e05a4e49 100755 --- a/docker/artemis/config/prod-multinode.env +++ b/docker/artemis/config/prod-multinode.env @@ -35,7 +35,7 @@ EUREKA_INSTANCE_APPNAME='Artemis' JHIPSTER_REGISTRY_PASSWORD="admin" JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64SECRET="bXktc2VjcmV0LWtleS13aGljaC1zaG91bGQtYmUtY2hhbmdlZC1pbi1wcm9kdWN0aW9uLWFuZC1iZS1iYXNlNjQtZW5jb2RlZAo=" -ARTEMIS_VERSIONCONTROL_URL='https://localhost' +ARTEMIS_VERSIONCONTROL_URL='http://artemis-app-node-2:8080' ARTEMIS_VERSIONCONTROL_USER='demo' ARTEMIS_VERSIONCONTROL_PASSWORD='demo' ARTEMIS_CONTINUOUSINTEGRATION_ARTEMISAUTHENTICATIONTOKENVALUE='demo' diff --git a/docker/nginx/artemis-upstream-multi-node.conf b/docker/nginx/artemis-upstream-multi-node.conf index f8d94a933494..1ea41db8232e 100644 --- a/docker/nginx/artemis-upstream-multi-node.conf +++ b/docker/nginx/artemis-upstream-multi-node.conf @@ -1,3 +1,2 @@ server artemis-app-node-1:8080; server artemis-app-node-2:8080; -server artemis-app-node-3:8080; diff --git a/docs/admin/setup/distributed.rst b/docs/admin/setup/distributed.rst index 00b21980bc57..f1bd48e0e7ec 100644 --- a/docs/admin/setup/distributed.rst +++ b/docs/admin/setup/distributed.rst @@ -43,6 +43,8 @@ have to be synchronized: Each of these three aspects is synchronized using a different solution +.. _Database Cache: + Database cache ^^^^^^^^^^^^^^ Artemis uses a cache provider that supports distributed caching: Hazelcast_. @@ -281,6 +283,8 @@ This enables the registry in nginx This will apply the config changes and the registry will be reachable. +.. _WebSockets: + WebSockets ^^^^^^^^^^ @@ -652,3 +656,116 @@ different ports and a unique instance ID for each instance. #. Start the remaining instances. You should now be able to see all instances in the registry interface at ``http://localhost:8761``. + +.. _Running multiple instances locally with Docker: + +Running multiple instances locally with Docker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can also run multiple instances of Artemis locally using Docker. This will start 3 Artemis instances, each running +on a its own container. A load balancer (nginx) will be used to distribute the requests to the different instances. The +load balancer will be running in a separate container and will be accessible on ports 80/443 of the host system. The +instances will be registered in the registry service running on a separate container. The instances will use the registry +service to discover each other and form a Hazelcast cluster. Further details can be found in :ref:`Database Cache`. The +instances will also use a ActiveMQ Artemis broker to synchronize WebSocket messages. Further details can be found in +:ref:`WebSockets`. In summary, the setup will look like this: + +* 3 Artemis instances: + + * artemis-app-node-1: using following spring profile: ``prod,localvc,localci,core,scheduling,docker`` + * artemis-app-node-2: using following profile: ``prod,localvc,localci,buildagent,core,docker`` + * artemis-app-node-3: using following profile: ``prod,buildagent`` +* A MySQL database addressable on port 3306 of the host system +* A Load balancer (nginx) addressable on ports 80/443 of the host system: ``http(s)://localhost`` +* A Registry service addressable on port 8761 of the host system: ``http://localhost:8761`` +* An ActiveMQ broker + + + .. figure:: distributed/multi-node-setup.drawio.png + :align: center + + + +.. note:: + + - You don't have to start the client manually. The client files are served by the Artemis instances and can be + accessed through the load balancer on ``http(s)://localhost``. + + - You may run into the following error when starting the containers + ``No member group is available to assign partition ownership...``. This issue should resolve itself after a few + minutes. Otherwise, you can first start the following containers: + ``docker compose -f docker/test-server-multi-node-mysql-localci.yml up mysql jhipster-registry activemq-broker artemis-app-node-1``. + After these containers are up and running, you can start the remaining containers: + ``docker compose -f docker/test-server-multi-node-mysql-localci.yml up artemis-app-node-2 artemis-app-node-3 nginx``. + + +Linux setup +""""""""""" + +#. When running the Artemis container on a Unix system, you will have to give the user running in the container + permission to access the Docker socket by adding them to the docker group. You can find the group ID of the docker + group by running ``getent group docker | cut -d: -f3``. Afterwards, create a new file ``docker/.env`` with the + following content: + + .. code:: bash + + DOCKER_GROUP_ID=<REPLACE_WITH_DOCKER_GROUP_ID_OF_YOUR_SYSTEM> + +#. The docker compose setup which we will use will mount some local directories + (namely the ones under docker/.docker-data) into the containers. To ensure that the user running in the container has + the necessary permissions to these directories, you will have to change the owner of these directories to the + user running in the container (User with ID 1337). You can do this by running the following command: + + .. code:: bash + + sudo chown -R 1337:1337 docker/.docker-data + + .. note:: + + - If you don't want to change the owner of the directories, you can create other directories with the necessary + permissions and adjust the paths in the docker-compose file accordingly. + - You could also use docker volumes instead of mounting local directories. You will have to adjust the docker-compose + file accordingly (`Docker docs <https://docs.docker.com/storage/volumes/#use-a-volume-with-docker-compose/>`_). + However, this would make it more difficult to access the files on the host system. + +#. Start the docker containers by running the following command: + + .. code:: bash + + docker compose -f docker/test-server-multi-node-mysql-localci.yml up + +#. You can now access artemis on ``http(s)://localhost`` and the registry on ``http://localhost:8761``. + +Windows setup +""""""""""""" + +#. When running the Artemis container on a Windows system, you will have to change the value for the Docker connection + URI. You need to change the value of the environment variable ``ARTEMIS_CONTINUOUSINTEGRATION_DOCKERCONNECTIONURI`` + in the file ``docker/artemis/config/prod-multinode.env`` to ``tcp://host.docker.internal:2375``. + + .. note:: + + - Make sure that option "Expose daemon on tcp://localhost:2375 without TLS" is enabled. This can be found under + Settings > General in Docker Desktop. + +#. Start the docker containers by running the following command: + + .. code:: bash + + docker compose -f docker/test-server-multi-node-mysql-localci.yml up + +#. You can now access artemis on ``http(s)://localhost`` and the registry on ``http://localhost:8761``. + +MacOS setup +""""""""""" + +#. Make sure to enable "Allow the default Docker socket to be used (requires password)" in the Docker Desktop settings. + This can be found under Settings > Advanced in Docker Desktop. + +#. Start the docker containers by running the following command: + + .. code:: bash + + docker compose -f docker/test-server-multi-node-mysql-localci.yml up + +#. You can now access artemis on ``http(s)://localhost`` and the registry on ``http://localhost:8761``. diff --git a/docs/admin/setup/distributed/multi-node-setup.drawio.png b/docs/admin/setup/distributed/multi-node-setup.drawio.png new file mode 100644 index 000000000000..2ed573edef8c Binary files /dev/null and b/docs/admin/setup/distributed/multi-node-setup.drawio.png differ diff --git a/docs/dev/development-process.rst b/docs/dev/development-process.rst deleted file mode 100644 index f19030672c77..000000000000 --- a/docs/dev/development-process.rst +++ /dev/null @@ -1,114 +0,0 @@ -******************* -Development Process -******************* - -.. contents:: Content of this document - :local: - :depth: 2 - -Naming Conventions for GitHub Pull Requests -=========================================== - -1. The first term is a main feature of Artemis and is using code highlighting, e.g. “``Programming exercises``:”. - - 1. Possible feature tags are: ``Programming exercises``, ``Quiz exercises``, ``Modeling exercises``, ``Text exercises``, ``File upload exercises``, ``Exam mode``, - ``Grading``, ``Assessment``, ``Communication``, ``Notifications``, ``Team exercises``, ``Lectures``, ``Plagiarism checks``, ``Learning analytics``, - ``Adaptive learning``, ``Learning path``, ``Tutorial groups``, ``Iris``. - 2. If the change is not visible to end users, or it is a pure development or test improvement, we use the term “``Development``:”. - 3. Everything else belongs to the ``General`` category. - -2. The colon is not highlighted. - -3. After the colon, there should be a verbal form that is understandable by end users and non-technical persons, because this will automatically become part of the release notes. - - 1. The text should be short, non-capitalized (except the first word) and should include the most important keywords. Do not repeat the feature if it is possible. - 2. We generally distinguish between bugfixes (the verb “Fix”) and improvements (all kinds of verbs) in the release notes. This should be immediately clear from the title. - 3. Good examples: - - - “Allow instructors to delete submissions in the participation detail view” - - “Fix an issue when clicking on the start exercise button” - - “Add the possibility for instructors to define submission policies” - - - -Steps to Create and Merge a Pull Request -======================================== - -0. Precondition -> only Developer ---------------------------------- - -* Limit yourself to one functionality per pull request. -* Split up your task in multiple branches & pull requests if necessary. -* `Commit Early, Commit Often, Perfect Later, Publish Once. <https://speakerdeck.com/lemiorhan/10-git-anti-patterns-you-should-be-aware-of>`_ - -1. Start Implementation -> only Developer ------------------------------------------ - -* `Open a draft pull request. <https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request>`_ This allows for code related questions and discussions. - -2. Implementation is "done" -> only Developer ---------------------------------------------- - -* Make sure all steps in the `Checklist <https://github.com/ls1intum/Artemis/blob/develop/.github/PULL_REQUEST_TEMPLATE.md>`_ are completed. -* Add or update the "Steps for Testing" in the description of your pull request. -* Make sure that the changes in the pull request are only the ones necessary. -* Mark the pull request as `ready for review. <https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request>`_ - -3. Review ---------- - -Developer -^^^^^^^^^ -* Organize or join a testing session. Especially for large pull requests this makes testing a lot easier. -* Actively look for reviews. Do not just open the pull request and wait. - -Reviewer -^^^^^^^^ -* Perform the "Steps for Testing" and verify that the new functionality is working as expected. -* Verify that related functionality is still working as expected. -* Check the changes to - * conform with the code style. - * make sure you can easily understand the code. - * make sure that (extensive) comments are present where deemed necessary. - * performance is reasonable (e.g. number of database queries or HTTP calls). -* Submit your comments and status (👍 Approve or 👎 Request Changes) using GitHub. - * Explain what you did (test, review code) and on which test server in the review comment. - -4. Respond to review --------------------- - -Developer -^^^^^^^^^ -* Use the pull request to discuss comments or ask questions. -* Update your code where necessary. -* Revert to draft if the changes will take a while during which review is not needed/possible. -* Set back to ready for review afterwards. -* Notify the reviewer(s) once your revised version is ready for the next review. -* Comment on "inline comments" (e.g. "Done"). - -Reviewer -^^^^^^^^ -* Respond to questions raised by the reviewer. -* Mark conversations as resolved if the change is sufficient. - -Iterate steps 3 & 4 until ready for merge (all reviewers approve 👍) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -5. Merge --------- -A project maintainer merges your changes into the ``develop`` branch. - - - -Stale Bot -========= - -If the pull request doesn't have any activity for at least 7 days, the stale bot will mark the PR as `stale`. -The `stale` status can simply be removed by adding a comment or a commit to the PR. -After the PR is marked as `stale`, the bot waits another 14 days until the PR will be closed (21 days in total). -Adding activity to the PR will remove the `stale` label again and reset the stale timer. -To prevent the bot from adding the `stale` label to the PR in the first place, the `no-stale` label can be used. -This label should only be utilized if the PR is blocked by another PR or the PR needs help from another developer. - -A full documentation on this bit can be found here: -https://github.com/actions/stale diff --git a/docs/dev/development-process/artemis-feature-proposal-flow.png b/docs/dev/development-process/artemis-feature-proposal-flow.png new file mode 100644 index 000000000000..08afedceb125 Binary files /dev/null and b/docs/dev/development-process/artemis-feature-proposal-flow.png differ diff --git a/docs/dev/development-process/artemis-process-model.png b/docs/dev/development-process/artemis-process-model.png new file mode 100644 index 000000000000..2c9b663046df Binary files /dev/null and b/docs/dev/development-process/artemis-process-model.png differ diff --git a/docs/dev/development-process/development-process.rst b/docs/dev/development-process/development-process.rst new file mode 100644 index 000000000000..b890ba99774c --- /dev/null +++ b/docs/dev/development-process/development-process.rst @@ -0,0 +1,220 @@ +******************* +Development Process +******************* + + +.. figure:: ./artemis-process-model.png + :align: center + :alt: Artemis Process Model - Activity Diagram + :figclass: align-center + +1. Submit a Feature Request +=========================== +The initial step in our development process involves the creation of a feature request, which is accomplished through the submission of a GitHub Issue. +This action can be performed by any stakeholder, including developers, users, or maintainers. +The feature request should include a detailed description of the desired functionality, as well as any relevant information that may be useful to the development team. +This information should include the rationale for the feature, the expected benefits, and any potential risks or challenges that may be associated with the implementation of the feature. + +2. Evaluate Feature Request +=========================== +Once a feature request has been submitted, the maintainers will evaluate the request together with the development team to determine its feasibility and potential impact on the system. + +3. Create a Feature Proposal +=============================== +If the feature request is deemed feasible, the development team will create a feature proposal that extensively describes the proposed feature. This step will consist of the artifacts mentioned in the model "Create Feature Proposal" below: + +.. figure:: ./artemis-feature-proposal-flow.png + :align: center + :alt: Create a Feature Proposal - Activity Diagram + :figclass: align-center + + +Step 1: Append Feature Proposal Template to Feature Request on GitHub +--------------------------------------------------------------------- +The Feature Proposal Template outlines the structure of the feature proposal and provides a guideline for the development team to follow. + +We have developed a GitHub Action that automatically appends the feature proposal template to the issue description once the issue is tagged with 'needs-feature-proposal.' Additionally, when someone is assigned to such an issue, an instructional comment is automatically added. +This comment reminds the assignee to fill out the feature proposal and provides a link to the relevant documentation for further guidance. + +.. note:: + The GitHub Action will be skipped for issues that are labeled with "bug". + +.. literalinclude:: ../../../.github/ISSUE_TEMPLATE/feature-proposal--developer-.md + :caption: Artemis/.github/ISSUE_TEMPLATE/feature-proposal--developer-.md + :language: markdown + + +Step 2: Requirements Engineering +-------------------------------- +In this section, the foundation for the feature's development is laid by meticulously defining both functional and non-functional requirements. +Functional requirements detail what the feature will do, covering specific behaviors or functions. +Non-functional requirements, on the other hand, address the feature's operation, such as performance, usability, reliability, and security specifications. + +In the Feature Proposal Template, you can find examples for structuring such functional and non-functional requirements + +Step 3: Analysis +---------------- +Within the analysis section, the emphasis is on understanding and documenting the feature's intended behavior and interactions within the system. +This includes creating an Analysis Object Model, which represents the static structure of the system using class diagrams to identify key entities and their relationships. +Additionally, dynamic aspects of the feature are explored through Activity Diagrams (for workflow representation), State Chart Diagrams (to model the feature's states and transitions), and Communication Diagrams (illustrating how objects interact to achieve the feature's functionality). +These models serve as a blueprint for the development and ensure a shared understanding among all people working on Artemis. + +Step 4: System Architecture +--------------------------- +In this step, the high-level structure of the system supporting the feature is outlined, focusing on analyzing the system components relevant to the implementation of the feature, +subsystem decomposition, interfaces, persistent data management, and security aspects. +The subsystem decomposition process is used to identify the main components of the system and define their responsibilities, which facilitates modular development and maintenance. +It is important to define the interfaces between these subsystems to ensure seamless interaction and integration. +In addition, the development of a comprehensive data model is important for a clear understanding of how data is securely stored, retrieved, and managed within the system, taking into account both logical and physical data perspectives. +Security aspects, especially access control mechanisms, are considered to ensure data integrity and privacy. + +Step 5: UI/UX Design +-------------------- +If the proposed feature requires a change to the software's user interface, this section should include a detailed description of the proposed changes, as well as a mockup that illustrates the new user interface. +The mockup should be created using Figma in conjunction with the [Artemis Design System](https://www.figma.com/files/team/1238833580691926997/Artemis) and should be added to the feature proposal as a screenshot. +Furthermore it is important to include a description of the user flow that references the dynamic model created in the analysis section. + +.. figure:: ./uiux_workflow.png + :align: center + :alt: UI/UX Design Workflow - Activity Diagram + :figclass: align-center + +.. raw:: html + + <div style="text-align: center;"> + <iframe src="https://live.rbg.tum.de/w/artemisintro/46233?video_only=1&t=0" allowfullscreen="1" frameborder="0" width="600" height="300"> + Watch this video on TUM-Live. + </iframe> + </div> + + +4. Create a local Branch +======================== +We use the following structure for branch names: ``<type>/<area>/<short-description>`` +- Possible types are: **feature**, **chore**, and **bugfix** +- Possible areas are all allowed feature tags (see :ref:`pr_naming_conventions`), written kebab-case. For example, ``Programming exercises`` becomes **programming-exercises**. + +**Branches that do not follow this structure will get rejected automatically!** + +5. Implement the Feature +======================== +In this step, the development team converts the detailed plans and designs outlined in the functional proposal into working code. +This step requires careful adherence to the previously defined requirements and system architecture to ensure that the function fits seamlessly into the existing system and fulfills the specified functional and performance criteria. + +.. note:: + Make sure to follow the `Artemis Code and Design Guidelines <https://docs.artemis.cit.tum.de/dev/guidelines.html>`_. + +6. Create a Pull Request +======================== +After the feature implementation is complete, the developer is required to create a pull request for integrating the feature into the develop branch. +The subsequent sections provide guidance on the naming conventions and outline the necessary steps for creating and merging a pull request. + +.. _pr_naming_conventions: + +Naming Conventions for GitHub Pull Requests +------------------------------------------- + +1. The first term is a main feature of Artemis and should use code highlighting, e.g. ``Programming exercises``: + + 1. Possible feature tags are: ``Programming exercises``, ``Integrated code lifecycle``, ``Quiz exercises``, ``Modeling exercises``, ``Text exercises``, ``File upload exercises``, ``Exam mode``, + ``Grading``, ``Assessment``, ``Communication``, ``Notifications``, ``Team exercises``, ``Lectures``, ``Integrated markdown editor``, ``Plagiarism checks``, ``Learning analytics``, + ``Adaptive learning``, ``Learning path``, ``Tutorial groups``, ``Iris``, ``Scalability``, ``Usability``, ``Performance``, ``Infrastructure``, ``Mobile apps``. + 2. If the change is not visible to end users, or it is a pure development or test improvement, we use the term ``Development``:. + 3. Everything else belongs to the ``General`` category. + +2. The colon is not highlighted. + +3. After the colon, there should be a verbal form that is understandable by end users and non-technical persons, because this will automatically become part of the release notes. + + 1. The text should be short, non-capitalized (except the first word) and should include the most important keywords. Do not repeat the feature if it is possible. + 2. We generally distinguish between bugfixes (the verb “Fix”) and improvements (all kinds of verbs) in the release notes. This should be immediately clear from the title. + 3. Good examples: + + - “Allow instructors to delete submissions in the participation detail view” + - “Fix an issue when clicking on the start exercise button” + - “Add the possibility for instructors to define submission policies” + +Steps to Create and Merge a Pull Request +---------------------------------------- + +0. Preconditions (For Developers Only) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* **Merge fast**: PRs should only be open for a couple of days. +* Limit yourself to one functionality per pull request. +* Split up your task in multiple branches & pull requests if necessary. +* `Commit Early, Commit Often, Perfect Later, Publish Once. <https://speakerdeck.com/lemiorhan/10-git-anti-patterns-you-should-be-aware-of>`_ + +1. Begin Implementation (For Developers Only) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* `Open a draft pull request. <https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request>`_ This allows for code related questions and discussions. + +2. Complete Implementation (For Developers Only) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* Make sure all steps in the `Checklist <https://github.com/ls1intum/Artemis/blob/develop/.github/PULL_REQUEST_TEMPLATE.md>`_ are completed. +* Add or update the "Steps for Testing" in the description of your pull request. +* Make sure that the changes in the pull request are only the ones necessary. +* Make sure that the PR is up-to-date with develop. +* Make sure at least all required checks pass. +* Mark the pull request as `ready for review. <https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request>`_ + +3. Review Process +^^^^^^^^^^^^^^^^^ +**For Developers:** + +* Organize or join a testing session. Especially for large pull requests this makes testing a lot easier. +* Actively look for reviews. Do not just open the pull request and wait. + +**For Reviewers:** + +* Perform the "Steps for Testing" and verify that the new functionality is working as expected. +* Verify that related functionality is still working as expected. +* Ensure that the code changes... + * conform to the code style and code design guidelines. + * are easily understandable. + * contain thorough documentation where applicable. + * maintain reasonable performance (e.g. no excessive/unnecessary database queries or HTTP calls). +* Respect the PR scope: Implementations that are unrelated to the PRs topic should not be enforced in a code review. +* Explain your rationale when requesting changes: E.g., not "Please change X to Y", but "Please change X to Y, because this would improve Z" +* Submit your comments and status (✅ Approve or ❌ Request Changes) using GitHub. + * Explain what you did (test, review code) and on which test server in the review comment. + +4. Address Review Comments +^^^^^^^^^^^^^^^^^^^^^^^^^^ +**For Developers:** + +* Use the pull request to discuss comments or ask questions. +* Update your code where necessary. +* Revert to draft if the changes will take a while during which review is not needed/possible. +* Set back to ready for review afterwards. +* After making revisions, re-request reviews through GitHub initially. If you receive no response, then follow up with a targeted message on Slack to the relevant reviewers. +* Comment on "inline comments" (e.g. "Done"). +* Once all changes are made, ensure that the feature-specific maintainer reviews and approves the changes. +* After maintainer approval, an "Artemis Maintainer" reviews and finalizes the approval process. + +**For Reviewers:** + +* Respond to questions raised by the developer. +* Mark conversations as resolved if the change is sufficient. +* Feature-specific maintainers should verify changes within their scope and mark them as approved. +* Artemis Maintainers conduct the final review before merging. + +.. note:: + Repeat steps 3 & 4 until the pull request is approved by all designated reviewers. It's only ready to merge when it has approvals from at least four reviewers. + + +5. Merge Changes +^^^^^^^^^^^^^^^^ +A project maintainer merges your changes into the ``develop`` branch. + +Stale Bot +--------- + +If the pull request doesn't have any activity for at least 7 days, the Stale Bot will mark the PR as `stale`. +The `stale` status can simply be removed by adding a comment or a commit to the PR. +After 21 total days of inactivity and 14 days after marking the PR as stale, the Stale Bot will close the PR. +Adding activity to the PR will remove the `stale` label again and reset the stale timer. +To prevent the bot from adding the `stale` label to the PR in the first place, the `no-stale` label can be used. +This label should only be utilized if the PR is blocked by another PR or the PR needs help from another developer. + +Further documentation on the Stale Bot can be found here: +https://github.com/actions/stale diff --git a/docs/dev/development-process/uiux_workflow.png b/docs/dev/development-process/uiux_workflow.png new file mode 100644 index 000000000000..73458415f3cf Binary files /dev/null and b/docs/dev/development-process/uiux_workflow.png differ diff --git a/docs/dev/guidelines/client.rst b/docs/dev/guidelines/client.rst index dac898809b57..e82d352d99df 100644 --- a/docs/dev/guidelines/client.rst +++ b/docs/dev/guidelines/client.rst @@ -28,8 +28,9 @@ Some general aspects: 3. Use PascalCase for enum values. 4. Use camelCase for function names. 5. Use camelCase for property names and local variables. -6. Do not use "_" as a prefix for private properties. -7. Use whole words in names when possible. +6. Use SCREAMING_SNAKE_CASE for constants, i.e. properties with the ``readonly`` keyword. +7. Do not use "_" as a prefix for private properties. +8. Use whole words in names when possible. 2. Components ============= @@ -470,3 +471,29 @@ Best Practices: 1. Dynamic Subscription Handling: Subscribe to topics on an as-needed basis. Unsubscribe from topics that are no longer needed to keep the number of active subscriptions within the recommended limit. 2. Efficient Topic Aggregation: Use topic aggregation techniques to consolidate related data streams into a single subscription wherever possible. Consequently, don't create a new topic if an existing one can be reused. 3. Small Messages: Send small messages and use DTOs. See :ref:`server-guideline-dto-usage` for more information and examples. + +19. Styling +=========== + +We are using `Scss <https://sass-lang.com>`_ to write modular, reusable css. We have a couple of global scss files in ``webapp/content/scss``, but encourage component dependent css using `Angular styleUrls <https://angular.io/guide/component-styles>`_. + +From a methodology viewpoint we encourage the use of `BEM <http://getbem.com/introduction/>`_: + +.. code-block:: scss + + .my-container { + // container styles + &__content { + // content styles + &--modifier { + // modifier styles + } + } + } + +Within the component html files, we encourage the use of `bootstrap css <https://getbootstrap.com/>`_: + +.. code-block:: html + + <div class="d-flex ms-2">some content</div> + diff --git a/docs/dev/guidelines/server.rst b/docs/dev/guidelines/server.rst index abda63d02027..07ed30cefd92 100644 --- a/docs/dev/guidelines/server.rst +++ b/docs/dev/guidelines/server.rst @@ -27,7 +27,27 @@ The main application is stored under ``/src/main`` and the main folders are: 1. Naming convention ==================== -All variables, methods and classes should use CamelCase style. The only difference: the first letter of any class should be capital. Most importantly use intention-revealing, pronounceable names. +All methods and classes should use camelCase style. The only difference: the first letter of any class should be capitalized. Most importantly, use intention-revealing, pronounceable names. +Variable names should also use camelCase style, where the first letter should be lowercase. For constants, i.e. arguments with the ``static final`` keywords, use all uppercase letters with underscores to separate words: SCREAMING_SNAKE_CASE. +The only exception to this rule is for the logger, which should be named ``log``. +Variable and constant names should also be intention-revealing and pronounceable. + +Example: + +.. code-block:: java + + public class ExampleClass { + private static final Logger log = LoggerFactory.getLogger(ExampleClass.class); + + private static final int MAXIMUM_NUMBER_OF_STUDENTS = 10; + + private final ExampleService exampleService; + + public void exampleMethod() { + int numberOfStudents = 0; + [...] + } + } 2. Single responsibility principle ================================== diff --git a/docs/index.rst b/docs/index.rst index 74297c2e9156..056dacd5ce2b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,7 +48,7 @@ All these exercises are supposed to be run either live in the lecture with insta :maxdepth: 3 dev/setup - dev/development-process + dev/development-process/development-process dev/guidelines dev/system-design dev/migration diff --git a/gradle.properties b/gradle.properties index 97ce34ef74a4..c68175ae46ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,12 +18,12 @@ jaxb_runtime_version=4.0.5 hazelcast_version=5.4.0 junit_version=5.10.2 mockito_version=5.11.0 -fasterxml_version=2.17.0 +fasterxml_version=2.17.1 jgit_version=6.9.0.202403050737-r -checkstyle_version=10.15.0 +checkstyle_version=10.16.0 jplag_version=5.0.0 slf4j_version=2.0.13 -sentry_version=7.8.0 +sentry_version=7.9.0 liquibase_version=4.27.0 docker_java_version=3.3.6 logback_version=1.5.6 diff --git a/jest.config.js b/jest.config.js index 1cf3a52771a9..2897ba2a88fc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -101,10 +101,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.3, - branches: 73.9, - functions: 81.8, - lines: 87.4, + statements: 87.38, + branches: 73.88, + functions: 81.80, + lines: 87.46, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index fbbc7827b592..d62a5e5e7e4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,27 @@ { "name": "artemis", - "version": "7.0.3", + "version": "7.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.0.3", + "version": "7.1.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "17.3.7", - "@angular/cdk": "17.3.7", - "@angular/common": "17.3.7", - "@angular/compiler": "17.3.7", - "@angular/core": "17.3.7", - "@angular/forms": "17.3.7", - "@angular/localize": "17.3.7", - "@angular/material": "17.3.7", - "@angular/platform-browser": "17.3.7", - "@angular/platform-browser-dynamic": "17.3.7", - "@angular/router": "17.3.7", - "@angular/service-worker": "17.3.7", + "@angular/animations": "17.3.8", + "@angular/cdk": "17.3.8", + "@angular/common": "17.3.8", + "@angular/compiler": "17.3.8", + "@angular/core": "17.3.8", + "@angular/forms": "17.3.8", + "@angular/localize": "17.3.8", + "@angular/material": "17.3.8", + "@angular/platform-browser": "17.3.8", + "@angular/platform-browser-dynamic": "17.3.8", + "@angular/router": "17.3.8", + "@angular/service-worker": "17.3.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "17.0.0", "@fingerprintjs/fingerprintjs": "4.3.0", @@ -30,12 +30,12 @@ "@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/free-regular-svg-icons": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2", - "@ls1intum/apollon": "3.3.12", + "@ls1intum/apollon": "3.3.14", "@ng-bootstrap/ng-bootstrap": "16.0.0", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.113.0", - "@sentry/tracing": "7.113.0", + "@sentry/angular-ivy": "7.114.0", + "@sentry/tracing": "7.114.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.3.0", "@vscode/codicons": "0.0.35", @@ -61,7 +61,7 @@ "ngx-infinite-scroll": "17.0.0", "ngx-webstorage": "13.0.1", "papaparse": "5.4.1", - "posthog-js": "1.130.2", + "posthog-js": "1.131.3", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -79,22 +79,22 @@ }, "devDependencies": { "@angular-builders/jest": "17.0.3", - "@angular-devkit/build-angular": "17.3.6", - "@angular-eslint/builder": "17.3.0", - "@angular-eslint/eslint-plugin": "17.3.0", - "@angular-eslint/eslint-plugin-template": "17.3.0", - "@angular-eslint/schematics": "17.3.0", - "@angular-eslint/template-parser": "17.3.0", - "@angular/cli": "17.3.6", - "@angular/compiler-cli": "17.3.7", - "@angular/language-service": "17.3.7", - "@sentry/types": "7.113.0", + "@angular-devkit/build-angular": "17.3.7", + "@angular-eslint/builder": "17.4.0", + "@angular-eslint/eslint-plugin": "17.4.0", + "@angular-eslint/eslint-plugin-template": "17.4.0", + "@angular-eslint/schematics": "17.4.0", + "@angular-eslint/template-parser": "17.4.0", + "@angular/cli": "17.3.7", + "@angular/compiler-cli": "17.3.8", + "@angular/language-service": "17.3.8", + "@sentry/types": "7.114.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "20.12.8", + "@types/node": "20.12.11", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", @@ -105,7 +105,7 @@ "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "2.0.0", - "eslint-plugin-jest": "28.3.0", + "eslint-plugin-jest": "28.5.0", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.1.3", "folder-hash": "4.0.4", @@ -116,11 +116,11 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.2.0", "jest-junit": "16.0.0", - "jest-preset-angular": "14.0.3", + "jest-preset-angular": "14.0.4", "lint-staged": "15.2.2", "ng-mocks": "14.12.2", "prettier": "3.2.5", - "sass": "1.76.0", + "sass": "1.77.0", "ts-jest": "29.1.2", "typescript": "5.4.5", "weak-napi": "2.0.2" @@ -178,13 +178,41 @@ "jest": ">=29" } }, + "node_modules/@angular-builders/jest/node_modules/jest-preset-angular": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.3.tgz", + "integrity": "sha512-usgBL7x0rXMnMSx8iEFeOozj50W6fp+YAmQcQBUdAXhN+PAXRy4UXL6I/rfcAOU09rnnq7RKsLsmhpp/fFEuag==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.15.13", + "jest-environment-jsdom": "^29.0.0", + "jest-util": "^29.0.0", + "pretty-format": "^29.0.0", + "ts-jest": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": ">=0.15.13" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=15.0.0 <18.0.0", + "@angular/compiler-cli": ">=15.0.0 <18.0.0", + "@angular/core": ">=15.0.0 <18.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <18.0.0", + "jest": "^29.0.0", + "typescript": ">=4.8" + } + }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.6.tgz", - "integrity": "sha512-Ck501FD/QuOjeKVFs7hU92w8+Ffetv0d5Sq09XY2/uygo5c/thMzp9nkevaIWBxUSeU5RqYZizDrhFVgYzbbOw==", + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.7.tgz", + "integrity": "sha512-SwXbdsZqEE3JtvujCLChAii+FA20d1931VDjDYffrGWdQEViTBAr4NKtDr/kOv8KkgiL3fhGibPnRNUHTeAMtg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.6", + "@angular-devkit/core": "17.3.7", "rxjs": "7.8.1" }, "engines": { @@ -194,15 +222,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.6.tgz", - "integrity": "sha512-K4CEZvhQZUUOpmXPVoI1YBM8BARbIlqE6FZRxakmnr+YOtVTYE5s+Dr1wgja8hZIohNz6L7j167G9Aut7oPU/w==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.7.tgz", + "integrity": "sha512-AsV80kiFMIPIhm3uzJgOHDj4u6JteUkZedPTKAFFFJC7CTat1luW5qx306vfF7wj62aMvUl5g9HFWaeLghTQGA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.6", - "@angular-devkit/build-webpack": "0.1703.6", - "@angular-devkit/core": "17.3.6", + "@angular-devkit/architect": "0.1703.7", + "@angular-devkit/build-webpack": "0.1703.7", + "@angular-devkit/core": "17.3.7", "@babel/core": "7.24.0", "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", @@ -213,7 +241,7 @@ "@babel/preset-env": "7.24.0", "@babel/runtime": "7.24.0", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.6", + "@ngtools/webpack": "17.3.7", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.18", @@ -340,12 +368,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.6.tgz", - "integrity": "sha512-pJu0et2SiF0kfXenHSTtAART0omzbWpLgBfeUo4hBh4uwX5IaT+mRpYpr8gCXMq+qsjoQp3HobSU3lPDeBn+bg==", + "version": "0.1703.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.7.tgz", + "integrity": "sha512-gpt2Ia5I1gmdp3hdbtB7tkZTba5qWmKeVhlCYswa/LvbceKmkjedoeNRAoyr1UKM9GeGqt6Xl1B2eHzCH+ykrg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.6", + "@angular-devkit/architect": "0.1703.7", "rxjs": "7.8.1" }, "engines": { @@ -359,9 +387,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.6.tgz", - "integrity": "sha512-FVbkT9dEwHEvjnxr4mvMNSMg2bCFoGoP4X68xXU9dhLEUpC05opLvfbaR3Qh543eCJ5AstosBFVzB/krfIkOvA==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.7.tgz", + "integrity": "sha512-qpZ7BShyqS/Jqld36E7kL02cyb2pjn1Az1p9439SbP8nsvJgYlsyjwYK2Kmcn/Wi+TZGIKxkqxgBBw9vqGgeJw==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -386,12 +414,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.6.tgz", - "integrity": "sha512-2G1YuPInd8znG7uUgKOS7z72Aku50lTzB/2csWkWPJLAFkh7vKC8QZ40x8S1nC9npVYPhI5CRLX/HVpBh9CyxA==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.7.tgz", + "integrity": "sha512-d7NKSwstdxYLYmPsbcYO3GOFNfXxXwOyHxSqDa1JNKoSzMdbLj4tvlCpfXw0ThNM7gioMx8aLBaaH1ac+yk06Q==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.6", + "@angular-devkit/core": "17.3.7", "jsonc-parser": "3.2.1", "magic-string": "0.30.8", "ora": "5.4.1", @@ -404,9 +432,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.3.0.tgz", - "integrity": "sha512-JXSZE7+KA3UGU6jwc0v9lwOIMptosrvLIOXGlXqrhHWEXfkfu3ENPq1Lm3K8jLndQ57XueEhC+Nab/AuUiWA/Q==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.4.0.tgz", + "integrity": "sha512-+3ujbi+ar/iqAAwnJ2bTdWzQpHh9iVEPgjHUOeQhrEM8gcaOLnZXMlUyZL7D+NlXg7aDoEIxETb73dgbIBm55A==", "dev": true, "dependencies": { "@nx/devkit": "^17.2.8 || ^18.0.0", @@ -418,19 +446,20 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.3.0.tgz", - "integrity": "sha512-ejfNzRuBeHUV8m2fkgs+M809rj5STuCuQo4fdfc6ccQpzXDI6Ha7BKpTznWfg5g529q/wrkoGSGgFxU9Yc2/dQ==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.4.0.tgz", + "integrity": "sha512-cYEJs4PO+QLDt1wfgWh9q8OjOphnoe1OTTFtMqm9lHl0AkBynPnFA6ghiiG5NaT03l7HXi2TQ23rLFlXl3JOBg==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.3.0.tgz", - "integrity": "sha512-81cQbOEPoQupFX8WmpqZn+y8VA7JdVRGBtt+uJNKBXcJknTpPWdLBZRFlgVakmC24iEZ0Fint/N3NBBQI3mz2A==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.4.0.tgz", + "integrity": "sha512-E+/O83PXttQUACurGEskLDU+wboBqMMVqvo4T8C/iMcpLx+01M5UBzqpCmfz6ri609G96Au7uDbUEedU1hwqmQ==", "dev": true, "dependencies": { - "@angular-eslint/utils": "17.3.0", - "@typescript-eslint/utils": "7.2.0" + "@angular-eslint/bundled-angular-compiler": "17.4.0", + "@angular-eslint/utils": "17.4.0", + "@typescript-eslint/utils": "7.8.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -438,15 +467,15 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.3.0.tgz", - "integrity": "sha512-9l/aRfpE9MCRVDWRb+rSB9Zei0paep1vqV6M/87VUnzBnzqeMRnVuPvQowilh2zweVSGKBF25Vp4HkwOL6ExDQ==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.4.0.tgz", + "integrity": "sha512-o1Vb7rt3TpPChVzaxswOKBDWRboMcpC4qUUyoHfeSYa7sDuQHMeIQlCS5QXuykR/RYnIQJSKd89FOd28nGmmRw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", - "@angular-eslint/utils": "17.3.0", - "@typescript-eslint/type-utils": "7.2.0", - "@typescript-eslint/utils": "7.2.0", + "@angular-eslint/bundled-angular-compiler": "17.4.0", + "@angular-eslint/utils": "17.4.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "aria-query": "5.3.0", "axobject-query": "4.0.0" }, @@ -456,13 +485,13 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.3.0.tgz", - "integrity": "sha512-5yssd5EOomxlKt9vN/OXXCTCuI3Pmfj16pkjBDoW0wzC8/M2l5zlXIEfoKumHYv2wtF553LhaMXVYVU35e0lTw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.4.0.tgz", + "integrity": "sha512-3WQQbwwBD1N3dZbbx1a1KY/jRujUQgz5778Ac21LU+AdCtvbjnmSpxRfsE3HH8MAreqr8Lv1kjLyiRzPTS5GQQ==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "17.3.0", - "@angular-eslint/eslint-plugin-template": "17.3.0", + "@angular-eslint/eslint-plugin": "17.4.0", + "@angular-eslint/eslint-plugin-template": "17.4.0", "@nx/devkit": "^17.2.8 || ^18.0.0", "ignore": "5.3.1", "nx": "^17.2.8 || ^18.0.0", @@ -474,12 +503,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.3.0.tgz", - "integrity": "sha512-m+UzAnWgtjeS0x6skSmR0eXltD/p7HZA+c8pPyAkiHQzkxE7ohhfyZc03yWGuYJvWQUqQAKKdO/nQop14TP0bg==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.4.0.tgz", + "integrity": "sha512-vT/Tg8dl6Uy++MS9lPS0l37SynH3EaMcggDiTJqn15pIb4ePO65fafOIIKKYG+BN6R6iFe/g9mH/9nb8ohlzdQ==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", + "@angular-eslint/bundled-angular-compiler": "17.4.0", "eslint-scope": "^8.0.0" }, "peerDependencies": { @@ -488,13 +517,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.3.0.tgz", - "integrity": "sha512-PJT9pxWqpvI9OXO+7L5SIVhvMW+RFjeafC7PYjtvSbNFpz+kF644BiAcfMJ0YqBnkrw3JXt+RAX25CT4mXIoXw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.4.0.tgz", + "integrity": "sha512-lHgRXyT878fauDITygraICDM6RHLb51QAJ3gWNZLr7SXcywsZg5d3rxRPCjrCnjgdxNPU0fJ+VJZ5AMt5Ibn7w==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.3.0", - "@typescript-eslint/utils": "7.2.0" + "@angular-eslint/bundled-angular-compiler": "17.4.0", + "@typescript-eslint/utils": "7.8.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -502,9 +531,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.7.tgz", - "integrity": "sha512-ahenGALPPweeHgqtl9BMkGIAV4fUNI5kOWUrLNbKBfwIJN+aOBOYV1Jz6NKUQq6eYn/1ZYtm0f3lIkHIdtLKEw==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.8.tgz", + "integrity": "sha512-ywT3dH0yZeAlo+Vu/6RpRozxzTbu4Bwqky6RgNfk/UMoyXZ5UiFStszDqO/HAyBGGCDHagm1XJkgsNZcStWq8A==", "dependencies": { "tslib": "^2.3.0" }, @@ -512,13 +541,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.7" + "@angular/core": "17.3.8" } }, "node_modules/@angular/cdk": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.7.tgz", - "integrity": "sha512-aFEh8tzKFOwini6aNEp57S54Ocp9T7YIJfBVMESptu2TCPdMTlJ1HJTg5XS8NcQO+vwi9cFPGVwGF1frOx4LXA==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.8.tgz", + "integrity": "sha512-9UQovtq1R3iGppBP6c1xgnokhG3LaUObpm6htMyuQ2v034WinemoeMdHbqs/OvyUbqOUttQI/9vz37TVB0DjXA==", "dependencies": { "tslib": "^2.3.0" }, @@ -532,15 +561,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.6.tgz", - "integrity": "sha512-poKaRPeI+hFqX+AxIaEriaIggFVcC3XqlT9E1/uBC2rfHirE1n5F9Z7xqEDtMHduKwLbNXhQIPoKIKya8+Hnew==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.7.tgz", + "integrity": "sha512-JgCav3sdRCoJHwLXxmF/EMzArYjwbqB+AGUW/xIR98oZET8QxCB985bOFUAm02SkAEUVcMJvjxec+WCaa60m/A==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.6", - "@angular-devkit/core": "17.3.6", - "@angular-devkit/schematics": "17.3.6", - "@schematics/angular": "17.3.6", + "@angular-devkit/architect": "0.1703.7", + "@angular-devkit/core": "17.3.7", + "@angular-devkit/schematics": "17.3.7", + "@schematics/angular": "17.3.7", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -566,9 +595,9 @@ } }, "node_modules/@angular/common": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.7.tgz", - "integrity": "sha512-A7LRJu1vVCGGgrfZXjU+njz50SiU4weheKCar5PIUprcdIofS1IrHAJDqYh+kwXxkjXbZMOr/ijQY0+AESLEsw==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.8.tgz", + "integrity": "sha512-HEhTibrsWmoKilyhvAFmqg4SH1hWBP3eV9Y689lmsxBQCTRAmRI2pMAoRKQ+dBcoYLE/FZhcmdHJUSl5jR7Isg==", "dependencies": { "tslib": "^2.3.0" }, @@ -576,14 +605,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.7", + "@angular/core": "17.3.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.7.tgz", - "integrity": "sha512-AlKiqPoxnrpQ0hn13fIaQPSVodaVAIjBW4vpFyuKFqs2LBKg6iolwZ21s8rEI0KR2gXl+8ugj0/UZ6YADiVM5w==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.8.tgz", + "integrity": "sha512-7vZSh2Oa95lZdRR4MhE0icvZ7JUuYY+NSo3eTSOMZSlH5I9rtwQoSFqfoGW+35rXCzGFLOhQmZBbXkxDPDs97Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -591,7 +620,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.3.7" + "@angular/core": "17.3.8" }, "peerDependenciesMeta": { "@angular/core": { @@ -600,9 +629,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.7.tgz", - "integrity": "sha512-vSg5IQZ9jGmvYjpbfH8KbH4Sl1IVeE+Mr1ogcxkGEsURSRvKo7EWc0K7LSEI9+gg0VLamMiP9EyCJdPxiJeLJQ==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.8.tgz", + "integrity": "sha512-/TsbCmk7QJUEEZnRdNzi6znsPfoDJuy6vHDqcwWVEcw7y6W7DjirSFmtT9u1QwrV67KM6kOh22+RvPdGM8sPmg==", "dependencies": { "@babel/core": "7.23.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -622,7 +651,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.3.7", + "@angular/compiler": "17.3.8", "typescript": ">=5.2 <5.5" } }, @@ -661,9 +690,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@angular/core": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.7.tgz", - "integrity": "sha512-HWcrbxqnvIMSxFuQdN0KPt08bc87hqr0LKm89yuRTUwx/2sNJlNQUobk6aJj4trswGBttcRDT+GOS4DQP2Nr4g==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.8.tgz", + "integrity": "sha512-+tUQ+B1yVvNbczzaWBCgJWWIgZ2z+GVJWu+UNOHHWzdqD8qpXjuIkDfnhyLNeGvvXgsqey4u6ApFf2SoFYLjuA==", "dependencies": { "tslib": "^2.3.0" }, @@ -676,9 +705,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.7.tgz", - "integrity": "sha512-FEhXh/VmT++XCoO8i7bBtzxG7Am/cE1zrr9aF+fWW+4jpWvJvVN1IaSiJxgBB+iPsOJ9lTBRwfRW3onlcDkhrw==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.8.tgz", + "integrity": "sha512-ZoPJMx3O1eKliK6oEUqtKJNqrLwwOLBC5x+zbCHrwJeBB3lbgWXrrnTrFvCXpp3QVERAboZTzZ3XBmHX1o6gmw==", "dependencies": { "tslib": "^2.3.0" }, @@ -686,25 +715,25 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "17.3.8", + "@angular/core": "17.3.8", + "@angular/platform-browser": "17.3.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.7.tgz", - "integrity": "sha512-mt/Q2Hp4B0vFbOp+L709sN0zQRD0Cojfneo6XrHHHRYWpHuaQUhaWGp2ney7X6BgwqMubpxSWb0+5f0R6GRgjw==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.8.tgz", + "integrity": "sha512-Vyad/h0FSgLF17STiJujlOeulRq/PSmH+5sUtd3Zsw4jcy2C0QRr4FaP5s9ZidMMAnfMMFlc5Sh/0QEJV/dbJQ==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0" } }, "node_modules/@angular/localize": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.3.7.tgz", - "integrity": "sha512-GidwcxquawJBZXNQs6cJ3GvmyowupW9JFkG5sVsS6KG4yu9SIt4FZC+EbrVtYDhXI3U2wxGkm+9vDKvkSGzG0g==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.3.8.tgz", + "integrity": "sha512-2Ddv58fmCfow/pBvvOzkNIc/pBtZpu6Ow0em+6Hx8ln6wwZaFHEPhe6t5SaRG2GTXWUqAnsjWSdNLlAviXOxtg==", "dependencies": { "@babel/core": "7.23.9", "@types/babel__core": "7.20.5", @@ -720,8 +749,8 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.3.7", - "@angular/compiler-cli": "17.3.7" + "@angular/compiler": "17.3.8", + "@angular/compiler-cli": "17.3.8" } }, "node_modules/@angular/localize/node_modules/@babel/core": { @@ -759,9 +788,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@angular/material": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.7.tgz", - "integrity": "sha512-wjSKkk9KZE8QiBPkMd5axh5u/3pUSxoLKNO7OasFhEagMmSv5oYTLm40cErhtb4UdkSmbC19WuuluS6P3leoPA==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.8.tgz", + "integrity": "sha512-P15p3ixO119DvqtFPCUc+9uKlFgwrwoZtKstcdx/knFlw9c+wS5s9SZzTbB2yqjZoBZ4gC92kqbUQI2o7AUbUQ==", "dependencies": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -814,7 +843,7 @@ }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.7", + "@angular/cdk": "17.3.8", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", @@ -823,9 +852,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.7.tgz", - "integrity": "sha512-Nn8ZMaftAvO9dEwribWdNv+QBHhYIBrRkv85G6et80AXfXoYAr/xcfnQECRFtZgPmANqHC5auv/xrmExQG+Yeg==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.8.tgz", + "integrity": "sha512-UMGSV3TdJqMtf2xvhbW6fx8TKJLOoHQgFxohhy3y8GvxHBu+PUyrwhovb7r03bs+muY6u4ygGCMm7Mt1TFVwfQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -833,9 +862,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.3.7", - "@angular/common": "17.3.7", - "@angular/core": "17.3.7" + "@angular/animations": "17.3.8", + "@angular/common": "17.3.8", + "@angular/core": "17.3.8" }, "peerDependenciesMeta": { "@angular/animations": { @@ -844,9 +873,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.7.tgz", - "integrity": "sha512-9c2I4u0L1p2v1/lW8qy+WaNHisUWbyy6wqsv2v9FfCaSM49Lxymgo9LPFPC4qEG5ei5nE+eIQ2ocRiXXsf5QkQ==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.8.tgz", + "integrity": "sha512-uL6FPh+Pr9xzIjyiv3p66jteq/CytHP1+m5jOsIKa1LUwTXx0a2pmOYcZxXpNkQGR9Ir/dlbrYmKlSP3QZf7uw==", "dependencies": { "tslib": "^2.3.0" }, @@ -854,16 +883,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/compiler": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7" + "@angular/common": "17.3.8", + "@angular/compiler": "17.3.8", + "@angular/core": "17.3.8", + "@angular/platform-browser": "17.3.8" } }, "node_modules/@angular/router": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.7.tgz", - "integrity": "sha512-lMkuRrc1ZjP5JPDxNHqoAhB0uAnfPQ/q6mJrw1s8IZoVV6VyM+FxR5r13ajNcXWC38xy/YhBjpXPF1vBdxuLXg==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.8.tgz", + "integrity": "sha512-2JKTW1u1H+iNDfAmIjEiMJjQHfzb97TBk23/euIR0JuyGHjyywkrQ97HHiOEAJyy/Zpr0Vbem3HRqDqSfjTWvg==", "dependencies": { "tslib": "^2.3.0" }, @@ -871,16 +900,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "17.3.8", + "@angular/core": "17.3.8", + "@angular/platform-browser": "17.3.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-17.3.7.tgz", - "integrity": "sha512-x09Mr0QarAgGusG8Hei6cqBd0tEG9sWaSxp29yvSemAkuG9APewm1WJk0RnuDmjIB7g1Z+FQlZfe+Lo/zyms6A==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-17.3.8.tgz", + "integrity": "sha512-TlITQMosCsHzDajbq9Fx7fdaLVEc1m5CqrutTPeCFP30fMNNBavVplJ7338l/fjXtfTc/f37ccacH3nGSA/wIQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -891,8 +920,8 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7" + "@angular/common": "17.3.8", + "@angular/core": "17.3.8" } }, "node_modules/@babel/code-frame": { @@ -4221,9 +4250,9 @@ } }, "node_modules/@jsonjoy.com/base64": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.1.tgz", - "integrity": "sha512-LnFjVChaGY8cZVMwAIMjvA1XwQjZ/zIXHyh28IyJkyNkzof4Dkm1+KN9UIm3lHhREH4vs7XwZ0NpkZKnwOtEfg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", "dev": true, "engines": { "node": ">=10.0" @@ -4237,9 +4266,9 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.0.3.tgz", - "integrity": "sha512-Q0SPAdmK6s5Fe3e1kcNvwNyk6e2+CxM8XZdGbf4abZG7nUO05KSie3/iX29loTBuY+75uVP6RixDSPVpotfzmQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz", + "integrity": "sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==", "dev": true, "dependencies": { "@jsonjoy.com/base64": "^1.1.1", @@ -4259,9 +4288,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.1.2.tgz", - "integrity": "sha512-HOGa9wtE6LEz2I5mMQ2pMSjth85PmD71kPbsecs02nEUq3/Kw0wRK3gmZn5BCEB8mFLXByqPxjHgApoMwIPMKQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.1.3.tgz", + "integrity": "sha512-g//kkF4kOwUjemValCtOc/xiYzmwMRmWq3Bn+YnzOzuZLHq2PpMOxxIayN3cKbo7Ko2Np65t6D9H81IvXbXhqg==", "dev": true, "engines": { "node": ">=10.0" @@ -4293,9 +4322,9 @@ } }, "node_modules/@ls1intum/apollon": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/@ls1intum/apollon/-/apollon-3.3.12.tgz", - "integrity": "sha512-6vWYlJZJtyieZ+phCPwrvDRw0oQCIRICNYxDa4Vn/p9FYHCJkggM8wD3ERO2moli8eHcz8+efnmbcdxJClL9WQ==", + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/@ls1intum/apollon/-/apollon-3.3.14.tgz", + "integrity": "sha512-XN6M72Oeuw7Dv1ZLkU6wZVFcCuYIZXWKNH5ZG9+QraCdeaihbBADhaX7AY89LUAnjNMq0WmO5evb54RcfobxAw==", "dependencies": { "fast-json-patch": "3.1.1", "is-mobile": "4.0.0", @@ -5086,9 +5115,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.6.tgz", - "integrity": "sha512-equxbgh2DKzZtiFMoVf1KD4yJcH1q8lpqQ/GSPPQUvONcmHrr+yqdRUdaJ7oZCyCYmXF/nByBxtMKtJr6nKZVg==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.7.tgz", + "integrity": "sha512-kQNS68jsPQlaWAnKcVeFKNHp6K90uQANvq+9oXb/i+JnYWzuBsHzn2r8bVdMmvjd1HdBRiGtg767XRk3u+jgRw==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -5199,9 +5228,9 @@ } }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -5211,9 +5240,9 @@ } }, "node_modules/@npmcli/git": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.6.tgz", - "integrity": "sha512-4x/182sKXmQkf0EtXxT26GEsaOATpD7WVtza5hrYivWZeo6QefC6xq9KAXrnjtFKBZ4rZwR7aX/zClYYXgtwLw==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", + "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", @@ -5346,9 +5375,9 @@ } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", - "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", "dev": true, "dependencies": { "which": "^4.0.0" @@ -5922,13 +5951,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.6.tgz", - "integrity": "sha512-jCNZdjHSVrI8TrrCnCoXC8GYvQRj7zh+SDdmm91Ve8dbikYNmBOKYLuPaCTsmojWx7ytv962yLlgKzpaa2bbfw==", + "version": "17.3.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.7.tgz", + "integrity": "sha512-HaJroKaberriP4wFefTTSVFrtU9GMvnG3I6ELbOteOyKMH7o2V91FXGJDJ5KnIiLRlBmC30G3r+9Ybc/rtAYkw==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.6", - "@angular-devkit/schematics": "17.3.6", + "@angular-devkit/core": "17.3.7", + "@angular-devkit/schematics": "17.3.7", "jsonc-parser": "3.2.1" }, "engines": { @@ -5938,54 +5967,54 @@ } }, "node_modules/@sentry-internal/feedback": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.113.0.tgz", - "integrity": "sha512-eEmL8QXauUnM3FXGv0GT29RpL0Jo0pkn/uMu3aqjhQo7JKNqUGVYIUxJxiGWbVMbDXqPQ7L66bjjMS3FR1GM2g==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.114.0.tgz", + "integrity": "sha512-kUiLRUDZuh10QE9JbSVVLgqxFoD9eDPOzT0MmzlPuas8JlTmJuV4FtSANNcqctd5mBuLt2ebNXH0MhRMwyae4A==", "dependencies": { - "@sentry/core": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry/core": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.113.0.tgz", - "integrity": "sha512-K8uA42aobNF/BAXf14el15iSAi9fonLBUrjZi6nPDq7zaA8rPvfcTL797hwCbqkETz2zDf52Jz7I3WFCshDoUw==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.114.0.tgz", + "integrity": "sha512-6rTiqmKi/FYtesdM2TM2U+rh6BytdPjLP65KTUodtxohJ+r/3m+termj2o4BhIYPE1YYOZNmbZfwebkuQPmWeg==", "dependencies": { - "@sentry/core": "7.113.0", - "@sentry/replay": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry/core": "7.114.0", + "@sentry/replay": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/tracing": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.113.0.tgz", - "integrity": "sha512-8MDnYENRMnEfQjvN4gkFYFaaBSiMFSU/6SQZfY9pLI3V105z6JQ4D0PGMAUVowXilwNZVpKNYohE7XByuhEC7Q==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.114.0.tgz", + "integrity": "sha512-dOuvfJN7G+3YqLlUY4HIjyWHaRP8vbOgF+OsE5w2l7ZEn1rMAaUbPntAR8AF9GBA6j2zWNoSo8e7GjbJxVofSg==", "dependencies": { - "@sentry/core": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry/core": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/angular-ivy": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.113.0.tgz", - "integrity": "sha512-bHFkDlE9jyE1sJmy3B/HQ9X4vkOAFbk+UFPPfVL25kuWIVELHuTYPpLy/xXBb1vBFG/UPHzTg+mKnqgMZHxgjQ==", - "dependencies": { - "@sentry/browser": "7.113.0", - "@sentry/core": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/angular-ivy/-/angular-ivy-7.114.0.tgz", + "integrity": "sha512-fgiyB2N6UzXwGL7rMHLTqeLIm3ZCxHKRqcQP5P/2DTnJAsyxgx0e4CYZV236IgpPFl1JndCXMYSD/aJQPH6h7Q==", + "dependencies": { + "@sentry/browser": "7.114.0", + "@sentry/core": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0", "tslib": "^2.4.1" }, "engines": { @@ -5999,43 +6028,43 @@ } }, "node_modules/@sentry/browser": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.113.0.tgz", - "integrity": "sha512-PdyVHPOprwoxGfKGsP2dXDWO0MBDW1eyP7EZlfZvM1A4hjk6ZRNfCv30g+TrqX4hiZDKzyqN3+AdP7N/J2IX0Q==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.114.0.tgz", + "integrity": "sha512-ijJ0vOEY6U9JJADVYGkUbLrAbpGSQgA4zV+KW3tcsBLX9M1jaWq4BV1PWHdzDPPDhy4OgfOjIfaMb5BSPn1U+g==", "dependencies": { - "@sentry-internal/feedback": "7.113.0", - "@sentry-internal/replay-canvas": "7.113.0", - "@sentry-internal/tracing": "7.113.0", - "@sentry/core": "7.113.0", - "@sentry/integrations": "7.113.0", - "@sentry/replay": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry-internal/feedback": "7.114.0", + "@sentry-internal/replay-canvas": "7.114.0", + "@sentry-internal/tracing": "7.114.0", + "@sentry/core": "7.114.0", + "@sentry/integrations": "7.114.0", + "@sentry/replay": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/core": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.113.0.tgz", - "integrity": "sha512-pg75y3C5PG2+ur27A0Re37YTCEnX0liiEU7EOxWDGutH17x3ySwlYqLQmZsFZTSnvzv7t3MGsNZ8nT5O0746YA==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.114.0.tgz", + "integrity": "sha512-YnanVlmulkjgZiVZ9BfY9k6I082n+C+LbZo52MTvx3FY6RE5iyiPMpaOh67oXEZRWcYQEGm+bKruRxLVP6RlbA==", "dependencies": { - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/integrations": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.113.0.tgz", - "integrity": "sha512-w0sspGBQ+6+V/9bgCkpuM3CGwTYoQEVeTW6iNebFKbtN7MrM3XsGAM9I2cW1jVxFZROqCBPFtd2cs5n0j14aAg==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.114.0.tgz", + "integrity": "sha512-BJIBWXGKeIH0ifd7goxOS29fBA8BkEgVVCahs6xIOXBjX1IRS6PmX0zYx/GP23nQTfhJiubv2XPzoYOlZZmDxg==", "dependencies": { - "@sentry/core": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0", + "@sentry/core": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0", "localforage": "^1.8.1" }, "engines": { @@ -6043,44 +6072,44 @@ } }, "node_modules/@sentry/replay": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.113.0.tgz", - "integrity": "sha512-UD2IaphOWKFdeGR+ZiaNAQ+wFsnwbJK6PNwcW6cHmWKv9COlKufpFt06lviaqFZ8jmNrM4H+r+R8YVTrqCuxgg==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.114.0.tgz", + "integrity": "sha512-UvEajoLIX9n2poeW3R4Ybz7D0FgCGXoFr/x/33rdUEMIdTypknxjJWxg6fJngIduzwrlrvWpvP8QiZXczYQy2Q==", "dependencies": { - "@sentry-internal/tracing": "7.113.0", - "@sentry/core": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry-internal/tracing": "7.114.0", + "@sentry/core": "7.114.0", + "@sentry/types": "7.114.0", + "@sentry/utils": "7.114.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/tracing": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.113.0.tgz", - "integrity": "sha512-eE7fcIqcIpLAdgt2GKzdHobK802Jf66qo3MywoCAj0BaVuSZpGi4SET/Fcb+ca/s7x65uoaOi1EnmF7SsZpdcA==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.114.0.tgz", + "integrity": "sha512-eldEYGADReZ4jWdN5u35yxLUSTOvjsiZAYd4KBEpf+Ii65n7g/kYOKAjNl7tHbrEG1EsMW4nDPWStUMk1w+tfg==", "dependencies": { - "@sentry-internal/tracing": "7.113.0" + "@sentry-internal/tracing": "7.114.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/types": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.113.0.tgz", - "integrity": "sha512-PJbTbvkcPu/LuRwwXB1He8m+GjDDLKBtu3lWg5xOZaF5IRdXQU2xwtdXXsjge4PZR00tF7MO7X8ZynTgWbYaew==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.114.0.tgz", + "integrity": "sha512-tsqkkyL3eJtptmPtT0m9W/bPLkU7ILY7nvwpi1hahA5jrM7ppoU0IMaQWAgTD+U3rzFH40IdXNBFb8Gnqcva4w==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.113.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.113.0.tgz", - "integrity": "sha512-nzKsErwmze1mmEsbW2AwL2oB+I5v6cDEJY4sdfLekA4qZbYZ8pV5iWza6IRl4XfzGTE1qpkZmEjPU9eyo0yvYw==", + "version": "7.114.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.114.0.tgz", + "integrity": "sha512-319N90McVpupQ6vws4+tfCy/03AdtsU0MurIE4+W5cubHME08HtiEWlfacvAxX+yuKFhvdsO4K4BB/dj54ideg==", "dependencies": { - "@sentry/types": "7.113.0" + "@sentry/types": "7.114.0" }, "engines": { "node": ">=8" @@ -6117,28 +6146,39 @@ } }, "node_modules/@sigstore/sign": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.0.tgz", - "integrity": "sha512-tsAyV6FC3R3pHmKS880IXcDJuiFJiKITO1jxR1qbplcsBkZLBmjrEw5GbC7ikD6f5RU1hr7WnmxB/2kKc1qUWQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.1.tgz", + "integrity": "sha512-YZ71wKIOweC8ViUeZXboz0iPLqMkskxuoeN/D1CEpAyZvEepbX9oRMIoO6a/DxUqO1VEaqmcmmqzSiqtOsvSmw==", "dev": true, "dependencies": { "@sigstore/bundle": "^2.3.0", "@sigstore/core": "^1.0.0", "@sigstore/protobuf-specs": "^0.3.1", - "make-fetch-happen": "^13.0.0" + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@sigstore/tuf": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.2.tgz", - "integrity": "sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.3.tgz", + "integrity": "sha512-agQhHNkIddXFslkudjV88vTXiAMEyUtso3at6ZHUNJ1agZb7Ze6VW/PddHipdWBu1t+8OWLW5X5yZOPiOnaWJQ==", "dev": true, "dependencies": { "@sigstore/protobuf-specs": "^0.3.0", - "tuf-js": "^2.2.0" + "tuf-js": "^2.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -6339,13 +6379,13 @@ } }, "node_modules/@tufjs/models": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz", - "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", "dev": true, "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.3" + "minimatch": "^9.0.4" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -6591,9 +6631,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", - "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", + "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==", "dev": true }, "node_modules/@types/lodash-es": { @@ -6612,9 +6652,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", - "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -6821,58 +6861,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", - "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/@typescript-eslint/parser": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", @@ -6919,18 +6907,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", - "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -6945,79 +6933,6 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", - "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", - "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", @@ -7060,21 +6975,21 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", - "semver": "^7.5.4" + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "semver": "^7.6.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -7084,96 +6999,6 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", - "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", - "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", - "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", @@ -8301,15 +8126,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -8320,9 +8136,9 @@ } }, "node_modules/cacache": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", - "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", + "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -8419,9 +8235,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001615", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", - "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", + "version": "1.0.30001617", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz", + "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==", "funding": [ { "type": "opencollective", @@ -9994,9 +9810,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.754", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", - "integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==" + "version": "1.4.761", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.761.tgz", + "integrity": "sha512-PIbxpiJGx6Bb8dQaonNc6CGTRlVntdLg/2nMa1YhnrwYOORY9a3ZgGN0UQYE6lAcj/lkyduJN7BPt/JiY+jAQQ==" }, "node_modules/emittery": { "version": "0.13.1", @@ -10066,9 +9882,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", + "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -10468,12 +10284,12 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.3.0.tgz", - "integrity": "sha512-5LjCSSno8E+IUCOX4hJiIb/upPIgpkaDEcaN/40gOcw26t/5UTLHFc4JdxKjOOvGTh0XdCu+fNr0fpOVNvcxMA==", + "version": "28.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.5.0.tgz", + "integrity": "sha512-6np6DGdmNq/eBbA7HOUNV8fkfL86PYwBfwyb8n23FXgJNTR8+ot3smRHjza9LGsBBZRypK3qyF79vMjohIL8eQ==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^6.0.0" + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" }, "engines": { "node": "^16.10.0 || ^18.12.0 || >=20.0.0" @@ -10629,121 +10445,6 @@ "node": ">=4.0" } }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-jest/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -12000,9 +11701,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "dependencies": { "lru-cache": "^10.0.1" @@ -12292,9 +11993,9 @@ } }, "node_modules/ignore-walk": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", - "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -12845,9 +12546,9 @@ } }, "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dev": true, "dependencies": { "async": "^3.2.3", @@ -13861,9 +13562,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.3.tgz", - "integrity": "sha512-usgBL7x0rXMnMSx8iEFeOozj50W6fp+YAmQcQBUdAXhN+PAXRy4UXL6I/rfcAOU09rnnq7RKsLsmhpp/fFEuag==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.4.tgz", + "integrity": "sha512-O4WhVRdfiN9TtJMbJbuVJxD3zn6fyOF2Pqvu12fvEVR6FxCN1S1POfR2nU1fRdP+rQZv7iiW+ttxsy+qkE8iCw==", "dev": true, "dependencies": { "bs-logger": "^0.2.6", @@ -14777,9 +14478,9 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -15937,9 +15638,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz", + "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -15958,9 +15659,9 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, "dependencies": { "minipass": "^7.0.3", @@ -16442,9 +16143,9 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", - "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "dependencies": { "abbrev": "^2.0.0" @@ -16457,9 +16158,9 @@ } }, "node_modules/normalize-package-data": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", + "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", "dev": true, "dependencies": { "hosted-git-info": "^7.0.0", @@ -16489,9 +16190,9 @@ } }, "node_modules/npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", "dev": true, "dependencies": { "npm-normalize-package-bin": "^3.0.0" @@ -17630,9 +17331,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/posthog-js": { - "version": "1.130.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.130.2.tgz", - "integrity": "sha512-QR/j9Xs/STK3+VJgqiByeXFKT17LGZZvJtrCdgFhwydp8WfisJw7zrSy7rVDjYS0UeKJJ/3cO/qtlXD3dR2+Eg==", + "version": "1.131.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.131.3.tgz", + "integrity": "sha512-ds/TADDS+rT/WgUyeW4cJ+X+fX+O1KdkOyssNI/tP90PrFf0IJsck5B42YOLhfz87U2vgTyBaKHkdlMgWuOFog==", "dependencies": { "fflate": "^0.4.8", "preact": "^10.19.3" @@ -18033,9 +17734,9 @@ } }, "node_modules/read-package-json": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz", - "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", + "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", "dev": true, "dependencies": { "glob": "^10.2.2", @@ -18478,9 +18179,9 @@ "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" }, "node_modules/sass": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz", - "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.0.tgz", + "integrity": "sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -19052,9 +18753,9 @@ } }, "node_modules/sonic-forest": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sonic-forest/-/sonic-forest-1.0.2.tgz", - "integrity": "sha512-2rICdwIJi5kVlehMUVtJeHn3ohh5YZV4pDv0P0c1M11cRz/gXNViItpM94HQwfvnXuzybpqK0LZJgTa3lEwtAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sonic-forest/-/sonic-forest-1.0.3.tgz", + "integrity": "sha512-dtwajos6IWMEWXdEbW1IkEkyL2gztCAgDplRIX+OT5aRKnEd5e7r7YCxRgXZdhRP1FBdOBf8axeTPhzDv8T4wQ==", "dev": true, "dependencies": { "tree-dump": "^1.0.0" @@ -19227,9 +18928,9 @@ "dev": true }, "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, "dependencies": { "minipass": "^7.0.3" @@ -20086,14 +19787,14 @@ "dev": true }, "node_modules/tuf-js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", - "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", "dev": true, "dependencies": { - "@tufjs/models": "2.0.0", + "@tufjs/models": "2.0.1", "debug": "^4.3.4", - "make-fetch-happen": "^13.0.0" + "make-fetch-happen": "^13.0.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -20282,9 +19983,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", - "integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", + "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", "funding": [ { "type": "opencollective", @@ -20399,13 +20100,10 @@ } }, "node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } diff --git a/package.json b/package.json index 1d63e654a779..6284ecdec0e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.0.3", + "version": "7.1.0", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT", @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "17.3.7", - "@angular/cdk": "17.3.7", - "@angular/common": "17.3.7", - "@angular/compiler": "17.3.7", - "@angular/core": "17.3.7", - "@angular/forms": "17.3.7", - "@angular/localize": "17.3.7", - "@angular/material": "17.3.7", - "@angular/platform-browser-dynamic": "17.3.7", - "@angular/platform-browser": "17.3.7", - "@angular/router": "17.3.7", - "@angular/service-worker": "17.3.7", + "@angular/animations": "17.3.8", + "@angular/cdk": "17.3.8", + "@angular/common": "17.3.8", + "@angular/compiler": "17.3.8", + "@angular/core": "17.3.8", + "@angular/forms": "17.3.8", + "@angular/localize": "17.3.8", + "@angular/material": "17.3.8", + "@angular/platform-browser-dynamic": "17.3.8", + "@angular/platform-browser": "17.3.8", + "@angular/router": "17.3.8", + "@angular/service-worker": "17.3.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "17.0.0", "@fingerprintjs/fingerprintjs": "4.3.0", @@ -33,12 +33,12 @@ "@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/free-regular-svg-icons": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2", - "@ls1intum/apollon": "3.3.12", + "@ls1intum/apollon": "3.3.14", "@ng-bootstrap/ng-bootstrap": "16.0.0", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular-ivy": "7.113.0", - "@sentry/tracing": "7.113.0", + "@sentry/angular-ivy": "7.114.0", + "@sentry/tracing": "7.114.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.3.0", "@vscode/codicons": "0.0.35", @@ -64,7 +64,7 @@ "ngx-infinite-scroll": "17.0.0", "ngx-webstorage": "13.0.1", "papaparse": "5.4.1", - "posthog-js": "1.130.2", + "posthog-js": "1.131.3", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -109,22 +109,22 @@ }, "devDependencies": { "@angular-builders/jest": "17.0.3", - "@angular-devkit/build-angular": "17.3.6", - "@angular-eslint/builder": "17.3.0", - "@angular-eslint/eslint-plugin": "17.3.0", - "@angular-eslint/eslint-plugin-template": "17.3.0", - "@angular-eslint/schematics": "17.3.0", - "@angular-eslint/template-parser": "17.3.0", - "@angular/cli": "17.3.6", - "@angular/compiler-cli": "17.3.7", - "@angular/language-service": "17.3.7", - "@sentry/types": "7.113.0", + "@angular-devkit/build-angular": "17.3.7", + "@angular-eslint/builder": "17.4.0", + "@angular-eslint/eslint-plugin": "17.4.0", + "@angular-eslint/eslint-plugin-template": "17.4.0", + "@angular-eslint/schematics": "17.4.0", + "@angular-eslint/template-parser": "17.4.0", + "@angular/cli": "17.3.7", + "@angular/compiler-cli": "17.3.8", + "@angular/language-service": "17.3.8", + "@sentry/types": "7.114.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "20.12.8", + "@types/node": "20.12.11", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", @@ -135,7 +135,7 @@ "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "2.0.0", - "eslint-plugin-jest": "28.3.0", + "eslint-plugin-jest": "28.5.0", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.1.3", "folder-hash": "4.0.4", @@ -146,11 +146,11 @@ "jest-extended": "4.0.2", "jest-fail-on-console": "3.2.0", "jest-junit": "16.0.0", - "jest-preset-angular": "14.0.3", + "jest-preset-angular": "14.0.4", "lint-staged": "15.2.2", "ng-mocks": "14.12.2", "prettier": "3.2.5", - "sass": "1.76.0", + "sass": "1.77.0", "ts-jest": "29.1.2", "typescript": "5.4.5", "weak-napi": "2.0.2" diff --git a/src/main/java/de/tum/in/www1/artemis/config/ArtemisSpringManagedContext.java b/src/main/java/de/tum/in/www1/artemis/config/ArtemisSpringManagedContext.java deleted file mode 100644 index 2a9a857e3f4e..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/config/ArtemisSpringManagedContext.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.tum.in.www1.artemis.config; - -import static tech.jhipster.config.JHipsterConstants.SPRING_PROFILE_TEST; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.core.env.Environment; -import org.springframework.core.env.Profiles; - -import com.hazelcast.spring.context.SpringManagedContext; - -/** - * This class only exists to improve logging in case of slow Hazelcast operations - */ -public class ArtemisSpringManagedContext extends SpringManagedContext { - - private static final Logger log = LoggerFactory.getLogger(ArtemisSpringManagedContext.class); - - private final Environment env; - - public ArtemisSpringManagedContext(ApplicationContext applicationContext, Environment env) { - super(applicationContext); - this.env = env; - } - - @Override - public Object initialize(Object obj) { - // do not log during server tests to avoid issues - if (!env.acceptsProfiles(Profiles.of(SPRING_PROFILE_TEST))) { - String type = obj != null ? obj.getClass().getName() : "null"; - log.debug("Initialize obj {} of type {}", obj, type); - } - return super.initialize(obj); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java index 829f994a1a50..ead2d6fc7c6c 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/CacheConfiguration.java @@ -18,6 +18,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.info.BuildProperties; @@ -47,8 +48,10 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.spi.properties.ClusterProperty; import com.hazelcast.spring.cache.HazelcastCacheManager; +import com.hazelcast.spring.context.SpringManagedContext; import de.tum.in.www1.artemis.service.HazelcastPathSerializer; +import de.tum.in.www1.artemis.service.connectors.localci.LocalCIPriorityQueueComparator; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import tech.jhipster.config.JHipsterProperties; import tech.jhipster.config.cache.PrefixedKeyGenerator; @@ -111,7 +114,7 @@ public void destroy() { } @Bean - public CacheManager cacheManager(HazelcastInstance hazelcastInstance) { + public CacheManager cacheManager(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { log.debug("Starting HazelcastCacheManager"); return new HazelcastCacheManager(hazelcastInstance); } @@ -163,7 +166,7 @@ public void connectToAllMembers() { * @param jHipsterProperties the jhipster properties * @return the created HazelcastInstance */ - @Bean + @Bean(name = "hazelcastInstance") public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties) { log.debug("Configuring Hazelcast"); HazelcastInstance hazelCastInstance = Hazelcast.getHazelcastInstanceByName(instanceName); @@ -182,7 +185,7 @@ public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true); // Allows using @SpringAware and therefore Spring Services in distributed tasks - config.setManagedContext(new ArtemisSpringManagedContext(applicationContext, env)); + config.setManagedContext(new SpringManagedContext(applicationContext)); config.setClassLoader(applicationContext.getClassLoader()); config.getSerializationConfig().addSerializerConfig(createPathSerializerConfig()); @@ -273,7 +276,7 @@ private void configureQueueCluster(Config config, JHipsterProperties jHipsterPro log.debug("Configure Build Job Queue synchronization in Hazelcast for Local CI"); QueueConfig queueConfig = new QueueConfig("buildJobQueue"); queueConfig.setBackupCount(jHipsterProperties.getCache().getHazelcast().getBackupCount()); - queueConfig.setPriorityComparatorClassName("de.tum.in.www1.artemis.service.connectors.localci.LocalCIPriorityQueueComparator"); + queueConfig.setPriorityComparatorClassName(LocalCIPriorityQueueComparator.class.getName()); config.addQueueConfig(queueConfig); } diff --git a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java index d22025c09a0e..8a5c3525b7f9 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java +++ b/src/main/java/de/tum/in/www1/artemis/config/MetricsBean.java @@ -13,16 +13,17 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthContributor; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.NamedContributor; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthContributor; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.user.SimpUserRegistry; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Scheduled; @@ -78,7 +79,7 @@ public class MetricsBean { private final MeterRegistry meterRegistry; - private final TaskScheduler taskScheduler; + private final TaskScheduler scheduler; private final WebSocketMessageBrokerStats webSocketStats; @@ -100,6 +101,10 @@ public class MetricsBean { private final ProfileService profileService; + private final List<HealthContributor> healthContributors; + + private final Optional<HikariDataSource> hikariDataSource; + private final Optional<SharedQueueManagementService> localCIBuildJobQueueService; /** @@ -151,15 +156,17 @@ public class MetricsBean { private boolean scheduledMetricsEnabled = false; - public MetricsBean(MeterRegistry meterRegistry, TaskScheduler taskScheduler, WebSocketMessageBrokerStats webSocketStats, SimpUserRegistry userRegistry, + public MetricsBean(MeterRegistry meterRegistry, @Qualifier("taskScheduler") TaskScheduler scheduler, WebSocketMessageBrokerStats webSocketStats, SimpUserRegistry userRegistry, WebSocketHandler websocketHandler, List<HealthContributor> healthContributors, Optional<HikariDataSource> hikariDataSource, ExerciseRepository exerciseRepository, StudentExamRepository studentExamRepository, ExamRepository examRepository, CourseRepository courseRepository, UserRepository userRepository, StatisticsRepository statisticsRepository, ProfileService profileService, Optional<SharedQueueManagementService> localCIBuildJobQueueService) { this.meterRegistry = meterRegistry; - this.taskScheduler = taskScheduler; + this.scheduler = scheduler; this.webSocketStats = webSocketStats; this.userRegistry = userRegistry; this.webSocketHandler = websocketHandler; + this.healthContributors = healthContributors; + this.hikariDataSource = hikariDataSource; this.exerciseRepository = exerciseRepository; this.examRepository = examRepository; this.studentExamRepository = studentExamRepository; @@ -168,7 +175,37 @@ public MetricsBean(MeterRegistry meterRegistry, TaskScheduler taskScheduler, Web this.statisticsRepository = statisticsRepository; this.profileService = profileService; this.localCIBuildJobQueueService = localCIBuildJobQueueService; + } + /** + * Event listener method that is invoked when the application is ready. It registers various health and metric + * contributors, and conditionally enables metrics based on active profiles. + * + * <p> + * Specifically, this method performs the following actions: + * <ul> + * <li>Registers health contributors.</li> + * <li>Registers websocket metrics.</li> + * <li>If the scheduling profile is active: + * <ul> + * <li>Enables scheduled metrics.</li> + * <li>Calculates cached active user names.</li> + * <li>Registers exercise and exam metrics.</li> + * <li>Registers public Artemis metrics.</li> + * </ul> + * </li> + * <li>If the local CI profile is active, registers local CI metrics.</li> + * <li>Registers datasource metrics if the Hikari datasource is present.</li> + * <li>If the websocket logging profile is active: + * <ul> + * <li>Sets the logging period for websocket statistics.</li> + * <li>Schedules periodic logging of connected users and active websocket subscriptions.</li> + * </ul> + * </li> + * </ul> + */ + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { registerHealthContributors(healthContributors); registerWebsocketMetrics(); @@ -189,19 +226,14 @@ public MetricsBean(MeterRegistry meterRegistry, TaskScheduler taskScheduler, Web // the data source is optional as it is not used during testing hikariDataSource.ifPresent(this::registerDatasourceMetrics); - } - /** - * initialize the websocket logging - */ - @PostConstruct - public void init() { + // initialize the websocket logging // using Autowired leads to a weird bug, because the order of the method execution is changed. This somehow prevents messages send to single clients // later one, e.g. in the code editor. Therefore, we call this method here directly to get a reference and adapt the logging period! // Note: this mechanism prevents that this is logged during testing if (profileService.isProfileActive("websocketLog")) { webSocketStats.setLoggingPeriod(LOGGING_DELAY_SECONDS * 1000L); - taskScheduler.scheduleAtFixedRate(() -> { + scheduler.scheduleAtFixedRate(() -> { final var connectedUsers = userRegistry.getUsers(); final var subscriptionCount = connectedUsers.stream().flatMap(simpUser -> simpUser.getSessions().stream()).map(simpSession -> simpSession.getSubscriptions().size()) .reduce(0, Integer::sum); @@ -365,7 +397,7 @@ public void calculateCachedActiveUserNames() { * The update (and recalculation) is performed every 5 minutes. * Only executed if the "scheduling"-profile is present. */ - @Scheduled(fixedRate = 5 * 60 * 1000, initialDelay = 0) // Every 5 minutes + @Scheduled(fixedRate = 5 * 60 * 1000, initialDelay = 30 * 1000) // Every 5 minutes with an initial delay of 30 seconds public void recalculateMetrics() { if (!scheduledMetricsEnabled) { return; diff --git a/src/main/java/de/tum/in/www1/artemis/config/RestTemplateConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/RestTemplateConfiguration.java index 6fa1ca5d8013..3ae57f9aefc8 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/RestTemplateConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/RestTemplateConfiguration.java @@ -18,7 +18,7 @@ import org.springframework.web.client.RestTemplate; import de.tum.in.www1.artemis.config.auth.AthenaAuthorizationInterceptor; -import de.tum.in.www1.artemis.config.auth.IrisAuthorizationInterceptor; +import de.tum.in.www1.artemis.config.auth.PyrisAuthorizationInterceptor; import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabAuthorizationInterceptor; import de.tum.in.www1.artemis.service.connectors.jenkins.JenkinsAuthorizationInterceptor; @@ -77,8 +77,8 @@ public RestTemplate aeolusRestTemplate() { @Bean @Profile("iris") - public RestTemplate irisRestTemplate(IrisAuthorizationInterceptor irisAuthorizationInterceptor) { - return initializeRestTemplateWithInterceptors(irisAuthorizationInterceptor, createRestTemplate()); + public RestTemplate pyrisRestTemplate(PyrisAuthorizationInterceptor pyrisAuthorizationInterceptor) { + return initializeRestTemplateWithInterceptors(pyrisAuthorizationInterceptor, createRestTemplate()); } // Note: for certain requests, e.g. health(), we would like to have shorter timeouts, therefore we need additional rest templates, because @@ -121,8 +121,8 @@ public RestTemplate veryShortTimeoutAthenaRestTemplate(AthenaAuthorizationInterc @Bean @Profile("iris") - public RestTemplate shortTimeoutIrisRestTemplate(IrisAuthorizationInterceptor irisAuthorizationInterceptor) { - return initializeRestTemplateWithInterceptors(irisAuthorizationInterceptor, createShortTimeoutRestTemplate()); + public RestTemplate shortTimeoutPyrisRestTemplate(PyrisAuthorizationInterceptor pyrisAuthorizationInterceptor) { + return initializeRestTemplateWithInterceptors(pyrisAuthorizationInterceptor, createShortTimeoutRestTemplate()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/config/StartupDelayConfig.java b/src/main/java/de/tum/in/www1/artemis/config/StartupDelayConfig.java new file mode 100644 index 000000000000..9a59b94ff483 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/config/StartupDelayConfig.java @@ -0,0 +1,18 @@ +package de.tum.in.www1.artemis.config; + +public class StartupDelayConfig { + + public static final long PROGRAMMING_EXERCISE_SCHEDULE_DELAY_SEC = 20; + + public static final long MODELING_EXERCISE_SCHEDULE_DELAY_SEC = 25; + + public static final long QUIZ_EXERCISE_SCHEDULE_DELAY_SEC = 25; + + public static final long NOTIFICATION_SCHEDULE_DELAY_SEC = 30; + + public static final long ATHENA_SCHEDULE_DELAY_SEC = 30; + + public static final long EMAIL_SUMMARY_SCHEDULE_DELAY_SEC = 35; + + public static final long PARTICIPATION_SCORES_SCHEDULE_DELAY_SEC = 35; +} diff --git a/src/main/java/de/tum/in/www1/artemis/config/auth/IrisAuthorizationInterceptor.java b/src/main/java/de/tum/in/www1/artemis/config/auth/PyrisAuthorizationInterceptor.java similarity index 91% rename from src/main/java/de/tum/in/www1/artemis/config/auth/IrisAuthorizationInterceptor.java rename to src/main/java/de/tum/in/www1/artemis/config/auth/PyrisAuthorizationInterceptor.java index bb2f1df5e259..dbeef78de74c 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/auth/IrisAuthorizationInterceptor.java +++ b/src/main/java/de/tum/in/www1/artemis/config/auth/PyrisAuthorizationInterceptor.java @@ -15,7 +15,7 @@ @Component @Profile("iris") -public class IrisAuthorizationInterceptor implements ClientHttpRequestInterceptor { +public class PyrisAuthorizationInterceptor implements ClientHttpRequestInterceptor { @Value("${artemis.iris.secret-token}") private String secret; diff --git a/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java index 1b7fcdb9fe2e..c3f94acaff28 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/localvcci/LocalCIConfiguration.java @@ -64,6 +64,7 @@ public LocalCIConfiguration(ProgrammingLanguageConfiguration programmingLanguage * @return The HostConfig bean. */ @Bean + // TODO: reconsider if a bean is necessary here, this could also be created after application startup with @EventListener(ApplicationReadyEvent.class) to speed up the startup public HostConfig hostConfig() { long cpuCount = 0; long cpuPeriod = 100000L; @@ -159,6 +160,7 @@ public XMLInputFactory localCIXMLInputFactory() { * @return The DockerClient bean. */ @Bean + // TODO: reconsider if a bean is necessary here, this could also be created after application startup with @EventListener(ApplicationReadyEvent.class) to speed up the startup public DockerClient dockerClient() { log.debug("Create bean dockerClient"); DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(dockerConnectionUri).build(); diff --git a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java index 72ddb0c83f99..96a40a6ea352 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java +++ b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java @@ -32,7 +32,7 @@ public class CustomLti13Configurer extends Lti13Configurer { private static final String LOGIN_INITIATION_PATH = "/initiate-login"; /** Base path for LTI 1.3 API endpoints. */ - public static final String LTI13_BASE_PATH = "/api/public/lti13"; + public static final String LTI13_BASE_PATH = "api/public/lti13"; /** Full path for LTI 1.3 login. */ public static final String LTI13_LOGIN_PATH = LTI13_BASE_PATH + LOGIN_PATH; @@ -72,7 +72,7 @@ public void configure(HttpSecurity http) { // https://www.imsglobal.org/spec/security/v1p0/#step-3-authentication-response OAuth2LoginAuthenticationFilter defaultLoginFilter = configureLoginFilter(clientRegistrationRepository(http), oidcLaunchFlowAuthenticationProvider, authorizationRequestRepository); - http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, LTI13_BASE_PATH + LOGIN_PATH, lti13Service(http)), AbstractPreAuthenticatedProcessingFilter.class); + http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, "/" + LTI13_LOGIN_PATH, lti13Service(http)), AbstractPreAuthenticatedProcessingFilter.class); } protected Lti13Service lti13Service(HttpSecurity http) { diff --git a/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java index e4bbf50c3b79..f82720b640c0 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/config/websocket/WebsocketConfiguration.java @@ -18,6 +18,7 @@ import java.util.Optional; import java.util.regex.Pattern; +import jakarta.annotation.Nullable; import jakarta.servlet.http.Cookie; import jakarta.validation.constraints.NotNull; @@ -57,12 +58,10 @@ import com.google.common.collect.Iterators; import de.tum.in.www1.artemis.domain.Exercise; -import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; -import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.jwt.JWTFilter; import de.tum.in.www1.artemis.security.jwt.TokenProvider; @@ -91,8 +90,6 @@ public class WebsocketConfiguration extends DelegatingWebSocketMessageBrokerConf private final AuthorizationCheckService authorizationCheckService; - private final UserRepository userRepository; - private final ExerciseRepository exerciseRepository; private final ExamRepository examRepository; @@ -109,14 +106,13 @@ public class WebsocketConfiguration extends DelegatingWebSocketMessageBrokerConf public WebsocketConfiguration(MappingJackson2HttpMessageConverter springMvcJacksonConverter, TaskScheduler messageBrokerTaskScheduler, TokenProvider tokenProvider, StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authorizationCheckService, ExerciseRepository exerciseRepository, - UserRepository userRepository, ExamRepository examRepository) { + ExamRepository examRepository) { this.objectMapper = springMvcJacksonConverter.getObjectMapper(); this.messageBrokerTaskScheduler = messageBrokerTaskScheduler; this.tokenProvider = tokenProvider; this.studentParticipationRepository = studentParticipationRepository; this.authorizationCheckService = authorizationCheckService; this.exerciseRepository = exerciseRepository; - this.userRepository = userRepository; this.examRepository = examRepository; } @@ -280,7 +276,7 @@ public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel c * @param destination Destination topic to which the user wants to subscribe * @return flag whether subscription is allowed */ - private boolean allowSubscription(Principal principal, String destination) { + private boolean allowSubscription(@Nullable Principal principal, String destination) { /* * IMPORTANT: Avoid database calls in this method as much as possible (e.g. checking if the user * is an instructor in a course) @@ -288,6 +284,10 @@ private boolean allowSubscription(Principal principal, String destination) { * If you need to do a database call, make sure to first check if the destination is valid for your specific * use case. */ + if (principal == null) { + log.warn("Anonymous user tried to access the protected topic: {}", destination); + return false; + } final var login = principal.getName(); @@ -340,16 +340,6 @@ private boolean isParticipationOwnedByUser(Principal principal, Long participati return participation.isOwnedBy(principal.getName()); } - private boolean isUserInstructorOrHigherForExercise(Principal principal, Exercise exercise) { - User user = userRepository.getUserWithGroupsAndAuthorities(principal.getName()); - return authorizationCheckService.isAtLeastInstructorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user); - } - - private boolean isUserTAOrHigherForExercise(Principal principal, Exercise exercise) { - User user = userRepository.getUserWithGroupsAndAuthorities(principal.getName()); - return authorizationCheckService.isAtLeastTeachingAssistantForExercise(exercise, user); - } - /** * Returns the exam id if the given destination belongs to a topic for a whole exam. * Only instructors and admins should be allowed to subscribe to this topic. diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index f4605bd63a63..d96a914ad3ec 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -93,8 +93,9 @@ public abstract class Exercise extends BaseExercise implements LearningObject { @Column(name = "allow_complaints_for_automatic_assessments") private boolean allowComplaintsForAutomaticAssessments; + // TODO: rename in a follow up @Column(name = "allow_manual_feedback_requests") - private boolean allowManualFeedbackRequests; + private boolean allowFeedbackRequests; @Enumerated(EnumType.STRING) @Column(name = "included_in_overall_score") @@ -246,12 +247,12 @@ public Optional<ZonedDateTime> getCompletionDate(User user) { return this.getStudentParticipations().stream().filter((participation) -> participation.getStudents().contains(user)).map(Participation::getInitializationDate).findFirst(); } - public boolean getAllowManualFeedbackRequests() { - return allowManualFeedbackRequests; + public boolean getAllowFeedbackRequests() { + return allowFeedbackRequests; } - public void setAllowManualFeedbackRequests(boolean allowManualFeedbackRequests) { - this.allowManualFeedbackRequests = allowManualFeedbackRequests; + public void setAllowFeedbackRequests(boolean allowFeedbackRequests) { + this.allowFeedbackRequests = allowFeedbackRequests; } public boolean getAllowComplaintsForAutomaticAssessments() { diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index e468a9a7b704..77c81184c29a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -723,7 +723,7 @@ public boolean areManualResultsAllowed() { if (getAssessmentType() == AssessmentType.SEMI_AUTOMATIC || getAllowComplaintsForAutomaticAssessments()) { // The relevantDueDate check below keeps us from assessing feedback requests, // as their relevantDueDate is before the due date - if (getAllowManualFeedbackRequests()) { + if (getAllowFeedbackRequests()) { return true; } @@ -751,7 +751,8 @@ private boolean checkForRatedAndAssessedResult(Result result) { * @return true if the result is manual and the assessment is over, or it is an automatic result, false otherwise */ private boolean checkForAssessedResult(Result result) { - return result.getCompletionDate() != null && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic()); + return result.getCompletionDate() != null + && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaAutomatic()); } @Override @@ -836,10 +837,10 @@ public void validateStaticCodeAnalysisSettings(ProgrammingLanguageFeature progra } /** - * Validates settings for exercises, where allowManualFeedbackRequests is set + * Validates settings for exercises, where allowFeedbackRequests is set */ - public void validateManualFeedbackSettings() { - if (!this.getAllowManualFeedbackRequests()) { + public void validateSettingsForFeedbackRequest() { + if (!this.getAllowFeedbackRequests()) { return; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Result.java b/src/main/java/de/tum/in/www1/artemis/domain/Result.java index b6e9d040023f..5fd8dc53bb2d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Result.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Result.java @@ -619,6 +619,16 @@ public boolean isAutomatic() { return AssessmentType.AUTOMATIC == assessmentType; } + /** + * Checks whether the result is an automatic Athena result: AUTOMATIC_ATHENA + * + * @return true if the result is an automatic AI Athena result + */ + @JsonIgnore + public boolean isAthenaAutomatic() { + return AssessmentType.AUTOMATIC_ATHENA == assessmentType; + } + @Override public String toString() { return "Result{" + "id" + getId() + ", completionDate=" + completionDate + ", successful=" + successful + ", score=" + score + ", rated=" + rated + ", assessmentType=" diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java index 174d7440eebc..30f4275cc14f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java @@ -158,7 +158,7 @@ public Result getResultForCorrectionRound(int correctionRound) { */ @NotNull private List<Result> filterNonAutomaticResults() { - return results.stream().filter(result -> result == null || !result.isAutomatic()).toList(); + return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())).toList(); } /** @@ -184,7 +184,8 @@ public boolean hasResultForCorrectionRound(int correctionRound) { */ @JsonIgnore public void removeAutomaticResults() { - this.results = this.results.stream().filter(result -> result == null || !result.isAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())) + .collect(Collectors.toCollection(ArrayList::new)); } /** @@ -209,7 +210,7 @@ public List<Result> getResults() { @JsonIgnore public List<Result> getManualResults() { - return results.stream().filter(result -> result != null && !result.isAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/AssessmentType.java b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/AssessmentType.java index 49f400aa4849..0ec51de7bb09 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/enumeration/AssessmentType.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/enumeration/AssessmentType.java @@ -4,5 +4,5 @@ * The AssessmentType enumeration. */ public enum AssessmentType { - AUTOMATIC, SEMI_AUTOMATIC, MANUAL + AUTOMATIC, SEMI_AUTOMATIC, MANUAL, AUTOMATIC_ATHENA } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/session/IrisSession.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/session/IrisSession.java index 3debbc79d357..f735f49c14f7 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/session/IrisSession.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/session/IrisSession.java @@ -53,10 +53,10 @@ public abstract class IrisSession extends DomainObject { @Column(name = "creation_date") private ZonedDateTime creationDate = ZonedDateTime.now(); + // TODO: This is only used in the tests -> Remove public IrisMessage newMessage() { var message = new IrisMessage(); message.setSession(this); - this.messages.add(message); return message; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java index 93e835114ba5..c94c8810bca0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java @@ -2,16 +2,17 @@ import java.util.Comparator; import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; @Converter -public class IrisModelListConverter implements AttributeConverter<Set<String>, String> { +public class IrisModelListConverter implements AttributeConverter<SortedSet<String>, String> { @Override - public String convertToDatabaseColumn(Set<String> type) { + public String convertToDatabaseColumn(SortedSet<String> type) { if (type == null || type.isEmpty()) { return null; } @@ -20,7 +21,7 @@ public String convertToDatabaseColumn(Set<String> type) { } @Override - public Set<String> convertToEntityAttribute(String value) { + public SortedSet<String> convertToEntityAttribute(String value) { var treeSet = new TreeSet<String>(Comparator.naturalOrder()); if (value != null) { treeSet.addAll(Set.of(value.split(","))); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java index 26ab83b8c064..3387c97dd099 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.domain.iris.settings; -import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; import jakarta.annotation.Nullable; @@ -52,7 +52,7 @@ public abstract class IrisSubSettings extends DomainObject { @Column(name = "allowed_models") @Convert(converter = IrisModelListConverter.class) - private Set<String> allowedModels = new TreeSet<>(); + private SortedSet<String> allowedModels = new TreeSet<>(); @Nullable @Column(name = "preferred_model") @@ -66,11 +66,11 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public Set<String> getAllowedModels() { + public SortedSet<String> getAllowedModels() { return allowedModels; } - public void setAllowedModels(Set<String> allowedModels) { + public void setAllowedModels(SortedSet<String> allowedModels) { this.allowedModels = allowedModels; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Claims.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Claims.java index be99b242aed2..ca5d8494cc74 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Claims.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Claims.java @@ -13,4 +13,10 @@ public class Claims extends uk.ac.ox.ctl.lti13.lti.Claims { * Used to carry messages specific to LTI Deep Linking requests and responses. */ public static final String MSG = "https://purl.imsglobal.org/spec/lti-dl/claim/msg"; + + /** + * Constant for LTI Deep Linking return url claim. + * Used to carry url specific to LTI Deep Linking requests and responses. + */ + public static final String DEEPLINK_RETURN_URL_CLAIM = "deep_link_return_url"; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java index 4196ce423889..62282902ae69 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13AgsClaim.java @@ -6,20 +6,13 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** - * A wrapper class for an LTI 1.3 Assignment and Grading Services Claim. We support the Score Publishing Service in order to transmit scores. + * A wrapper record for an LTI 1.3 Assignment and Grading Services Claim. We support the Score Publishing Service in order to transmit scores. */ -public class Lti13AgsClaim { - - private List<String> scope; - - private String lineItem; +public record Lti13AgsClaim(List<String> scope, String lineItem) { /** * Returns an Ags-Claim representation if the provided idToken contains any. @@ -34,53 +27,29 @@ public static Optional<Lti13AgsClaim> from(OidcIdToken idToken) { } try { - JsonObject agsClaimJson = new Gson().toJsonTree(idToken.getClaim(Claims.AGS_CLAIM)).getAsJsonObject(); - Lti13AgsClaim agsClaim = new Lti13AgsClaim(); - JsonArray scopes = agsClaimJson.get("scope").getAsJsonArray(); - - if (scopes == null) { - return Optional.empty(); - } + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode agsClaimJson = objectMapper.convertValue(idToken.getClaim(Claims.AGS_CLAIM), JsonNode.class); - if (scopes.contains(new JsonPrimitive(Scopes.AGS_SCORE))) { - agsClaim.setScope(Collections.singletonList(Scopes.AGS_SCORE)); + JsonNode scopes = agsClaimJson.get("scope"); + List<String> scopeList = null; + if (scopes != null && scopes.isArray() && scopes.has(Scopes.AGS_SCORE)) { + scopeList = Collections.singletonList(Scopes.AGS_SCORE); } // For moodle lineItem is stored in lineitem claim, for edX it is in lineitems - JsonElement lineItem; + JsonNode lineItemNode; if (agsClaimJson.get("lineitem") == null) { - lineItem = agsClaimJson.get("lineitems"); + lineItemNode = agsClaimJson.get("lineitems"); } else { - lineItem = agsClaimJson.get("lineitem"); + lineItemNode = agsClaimJson.get("lineitem"); } - if (lineItem != null) { - agsClaim.setLineItem(lineItem.getAsString()); - } - else { - agsClaim.setLineItem(null); - } - return Optional.of(agsClaim); + String lineItem = lineItemNode != null ? lineItemNode.asText() : null; + return Optional.of(new Lti13AgsClaim(scopeList, lineItem)); } catch (IllegalStateException | ClassCastException ex) { throw new IllegalStateException("Failed to parse LTI 1.3 ags claim.", ex); } } - - public List<String> getScope() { - return scope; - } - - private void setScope(List<String> scope) { - this.scope = scope; - } - - public String getLineItem() { - return lineItem; - } - - private void setLineItem(String lineItem) { - this.lineItem = lineItem; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java index ff9a101f27a2..9e89152aec91 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13DeepLinkingResponse.java @@ -1,67 +1,46 @@ package de.tum.in.www1.artemis.domain.lti; -import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.gson.Gson; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** * Represents the LTI 1.3 Deep Linking Response. * It encapsulates the necessary information to construct a valid deep linking response according to the LTI 1.3 specification. * For more details, refer to <a href="https://www.imsglobal.org/spec/lti-dl/v2p0#deep-linking-response-message">LTI 1.3 Deep Linking Response Specification</a> + * + * @param aud The 'aud' (Audience) field is used to specify the intended recipient of the response + * In the context of LTI Deep Linking, this field is set to the 'iss' (Issuer) value from the original request. + * This indicates that the response is specifically intended for the issuer of the original request. + * @param iss * The 'iss' (Issuer) field identifies the principal that issued the response. + * For LTI Deep Linking, this field is set to the 'aud' (Audience) value from the original request. + * This reversal signifies that the tool (originally the audience) is now the issuer of the response. + * @param exp Expiration time of the response. + * @param iat Issued at time of the response. + * @param nonce A string value used to associate a Client session with an ID Token. + * @param message A message included in the deep linking response. + * @param deploymentId The deployment ID from the LTI request. + * @param messageType The type of LTI message, for deep linking this is "LtiDeepLinkingResponse". + * @param ltiVersion The LTI version, for deep linking responses this is typically "1.3.0". + * @param contentItems The actual content items being linked. + * @param deepLinkingSettings A JSON object containing deep linking settings. + * @param clientRegistrationId The client registration ID. + * @param returnUrl The URL to return to after deep linking is completed. */ -public class Lti13DeepLinkingResponse { +public record Lti13DeepLinkingResponse(@JsonProperty(IdTokenClaimNames.AUD) String aud, @JsonProperty(IdTokenClaimNames.ISS) String iss, + @JsonProperty(IdTokenClaimNames.EXP) String exp, @JsonProperty(IdTokenClaimNames.IAT) String iat, @JsonProperty(IdTokenClaimNames.NONCE) String nonce, + @JsonProperty(Claims.MSG) String message, @JsonProperty(Claims.LTI_DEPLOYMENT_ID) String deploymentId, @JsonProperty(Claims.MESSAGE_TYPE) String messageType, + @JsonProperty(Claims.LTI_VERSION) String ltiVersion, @JsonProperty(Claims.CONTENT_ITEMS) List<Map<String, Object>> contentItems, JsonNode deepLinkingSettings, + String clientRegistrationId, String returnUrl) { - /** - * The 'aud' (Audience) field is used to specify the intended recipient of the response - * In the context of LTI Deep Linking, this field is set to the 'iss' (Issuer) value from the original request. - * This indicates that the response is specifically intended for the issuer of the original request. - */ - @JsonProperty("aud") - private String aud; - - /** - * The 'iss' (Issuer) field identifies the principal that issued the response. - * For LTI Deep Linking, this field is set to the 'aud' (Audience) value from the original request. - * This reversal signifies that the tool (originally the audience) is now the issuer of the response. - */ - @JsonProperty("iss") - private String iss; - - @JsonProperty("exp") - private String exp; - - @JsonProperty("iat") - private String iat; - - @JsonProperty("nonce") - private String nonce; - - @JsonProperty(Claims.MSG) - private String message; - - @JsonProperty(Claims.LTI_DEPLOYMENT_ID) - private String deploymentId; - - @JsonProperty(Claims.MESSAGE_TYPE) - private String messageType; - - @JsonProperty(Claims.LTI_VERSION) - private String ltiVersion; - - @JsonProperty(Claims.CONTENT_ITEMS) - private ArrayList<Map<String, Object>> contentItems; - - private JsonObject deepLinkingSettings; - - private String clientRegistrationId; - - private String returnUrl; + private static final ObjectMapper objectMapper = new ObjectMapper(); /** * Constructs an Lti13DeepLinkingResponse from an OIDC ID token and client registration ID. @@ -73,52 +52,18 @@ public class Lti13DeepLinkingResponse { * @param ltiIdToken the OIDC ID token * @param clientRegistrationId the client registration ID */ - public Lti13DeepLinkingResponse(OidcIdToken ltiIdToken, String clientRegistrationId) { + public static Lti13DeepLinkingResponse from(OidcIdToken ltiIdToken, String clientRegistrationId) { validateClaims(ltiIdToken); + JsonNode deepLinkingSettingsJson = objectMapper.convertValue(ltiIdToken.getClaim(Claims.DEEP_LINKING_SETTINGS), JsonNode.class); + String returnUrl = deepLinkingSettingsJson.get(Claims.DEEPLINK_RETURN_URL_CLAIM).asText(); - Map<String, Object> deepLinkingSettings = ltiIdToken.getClaim(Claims.DEEP_LINKING_SETTINGS); - // convert the map to json - this.deepLinkingSettings = new Gson().toJsonTree(deepLinkingSettings).getAsJsonObject(); - this.setReturnUrl(this.deepLinkingSettings.get("deep_link_return_url").getAsString()); - this.clientRegistrationId = clientRegistrationId; - - // the issuer claim in the deep linking request becomes the audience claim in the response - this.setAud(ltiIdToken.getClaim("iss").toString()); - // the audience claim in the request becomes the issuer claim in the response - this.setIss(ltiIdToken.getClaim("aud").toString().replace("[", "").replace("]", "")); - - this.setExp(ltiIdToken.getClaim("exp").toString()); - this.setIat(ltiIdToken.getClaim("iat").toString()); - this.setNonce(ltiIdToken.getClaim("nonce").toString()); - this.setMessage("Content successfully linked"); - this.setDeploymentId(ltiIdToken.getClaim(Claims.LTI_DEPLOYMENT_ID).toString()); - this.setMessageType("LtiDeepLinkingResponse"); - this.setLtiVersion("1.3.0"); - } - - /** - * Retrieves a map of claims to be included in the ID token. - * - * @return a map of claims - */ - public Map<String, Object> getClaims() { - Map<String, Object> claims = new HashMap<>(); - - claims.put("aud", aud); - claims.put("iss", iss); - claims.put("exp", exp); - claims.put("iat", iat); - claims.put("nonce", nonce); - claims.put(Claims.MSG, message); - claims.put(Claims.LTI_DEPLOYMENT_ID, deploymentId); - claims.put(Claims.MESSAGE_TYPE, messageType); - claims.put(Claims.LTI_VERSION, ltiVersion); - claims.put(Claims.CONTENT_ITEMS, contentItems); - - return claims; + return new Lti13DeepLinkingResponse(ltiIdToken.getIssuer().toString(), ltiIdToken.getAudience().getFirst(), String.valueOf(ltiIdToken.getExpiresAt()), + String.valueOf(ltiIdToken.getIssuedAt()), ltiIdToken.getClaimAsString(IdTokenClaimNames.NONCE), "Content successfully linked", + ltiIdToken.getClaimAsString(Claims.LTI_DEPLOYMENT_ID), "LtiDeepLinkingResponse", "1.3.0", null, // ContentItems needs to be set separately + deepLinkingSettingsJson, clientRegistrationId, returnUrl); } - private void validateClaims(OidcIdToken ltiIdToken) { + private static void validateClaims(OidcIdToken ltiIdToken) { if (ltiIdToken == null) { throw new IllegalArgumentException("The OIDC ID token must not be null."); } @@ -128,122 +73,50 @@ private void validateClaims(OidcIdToken ltiIdToken) { throw new IllegalArgumentException("Missing or invalid deep linking settings in ID token."); } - ensureClaimPresent(ltiIdToken, "iss"); - ensureClaimPresent(ltiIdToken, "aud"); - ensureClaimPresent(ltiIdToken, "exp"); - ensureClaimPresent(ltiIdToken, "iat"); - ensureClaimPresent(ltiIdToken, "nonce"); + ensureClaimPresent(ltiIdToken, IdTokenClaimNames.ISS); + ensureClaimPresent(ltiIdToken, IdTokenClaimNames.AUD); + ensureClaimPresent(ltiIdToken, IdTokenClaimNames.EXP); + ensureClaimPresent(ltiIdToken, IdTokenClaimNames.IAT); + ensureClaimPresent(ltiIdToken, IdTokenClaimNames.NONCE); ensureClaimPresent(ltiIdToken, Claims.LTI_DEPLOYMENT_ID); } - private void ensureClaimPresent(OidcIdToken ltiIdToken, String claimName) { + private static void ensureClaimPresent(OidcIdToken ltiIdToken, String claimName) { Object claimValue = ltiIdToken.getClaim(claimName); if (claimValue == null) { throw new IllegalArgumentException("Missing claim: " + claimName); } } - public void setAud(String aud) { - this.aud = aud; - } - - public String getIss() { - return iss; - } - - public void setIss(String iss) { - this.iss = iss; - } - - public String getExp() { - return exp; - } - - public void setExp(String exp) { - this.exp = exp; - } - - public String getIat() { - return iat; - } - - public void setIat(String iat) { - this.iat = iat; - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getDeploymentId() { - return deploymentId; - } - - public void setDeploymentId(String deploymentId) { - this.deploymentId = deploymentId; - } - - public String getMessageType() { - return messageType; - } - - public void setMessageType(String messageType) { - this.messageType = messageType; - } - - public String getLtiVersion() { - return ltiVersion; - } - - public void setLtiVersion(String ltiVersion) { - this.ltiVersion = ltiVersion; - } - - public ArrayList<Map<String, Object>> getContentItems() { - return contentItems; - } - - public void setContentItems(ArrayList<Map<String, Object>> contentItems) { - this.contentItems = contentItems; - } - - public JsonObject getDeepLinkingSettings() { - return deepLinkingSettings; - } - - public void setDeepLinkingSettings(JsonObject deepLinkingSettings) { - this.deepLinkingSettings = deepLinkingSettings; - } - - public String getClientRegistrationId() { - return clientRegistrationId; - } - - public void setClientRegistrationId(String clientRegistrationId) { - this.clientRegistrationId = clientRegistrationId; - } - - public String getAud() { - return aud; - } + /** + * Retrieves a map of claims to be included in the ID token. + * + * @return a map of claims + */ + public Map<String, Object> getClaims() { + Map<String, Object> claims = new HashMap<>(); + claims.put(IdTokenClaimNames.AUD, aud()); + claims.put(IdTokenClaimNames.ISS, iss()); + claims.put(IdTokenClaimNames.EXP, exp()); + claims.put(IdTokenClaimNames.IAT, iat()); + claims.put(IdTokenClaimNames.NONCE, nonce()); + claims.put(Claims.MSG, message()); + claims.put(Claims.LTI_DEPLOYMENT_ID, deploymentId()); + claims.put(Claims.MESSAGE_TYPE, messageType()); + claims.put(Claims.LTI_VERSION, ltiVersion()); + claims.put(Claims.CONTENT_ITEMS, contentItems()); - public String getReturnUrl() { - return returnUrl; + return claims; } - public void setReturnUrl(String returnUrl) { - this.returnUrl = returnUrl; + /** + * Returns a new Lti13DeepLinkingResponse instance with updated contentItems. + * + * @param contentItems The new contentItems value. + * @return A new Lti13DeepLinkingResponse instance. + */ + public Lti13DeepLinkingResponse setContentItems(List<Map<String, Object>> contentItems) { + return new Lti13DeepLinkingResponse(this.aud, this.iss, this.exp, this.iat, this.nonce, this.message, this.deploymentId, this.messageType, this.ltiVersion, contentItems, + this.deepLinkingSettings, this.clientRegistrationId, this.returnUrl); } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java index 12ab100e7e81..cc4ea50a6555 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13LaunchRequest.java @@ -1,43 +1,59 @@ package de.tum.in.www1.artemis.domain.lti; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.util.Assert; -import com.google.gson.Gson; - -public class Lti13LaunchRequest { - - private final String iss; - - private final String sub; - - private final String deploymentId; - - private final String resourceLinkId; - - private final String targetLinkUri; - - private final Lti13AgsClaim agsClaim; - - private final String clientRegistrationId; - - public Lti13LaunchRequest(OidcIdToken ltiIdToken, String clientRegistrationId) { - this.iss = ltiIdToken.getClaim("iss"); - this.sub = ltiIdToken.getClaim("sub"); - this.deploymentId = ltiIdToken.getClaim(Claims.LTI_DEPLOYMENT_ID); +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represents an LTI 1.3 Launch Request, encapsulating the necessary information + * from an OIDC ID token and additional parameters required for launching an LTI tool. + * + * @param iss The issuer identifier from the OIDC ID token, representing the platform. + * @param sub The subject identifier from the OIDC ID token, representing the user. + * @param deploymentId The deployment ID from the OIDC ID token, unique to the tool and platform pair. + * @param resourceLinkId The resource link ID, identifying the specific resource from the launch request. + * @param targetLinkUri The target link URI from the OIDC ID token, where the tool is expected to send the user. + * @param agsClaim An optional {@link Lti13AgsClaim} representing the Assignment and Grade Services claim, if present. + * @param clientRegistrationId The client registration ID, identifying the tool registration with the platform. + */ +public record Lti13LaunchRequest(String iss, String sub, String deploymentId, String resourceLinkId, String targetLinkUri, Lti13AgsClaim agsClaim, String clientRegistrationId) { + + /** + * Factory method to create an instance of Lti13LaunchRequest from an OIDC ID token and client registration ID. + * Validates required fields and extracts information from the ID token. + * + * @param ltiIdToken the OIDC ID token containing the claims. + * @param clientRegistrationId the client registration ID for the request. + * @return an instance of Lti13LaunchRequest. + * @throws IllegalArgumentException if required fields are missing in the ID token. + */ + public static Lti13LaunchRequest from(OidcIdToken ltiIdToken, String clientRegistrationId) { + String iss = ltiIdToken.getClaim(IdTokenClaimNames.ISS); + String sub = ltiIdToken.getClaim(IdTokenClaimNames.SUB); + String deploymentId = ltiIdToken.getClaim(Claims.LTI_DEPLOYMENT_ID); + String resourceLinkId = extractResourceLinkId(ltiIdToken); + String targetLinkUri = ltiIdToken.getClaim(Claims.TARGET_LINK_URI); + Lti13AgsClaim agsClaim = Lti13AgsClaim.from(ltiIdToken).orElse(null); + + validateRequiredFields(iss, sub, deploymentId, resourceLinkId, targetLinkUri, clientRegistrationId); + + return new Lti13LaunchRequest(iss, sub, deploymentId, resourceLinkId, targetLinkUri, agsClaim, clientRegistrationId); + } - var resourceLinkClaim = ltiIdToken.getClaim(Claims.RESOURCE_LINK); + private static String extractResourceLinkId(OidcIdToken ltiIdToken) { + Object resourceLinkClaim = ltiIdToken.getClaim(Claims.RESOURCE_LINK); if (resourceLinkClaim != null) { - this.resourceLinkId = new Gson().toJsonTree(resourceLinkClaim).getAsJsonObject().get("id").getAsString(); - } - else { - this.resourceLinkId = null; + JsonNode resourceLinkJson = new ObjectMapper().convertValue(resourceLinkClaim, JsonNode.class); + JsonNode idNode = resourceLinkJson.get("id"); + return idNode != null ? idNode.asText() : null; } - this.targetLinkUri = ltiIdToken.getClaim(Claims.TARGET_LINK_URI); - - this.agsClaim = Lti13AgsClaim.from(ltiIdToken).orElse(null); - this.clientRegistrationId = clientRegistrationId; + return null; + } + private static void validateRequiredFields(String iss, String sub, String deploymentId, String resourceLinkId, String targetLinkUri, String clientRegistrationId) { Assert.notNull(iss, "Iss must not be empty in LTI 1.3 launch request"); Assert.notNull(sub, "Sub must not be empty in LTI 1.3 launch request"); Assert.notNull(deploymentId, "DeploymentId must not be empty in LTI 1.3 launch request"); @@ -45,32 +61,4 @@ public Lti13LaunchRequest(OidcIdToken ltiIdToken, String clientRegistrationId) { Assert.notNull(targetLinkUri, "TargetLinkUri must not be empty in LTI 1.3 launch request"); Assert.notNull(clientRegistrationId, "ClientRegistrationId must not be empty in LTI 1.3 launch request"); } - - public String getIss() { - return iss; - } - - public String getSub() { - return sub; - } - - public String getDeploymentId() { - return deploymentId; - } - - public String getResourceLinkId() { - return resourceLinkId; - } - - public String getTargetLinkUri() { - return targetLinkUri; - } - - public Lti13AgsClaim getAgsClaim() { - return agsClaim; - } - - public String getClientRegistrationId() { - return clientRegistrationId; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13PlatformConfiguration.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13PlatformConfiguration.java index 4b505d6a6e01..4428c38b9c05 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13PlatformConfiguration.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/Lti13PlatformConfiguration.java @@ -2,59 +2,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public class Lti13PlatformConfiguration { - - private String issuer; - - @JsonProperty("token_endpoint") - private String tokenEndpoint; - - @JsonProperty("authorization_endpoint") - private String authorizationEndpoint; - - @JsonProperty("jwks_uri") - private String jwksUri; - - @JsonProperty("registration_endpoint") - private String registrationEndpoint; - - public String getIssuer() { - return issuer; - } - - public void setIssuer(String issuer) { - this.issuer = issuer; - } - - public String getTokenEndpoint() { - return tokenEndpoint; - } - - public void setTokenEndpoint(String tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; - } - - public String getAuthorizationEndpoint() { - return authorizationEndpoint; - } - - public void setAuthorizationEndpoint(String authorizationEndpoint) { - this.authorizationEndpoint = authorizationEndpoint; - } - - public String getJwksUri() { - return jwksUri; - } - - public void setJwksUri(String jwksUri) { - this.jwksUri = jwksUri; - } - - public String getRegistrationEndpoint() { - return registrationEndpoint; - } - - public void setRegistrationEndpoint(String registrationEndpoint) { - this.registrationEndpoint = registrationEndpoint; - } +/** + * Represents the LTI 1.3 Platform Configuration, encapsulating various endpoints and keys for LTI platform communication. + * + * @param issuer The unique identifier for the issuer of the configuration. + * @param tokenEndpoint The endpoint URL for obtaining tokens. + * @param authorizationEndpoint The endpoint URL for authorization. + * @param jwksUri The URI for the JSON Web Key Set (JWKS). + * @param registrationEndpoint The endpoint URL for registration. + */ +public record Lti13PlatformConfiguration(@JsonProperty("issuer") String issuer, @JsonProperty("token_endpoint") String tokenEndpoint, + @JsonProperty("authorization_endpoint") String authorizationEndpoint, @JsonProperty("jwks_uri") String jwksUri, + @JsonProperty("registration_endpoint") String registrationEndpoint) { } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponse.java similarity index 72% rename from src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponseDTO.java rename to src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponse.java index 899db58f27d8..a89fa468c56d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponseDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiAuthenticationResponse.java @@ -7,5 +7,5 @@ * @param ltiIdToken LTI service provided ID token. * @param clientRegistrationId Client's registration ID with LTI service. */ -public record LtiAuthenticationResponseDTO(String targetLinkUri, String ltiIdToken, String clientRegistrationId) { +public record LtiAuthenticationResponse(String targetLinkUri, String ltiIdToken, String clientRegistrationId) { } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiResourceLaunch.java b/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiResourceLaunch.java index bbf96a254db1..72af5948bee5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiResourceLaunch.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/lti/LtiResourceLaunch.java @@ -50,10 +50,10 @@ public class LtiResourceLaunch extends DomainObject { */ public static LtiResourceLaunch from(Lti13LaunchRequest launchRequest) { LtiResourceLaunch launch = new LtiResourceLaunch(); - launch.iss = launchRequest.getIss(); - launch.sub = launchRequest.getSub(); - launch.deploymentId = launchRequest.getDeploymentId(); - launch.resourceLinkId = launchRequest.getResourceLinkId(); + launch.iss = launchRequest.iss(); + launch.sub = launchRequest.sub(); + launch.deploymentId = launchRequest.deploymentId(); + launch.resourceLinkId = launchRequest.resourceLinkId(); return launch; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java index 007cd2393590..f015bf1e9924 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/PlagiarismDetectionConfig.java @@ -22,6 +22,18 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class PlagiarismDetectionConfig extends DomainObject { + public PlagiarismDetectionConfig() { + } + + public PlagiarismDetectionConfig(PlagiarismDetectionConfig inputConfig) { + this.continuousPlagiarismControlEnabled = inputConfig.continuousPlagiarismControlEnabled; + this.continuousPlagiarismControlPostDueDateChecksEnabled = inputConfig.continuousPlagiarismControlPostDueDateChecksEnabled; + this.continuousPlagiarismControlPlagiarismCaseStudentResponsePeriod = inputConfig.continuousPlagiarismControlPlagiarismCaseStudentResponsePeriod; + this.similarityThreshold = inputConfig.similarityThreshold; + this.minimumScore = inputConfig.minimumScore; + this.minimumSize = inputConfig.minimumSize; + } + @Column(name = "continuous_plagiarism_control_enabled") private boolean continuousPlagiarismControlEnabled = false; diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 0648166ee4db..a930abcb8b37 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -139,8 +139,14 @@ SELECT COUNT(c) > 0 @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.attachments" }) Optional<Course> findWithEagerLecturesById(long courseId); - @EntityGraph(type = LOAD, attributePaths = { "exercises", "lectures", "lectures.attachments" }) - Optional<Course> findWithEagerExercisesAndLecturesById(long courseId); + /** + * Returns an optional course by id with eagerly loaded exercises, plagiarism detection configuration, team assignment configuration, lectures and attachments. + * + * @param courseId The id of the course to find + * @return the populated course or an empty optional if no course was found + */ + @EntityGraph(type = LOAD, attributePaths = { "exercises", "exercises.plagiarismDetectionConfig", "exercises.teamAssignmentConfig", "lectures", "lectures.attachments" }) + Optional<Course> findWithEagerExercisesAndExerciseDetailsAndLecturesById(long courseId); @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.lectureUnits", "lectures.attachments" }) Optional<Course> findWithEagerLecturesAndLectureUnitsById(long courseId); @@ -438,9 +444,16 @@ default void removeOrganizationFromCourse(long courseId, Organization organizati } } + /** + * Returns a course by id with eagerly loaded exercises, plagiarism detection configuration, team assignment configuration, lectures and attachments. + * + * @param courseId The id of the course to find + * @return the populated course + * @throws EntityNotFoundException if no course was found + */ @NotNull - default Course findByIdWithExercisesAndLecturesElseThrow(long courseId) { - return findWithEagerExercisesAndLecturesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); + default Course findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(long courseId) { + return findWithEagerExercisesAndExerciseDetailsAndLecturesById(courseId).orElseThrow(() -> new EntityNotFoundException("Course", courseId)); } @NotNull diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java index 32b98b3b92d0..7c05bc7c8f4e 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java @@ -208,6 +208,10 @@ SELECT COUNT(DISTINCT examUsers.user.id) @EntityGraph(type = LOAD, attributePaths = { "exerciseGroups", "exerciseGroups.exercises" }) Optional<Exam> findWithExerciseGroupsAndExercisesById(long examId); + @EntityGraph(type = LOAD, attributePaths = { "exerciseGroups", "exerciseGroups.exercises", "exerciseGroups.exercises.plagiarismDetectionConfig", + "exerciseGroups.exercises.teamAssignmentConfig" }) + Optional<Exam> findWithExerciseGroupsAndExercisesAndExerciseDetailsById(long examId); + @EntityGraph(type = LOAD, attributePaths = { "exerciseGroups", "exerciseGroups.exercises", "exerciseGroups.exercises.studentParticipations", "exerciseGroups.exercises.studentParticipations.submissions" }) Optional<Exam> findWithExerciseGroupsExercisesParticipationsAndSubmissionsById(long examId); @@ -413,13 +417,13 @@ default Exam findByIdWithExerciseGroupsElseThrow(long examId) { /** * Returns a set containing all exercises that are defined in the - * specified exam. + * specified exam including their details. * * @param examId The id of the exam * @return A set containing the exercises */ - default Set<Exercise> findAllExercisesByExamId(long examId) { - var exam = findWithExerciseGroupsAndExercisesById(examId); + default Set<Exercise> findAllExercisesWithDetailsByExamId(long examId) { + var exam = findWithExerciseGroupsAndExercisesAndExerciseDetailsById(examId); return exam.map(value -> value.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).collect(Collectors.toSet())).orElseGet(Set::of); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java index 3d1b12c1b48b..8a015c1a5d58 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingSubmissionRepository.java @@ -89,6 +89,9 @@ default ProgrammingSubmission findFirstByParticipationIdAndCommitHashOrderByIdDe @EntityGraph(type = LOAD, attributePaths = "results.feedbacks") Optional<ProgrammingSubmission> findWithEagerResultsAndFeedbacksById(long submissionId); + @EntityGraph(type = LOAD, attributePaths = { "results", "results.feedbacks", "results.feedbacks.testCase", "results.feedbacks.longFeedbackText", "buildLogEntries" }) + Optional<ProgrammingSubmission> findWithEagerResultsAndFeedbacksAndBuildLogsById(long submissionId); + @EntityGraph(type = LOAD, attributePaths = { "results", "results.feedbacks", "results.feedbacks.testCase", "results.assessor" }) Optional<ProgrammingSubmission> findWithEagerResultsFeedbacksTestCasesAssessorById(long submissionId); diff --git a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java index b7d89ba7c97c..26dedc7a0d51 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java +++ b/src/main/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetriever.java @@ -24,7 +24,8 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; @@ -87,9 +88,9 @@ public String getToken(ClientRegistration clientRegistration, String... scopes) if (exchange.getBody() == null) { return null; } - return JsonParser.parseString(exchange.getBody()).getAsJsonObject().get("access_token").getAsString(); + return new ObjectMapper().readTree(exchange.getBody()).get("access_token").asText(); } - catch (HttpClientErrorException e) { + catch (HttpClientErrorException | JsonProcessingException e) { log.error("Could not retrieve access token for client {}: {}", clientRegistration.getClientId(), e.getMessage()); return null; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java index 52b45e9e1a03..635e8938c36f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java @@ -30,6 +30,7 @@ import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java index 8e366fb33367..e6d1059cd455 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java @@ -13,6 +13,7 @@ import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.domain.Submission; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.repository.ExampleSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; @@ -37,7 +38,7 @@ protected ExerciseImportService(ExampleSubmissionRepository exampleSubmissionRep this.feedbackService = feedbackService; } - void copyExerciseBasis(final Exercise newExercise, final Exercise importedExercise, final Map<Long, GradingInstruction> gradingInstructionCopyTracker) { + protected void copyExerciseBasis(final Exercise newExercise, final Exercise importedExercise, final Map<Long, GradingInstruction> gradingInstructionCopyTracker) { if (importedExercise.isCourseExercise()) { newExercise.setCourse(importedExercise.getCourseViaExerciseGroupOrCourseMember()); newExercise.setPresentationScoreEnabled(importedExercise.getPresentationScoreEnabled()); @@ -61,6 +62,11 @@ void copyExerciseBasis(final Exercise newExercise, final Exercise importedExerci newExercise.setDifficulty(importedExercise.getDifficulty()); newExercise.setGradingInstructions(importedExercise.getGradingInstructions()); newExercise.setGradingCriteria(importedExercise.copyGradingCriteria(gradingInstructionCopyTracker)); + + if (importedExercise.getPlagiarismDetectionConfig() != null) { + newExercise.setPlagiarismDetectionConfig(new PlagiarismDetectionConfig(importedExercise.getPlagiarismDetectionConfig())); + } + if (newExercise.getExerciseGroup() != null) { newExercise.setMode(ExerciseMode.INDIVIDUAL); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java index 5c411e46a070..87aabf1c1eaa 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseService.java @@ -58,6 +58,7 @@ import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.web.rest.dto.CourseManagementOverviewExerciseStatisticsDTO; import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; diff --git a/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java b/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java index a475ce401ebb..1f87b3201b1e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/PlantUmlService.java @@ -13,8 +13,10 @@ import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -36,9 +38,20 @@ public class PlantUmlService { private final ResourceLoaderService resourceLoaderService; - public PlantUmlService(ResourceLoaderService resourceLoaderService) throws IOException { + public PlantUmlService(ResourceLoaderService resourceLoaderService) { this.resourceLoaderService = resourceLoaderService; + } + /** + * Initializes themes and sets system properties for PlantUML security when the application is ready. + * + * <p> + * Deletes temporary theme files to ensure updates, ensures themes are available, and configures PlantUML security settings. + * + * @throws IOException if an I/O error occurs during file deletion + */ + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() throws IOException { // Delete on first launch to ensure updates Files.deleteIfExists(PATH_TMP_THEME.resolve(DARK_THEME_FILE_NAME)); Files.deleteIfExists(PATH_TMP_THEME.resolve(LIGHT_THEME_FILE_NAME)); diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java index 4272ead1b1b4..bddcc6a2866d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java @@ -685,7 +685,7 @@ public void checkIfExerciseDueDateIsReached(Exercise exercise) { } else { // special check for programming exercises as they use buildAndTestStudentSubmissionAfterDueDate instead of dueDate - if (exercise instanceof ProgrammingExercise programmingExercise && !exercise.getAllowManualFeedbackRequests()) { + if (exercise instanceof ProgrammingExercise programmingExercise && !exercise.getAllowFeedbackRequests()) { if (programmingExercise.getBuildAndTestStudentSubmissionsAfterDueDate() != null && programmingExercise.getBuildAndTestStudentSubmissionsAfterDueDate().isAfter(ZonedDateTime.now())) { log.debug("The due date to build and test of exercise '{}' has not been reached yet.", exercise.getTitle()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/TitleCacheEvictionService.java b/src/main/java/de/tum/in/www1/artemis/service/TitleCacheEvictionService.java index b7fdb0e5c0ec..fc0a941737a7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/TitleCacheEvictionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/TitleCacheEvictionService.java @@ -16,8 +16,10 @@ import org.hibernate.persister.entity.EntityPersister; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Course; @@ -41,13 +43,31 @@ public class TitleCacheEvictionService implements PostUpdateEventListener, PostD private final CacheManager cacheManager; + private final EntityManagerFactory entityManagerFactory; + public TitleCacheEvictionService(EntityManagerFactory entityManagerFactory, CacheManager cacheManager) { this.cacheManager = cacheManager; + this.entityManagerFactory = entityManagerFactory; + } + /** + * Registers Hibernate event listeners for POST_UPDATE and POST_DELETE events when the application is ready. + * + * <p> + * If the {@link EventListenerRegistry} is available, the listeners are appended and a debug message is logged. + * If the registry is null, a warning is logged indicating a possible misconfiguration. + */ + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { var eventListenerRegistry = entityManagerFactory.unwrap(SessionFactoryImpl.class).getServiceRegistry().getService(EventListenerRegistry.class); - eventListenerRegistry.appendListeners(EventType.POST_UPDATE, this); - eventListenerRegistry.appendListeners(EventType.POST_DELETE, this); - log.debug("Registered Hibernate listeners"); + if (eventListenerRegistry != null) { + eventListenerRegistry.appendListeners(EventType.POST_UPDATE, this); + eventListenerRegistry.appendListeners(EventType.POST_DELETE, this); + log.debug("Registered Hibernate listeners"); + } + else { + log.warn("Could not register Hibernate listeners because the EventListenerRegistry is null. This is likely due to a misconfiguration of the entity manager factory."); + } } @Override diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java index 0ed155fc5718..d56e175774b0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/BuildScriptProviderService.java @@ -12,7 +12,9 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -44,15 +46,17 @@ public class BuildScriptProviderService { */ public BuildScriptProviderService(ResourceLoaderService resourceLoaderService) { this.resourceLoaderService = resourceLoaderService; - // load all scripts into the cache - cacheOnBoot(); } /** - * Loads all scripts from the resources/templates/aeolus directory into the cache - * The windfiles are ignored, since they are only used for the windfile and are cached in {@link AeolusTemplateService} + * Loads all scripts from the resources/templates/aeolus directory into the cache. + * + * <p> + * Windfiles are ignored since they are only used for the windfile and are cached in {@link AeolusTemplateService}. + * Each script is read, processed, and stored in the {@code scriptCache}. Errors during loading are logged. */ - private void cacheOnBoot() { + @EventListener(ApplicationReadyEvent.class) + public void cacheOnBoot() { var resources = this.resourceLoaderService.getResources(Path.of("templates", "aeolus")); for (var resource : resources) { try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java index 0ea9d2f6daf2..593eabbf1ec0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/GitService.java @@ -1131,17 +1131,7 @@ public void anonymizeStudentCommits(Repository repository, ProgrammingExercise p studentGit.branchDelete().setBranchNames(copyBranchName).setForce(true).call(); // Delete all remotes - for (RemoteConfig remote : studentGit.remoteList().call()) { - studentGit.remoteRemove().setRemoteName(remote.getName()).call(); - // Manually delete remote tracking branches since JGit apparently fails to do so - for (Ref ref : studentGit.getRepository().getRefDatabase().getRefs()) { - if (ref.getName().startsWith("refs/remotes/" + remote.getName())) { - RefUpdate update = studentGit.getRepository().updateRef(ref.getName()); - update.setForceUpdate(true); - update.delete(); - } - } - } + this.removeRemotes(studentGit); // Delete .git/logs/ folder to delete git reflogs Path logsPath = Path.of(repository.getDirectory().getPath(), "logs"); @@ -1161,6 +1151,46 @@ public void anonymizeStudentCommits(Repository repository, ProgrammingExercise p } } + /** + * Removes all remote configurations from the given Git repository. + * This includes both the remote configurations and the remote tracking branches. + * + * @param repository The Git repository from which to remove the remotes. + * @throws IOException If an I/O error occurs when accessing the repository. + * @throws GitAPIException If an error occurs in the JGit library while removing the remotes. + */ + private void removeRemotes(Git repository) throws IOException, GitAPIException { + // Delete all remotes + for (RemoteConfig remote : repository.remoteList().call()) { + repository.remoteRemove().setRemoteName(remote.getName()).call(); + // Manually delete remote tracking branches since JGit apparently fails to do so + for (Ref ref : repository.getRepository().getRefDatabase().getRefs()) { + if (ref.getName().startsWith("refs/remotes/" + remote.getName())) { + RefUpdate update = repository.getRepository().updateRef(ref.getName()); + update.setForceUpdate(true); + update.delete(); + } + } + } + } + + /** + * Removes all remotes from a given repository. + * + * @param repository The repository whose remotes to delete. + */ + public void removeRemotesFromRepository(Repository repository) { + try (Git gitRepo = new Git(repository)) { + this.removeRemotes(gitRepo); + } + catch (EntityNotFoundException | GitAPIException | JGitInternalException | IOException ex) { + log.warn("Cannot remove the remotes of the repo {} due to the following exception: {}", repository.getLocalPath(), ex.getMessage()); + } + finally { + repository.close(); + } + } + private static class FileAndDirectoryFilter implements IOFileFilter { private static final String GIT_DIRECTORY_NAME = ".git"; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/HazelcastHealthIndicator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/HazelcastHealthIndicator.java index 762837d5dd6b..833ac60b7f83 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/HazelcastHealthIndicator.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/HazelcastHealthIndicator.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.context.annotation.Profile; @@ -19,7 +20,7 @@ public class HazelcastHealthIndicator implements HealthIndicator { private final HazelcastInstance hazelcastInstance; - public HazelcastHealthIndicator(HazelcastInstance hazelcastInstance) { + public HazelcastHealthIndicator(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { this.hazelcastInstance = hazelcastInstance; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusTemplateService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusTemplateService.java index 29548a9d6bf8..3e1043d8b19f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusTemplateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/aeolus/AeolusTemplateService.java @@ -10,7 +10,9 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; @@ -49,11 +51,17 @@ public AeolusTemplateService(ProgrammingLanguageConfiguration programmingLanguag this.programmingLanguageConfiguration = programmingLanguageConfiguration; this.resourceLoaderService = resourceLoaderService; this.buildScriptProviderService = buildScriptProviderService; - // load all scripts into the cache - cacheOnBoot(); } - private void cacheOnBoot() { + /** + * Loads all YAML scripts from the "templates/aeolus" directory into the cache when the application is ready. + * + * <p> + * Scripts are read, processed, and stored in the {@code templateCache}. Errors during loading are logged. + */ + @EventListener(ApplicationReadyEvent.class) + public void cacheOnBoot() { + // load all scripts into the cache var resources = this.resourceLoaderService.getResources(Path.of("templates", "aeolus")); for (var resource : resources) { try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/apollon/ApollonConversionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/apollon/ApollonConversionService.java index 03a12b6eb36c..65affb314552 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/apollon/ApollonConversionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/apollon/ApollonConversionService.java @@ -47,14 +47,15 @@ public InputStream convertModel(String model) { request.setModel(model); var response = restTemplate.postForEntity(apollonConversionUrl + "/pdf", request, Resource.class); - assert response.getBody() != null; - return response.getBody().getInputStream(); + if (response.getBody() != null) { + return response.getBody().getInputStream(); + } } catch (HttpClientErrorException ex) { log.error("Error while calling Remote Service: {}", ex.getMessage()); } catch (IOException ex) { - log.error(ex.getMessage()); + log.error(ex.getMessage(), ex); } return null; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java index ce39742597a2..2bea0d587205 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java @@ -26,7 +26,7 @@ /** * Service for receiving feedback suggestions from the Athena service. - * Assumes that submissions and already given feedback have already been sent to Athena. + * Assumes that submissions and already given feedback have already been sent to Athena or that the feedback is non-graded. */ @Service @Profile("athena") @@ -60,7 +60,7 @@ public AthenaFeedbackSuggestionsService(@Qualifier("athenaRestTemplate") RestTem this.athenaModuleService = athenaModuleService; } - private record RequestDTO(ExerciseDTO exercise, SubmissionDTO submission) { + private record RequestDTO(ExerciseDTO exercise, SubmissionDTO submission, boolean isGraded) { } private record ResponseDTOText(List<TextFeedbackDTO> data) { @@ -77,10 +77,11 @@ private record ResponseDTOModeling(List<ModelingFeedbackDTO> data) { * * @param exercise the {@link TextExercise} the suggestions are fetched for * @param submission the {@link TextSubmission} the suggestions are fetched for + * @param isGraded the {@link Boolean} should Athena generate grade suggestions or not * @return a list of feedback suggestions */ - public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, TextSubmission submission) throws NetworkingException { - log.debug("Start Athena Feedback Suggestions Service for Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId()); + public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, TextSubmission submission, boolean isGraded) throws NetworkingException { + log.debug("Start Athena '{}' Feedback Suggestions Service for Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId()); if (!Objects.equals(submission.getParticipation().getExercise().getId(), exercise.getId())) { log.error("Exercise id {} does not match submission's exercise id {}", exercise.getId(), submission.getParticipation().getExercise().getId()); @@ -88,9 +89,9 @@ public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, T "Exercise", "exerciseIdDoesNotMatch"); } - final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission)); + final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); - log.info("Athena responded to feedback suggestions request: {}", response.data); + log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); return response.data.stream().toList(); } @@ -99,14 +100,15 @@ public List<TextFeedbackDTO> getTextFeedbackSuggestions(TextExercise exercise, T * * @param exercise the {@link ProgrammingExercise} the suggestions are fetched for * @param submission the {@link ProgrammingSubmission} the suggestions are fetched for + * @param isGraded the {@link Boolean} should Athena generate grade suggestions or not * @return a list of feedback suggestions */ - public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(ProgrammingExercise exercise, ProgrammingSubmission submission) throws NetworkingException { - log.debug("Start Athena Feedback Suggestions Service for Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId()); - - final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission)); + public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(ProgrammingExercise exercise, ProgrammingSubmission submission, boolean isGraded) + throws NetworkingException { + log.debug("Start Athena '{}' Feedback Suggestions Service for Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId()); + final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); - log.info("Athena responded to feedback suggestions request: {}", response.data); + log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); return response.data.stream().toList(); } @@ -115,19 +117,20 @@ public List<ProgrammingFeedbackDTO> getProgrammingFeedbackSuggestions(Programmin * * @param exercise the {@link ModelingExercise} the suggestions are fetched for * @param submission the {@link ModelingSubmission} the suggestions are fetched for + * @param isGraded the {@link Boolean} should Athena generate grade suggestions or not * @return a list of feedback suggestions generated by Athena */ - public List<ModelingFeedbackDTO> getModelingFeedbackSuggestions(ModelingExercise exercise, ModelingSubmission submission) throws NetworkingException { - log.debug("Start Athena Feedback Suggestions Service for Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId()); + public List<ModelingFeedbackDTO> getModelingFeedbackSuggestions(ModelingExercise exercise, ModelingSubmission submission, boolean isGraded) throws NetworkingException { + log.debug("Start Athena '{}' Feedback Suggestions Service for Modeling Exercise '{}' (#{}).", isGraded ? "Graded" : "Non Graded", exercise.getTitle(), exercise.getId()); if (!Objects.equals(submission.getParticipation().getExercise().getId(), exercise.getId())) { throw new ConflictException("Exercise id " + exercise.getId() + " does not match submission's exercise id " + submission.getParticipation().getExercise().getId(), "Exercise", "exerciseIdDoesNotMatch"); } - final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission)); + final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOModeling response = modelingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); - log.info("Athena responded to feedback suggestions request: {}", response.data); + log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); return response.data; } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java index 00424d0741ec..f0fadc007e26 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java @@ -64,8 +64,8 @@ public AthenaRepositoryExportService(ProgrammingExerciseRepository programmingEx * @param exercise the exercise to check * @throws AccessForbiddenException if the feedback suggestions are not enabled for the given exercise */ - private void checkFeedbackSuggestionsEnabledElseThrow(Exercise exercise) { - if (!exercise.areFeedbackSuggestionsEnabled()) { + private void checkFeedbackSuggestionsOrAutomaticFeedbackEnabledElseThrow(Exercise exercise) { + if (!(exercise.areFeedbackSuggestionsEnabled() || exercise.getAllowFeedbackRequests())) { log.error("Feedback suggestions are not enabled for exercise {}", exercise.getId()); throw new ServiceUnavailableException("Feedback suggestions are not enabled for exercise"); } @@ -86,7 +86,7 @@ public File exportRepository(long exerciseId, Long submissionId, RepositoryType log.debug("Exporting repository for exercise {}, submission {}", exerciseId, submissionId); var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - checkFeedbackSuggestionsEnabledElseThrow(programmingExercise); + checkFeedbackSuggestionsOrAutomaticFeedbackEnabledElseThrow(programmingExercise); var exportOptions = new RepositoryExportOptionsDTO(); exportOptions.setAnonymizeRepository(true); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorException.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorException.java deleted file mode 100644 index 61cd30bd97cd..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorException.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris; - -// TODO -public class IrisConnectorException extends Exception { - - public IrisConnectorException(String message) { - super(message); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorService.java deleted file mode 100644 index 09daf897f988..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/IrisConnectorService.java +++ /dev/null @@ -1,142 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris; - -import static java.util.concurrent.CompletableFuture.completedFuture; -import static java.util.concurrent.CompletableFuture.failedFuture; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisMessageResponseV2DTO; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisModelDTO; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisRequestV2DTO; -import de.tum.in.www1.artemis.service.iris.exception.IrisException; -import de.tum.in.www1.artemis.service.iris.exception.IrisForbiddenException; -import de.tum.in.www1.artemis.service.iris.exception.IrisInternalPyrisErrorException; -import de.tum.in.www1.artemis.service.iris.exception.IrisInvalidTemplateException; -import de.tum.in.www1.artemis.service.iris.exception.IrisModelNotAvailableException; -import de.tum.in.www1.artemis.service.iris.exception.IrisNoResponseException; -import de.tum.in.www1.artemis.service.iris.exception.IrisParseResponseException; - -/** - * This service connects to the Python implementation of Iris (called Pyris) responsible for connecting to different - * LLMs and handle messages with Microsoft Guidance - */ -@Service -@Profile("iris") -public class IrisConnectorService { - - private static final Logger log = LoggerFactory.getLogger(IrisConnectorService.class); - - private final RestTemplate restTemplate; - - private final ObjectMapper objectMapper; - - @Value("${artemis.iris.url}") - private String irisUrl; - - public IrisConnectorService(@Qualifier("irisRestTemplate") RestTemplate restTemplate, MappingJackson2HttpMessageConverter springMvcJacksonConverter) { - this.restTemplate = restTemplate; - this.objectMapper = springMvcJacksonConverter.getObjectMapper(); - } - - /** - * Requests all available models from Pyris - * - * @return A list of available Models as IrisModelDTO - */ - public List<IrisModelDTO> getOfferedModels() throws IrisConnectorException { - try { - var response = restTemplate.getForEntity(irisUrl + "/api/v1/models", JsonNode.class); - if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { - throw new IrisConnectorException("Could not fetch offered models"); - } - IrisModelDTO[] models = objectMapper.treeToValue(response.getBody(), IrisModelDTO[].class); - return Arrays.asList(models); - } - catch (HttpStatusCodeException | JsonProcessingException e) { - log.error("Failed to fetch offered models from Pyris", e); - throw new IrisConnectorException("Could not fetch offered models"); - } - } - - /** - * Requests a response from Pyris using the V2 Messages API - * - * @param template The guidance program to execute - * @param preferredModel The LLM model to be used (e.g., GPT3.5-turbo). Note: The used model might not be the - * preferred model (e.g., if an error occurs or the preferredModel is not reachable) - * @param argumentsDTO A map of argument variables required for the guidance template (if they are specified in - * the template) - * @return The response of the type {@link IrisMessageResponseV2DTO} - */ - @Async - public CompletableFuture<IrisMessageResponseV2DTO> sendRequestV2(String template, String preferredModel, Object argumentsDTO) { - var endpoint = "/api/v2/messages"; - var request = new IrisRequestV2DTO(template, preferredModel, argumentsDTO); - return tryGetResponse(endpoint, request, preferredModel, IrisMessageResponseV2DTO.class); - } - - private <T> CompletableFuture<T> tryGetResponse(String endpoint, Object request, String preferredModel, Class<T> responseType) { - try { - return sendRequestAndParseResponse(endpoint, request, responseType); - } - catch (JsonProcessingException e) { - log.error("Failed to parse response from Pyris", e); - return failedFuture(new IrisParseResponseException(e)); - } - catch (HttpStatusCodeException e) { - return failedFuture(toIrisException(e, preferredModel)); - } - catch (RestClientException | IllegalArgumentException e) { - log.error("Failed to send request to Pyris", e); - return failedFuture(new IrisConnectorException("Could not fetch response from Iris")); - } - } - - private <Response> CompletableFuture<Response> sendRequestAndParseResponse(String urlExtension, Object request, Class<Response> responseType) throws JsonProcessingException { - var response = restTemplate.postForEntity(irisUrl + urlExtension, objectMapper.valueToTree(request), JsonNode.class); - JsonNode body = response.getBody(); - if (body == null) { - return failedFuture(new IrisNoResponseException()); - } - Response parsed = objectMapper.treeToValue(body, responseType); - return completedFuture(parsed); - } - - private IrisException toIrisException(HttpStatusCodeException e, String preferredModel) { - return switch (e.getStatusCode().value()) { - case 401, 403 -> new IrisForbiddenException(); - case 400 -> new IrisInvalidTemplateException(tryExtractErrorMessage(e)); - case 404 -> new IrisModelNotAvailableException(preferredModel, tryExtractErrorMessage(e)); - case 500 -> new IrisInternalPyrisErrorException(tryExtractErrorMessage(e)); - default -> new IrisInternalPyrisErrorException(e.getMessage()); - }; - } - - private String tryExtractErrorMessage(HttpStatusCodeException ex) { - try { - return objectMapper.readTree(ex.getResponseBodyAsString()).required("detail").required("errorMessage").asText(); - } - catch (JsonProcessingException | IllegalArgumentException e) { - log.error("Failed to parse error message from Pyris", e); - return ""; - } - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisErrorResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisErrorResponseDTO.java deleted file mode 100644 index 849b0a41f607..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisErrorResponseDTO.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris.dto; - -// TODO -public record IrisErrorResponseDTO(String errorMessage) { -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisMessageResponseV2DTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisMessageResponseV2DTO.java deleted file mode 100644 index 2e7cb2f57f7d..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisMessageResponseV2DTO.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris.dto; - -import java.time.ZonedDateTime; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; - -public record IrisMessageResponseV2DTO(String usedModel, ZonedDateTime sentAt, JsonNode content) { - - /** - * Creates a new IrisMessageResponseV2DTO with the required fields. - * This constructor is a workaround for a strange issue with Jackson, where invalid data such as "invalid": "invalid" - * in the response from Iris would not result in an exception, but instead a successful deserialization of the response - * into an object with null fields (!?). - * We need the @JsonCreator annotation to tell Jackson to use this constructor, and the @JsonProperty annotations to - * tell Jackson that the fields are required and it should throw an exception if they are missing. - * See also: IrisMessageResponseDTO. - * TODO: Investigate this issue further and remove this workaround if possible. - */ - @JsonCreator // @formatter:off - public IrisMessageResponseV2DTO(@JsonProperty(value = "usedModel", required = true) String usedModel, - @JsonProperty(value = "sentAt", required = true) ZonedDateTime sentAt, - @JsonProperty(value = "content", required = true) JsonNode content) { - this.usedModel = usedModel; - this.sentAt = sentAt; - this.content = content; - } // @formatter:on - -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisModelDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisModelDTO.java deleted file mode 100644 index 53f686cce216..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisModelDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris.dto; - -public record IrisModelDTO(String id, String name, String description) { -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisRequestV2DTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisRequestV2DTO.java deleted file mode 100644 index 0e293ff50cfa..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisRequestV2DTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris.dto; - -public record IrisRequestV2DTO(String template, String preferredModel, Object parameters) { -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisStatusDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisStatusDTO.java deleted file mode 100644 index 51d346526569..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/iris/dto/IrisStatusDTO.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.iris.dto; - -public record IrisStatusDTO(String model, ModelStatus status) { - - public enum ModelStatus { - UP, DOWN, NOT_AVAILABLE - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIQueueWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIQueueWebsocketService.java index d78449fa41b1..17c2ac0b247d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIQueueWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIQueueWebsocketService.java @@ -1,9 +1,12 @@ package de.tum.in.www1.artemis.service.connectors.localci; +import java.util.List; + import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -45,7 +48,7 @@ public class LocalCIQueueWebsocketService { * @param localCIWebsocketMessagingService the local ci build queue websocket service * @param sharedQueueManagementService the local ci shared build job queue service */ - public LocalCIQueueWebsocketService(HazelcastInstance hazelcastInstance, LocalCIWebsocketMessagingService localCIWebsocketMessagingService, + public LocalCIQueueWebsocketService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, LocalCIWebsocketMessagingService localCIWebsocketMessagingService, SharedQueueManagementService sharedQueueManagementService) { this.hazelcastInstance = hazelcastInstance; this.localCIWebsocketMessagingService = localCIWebsocketMessagingService; @@ -75,8 +78,20 @@ private void sendProcessingJobsOverWebsocket(long courseId) { localCIWebsocketMessagingService.sendRunningBuildJobsForCourse(courseId, sharedQueueManagementService.getProcessingJobsForCourse(courseId)); } - private void sendBuildAgentInformationOverWebsocket() { - localCIWebsocketMessagingService.sendBuildAgentInformation(sharedQueueManagementService.getBuildAgentInformation()); + private void sendBuildAgentSummaryOverWebsocket() { + // remove the recentBuildJobs from the build agent information before sending it over the websocket + List<LocalCIBuildAgentInformation> buildAgentSummary = sharedQueueManagementService.getBuildAgentInformationWithoutRecentBuildJobs(); + localCIWebsocketMessagingService.sendBuildAgentSummary(buildAgentSummary); + } + + private void sendBuildAgentDetailsOverWebsocket(String agentName) { + sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() + .ifPresent(localCIWebsocketMessagingService::sendBuildAgentDetails); + } + + private void sendBuildAgentInformationOverWebsocket(String agentName) { + sendBuildAgentSummaryOverWebsocket(); + sendBuildAgentDetailsOverWebsocket(agentName); } private class QueuedBuildJobItemListener implements ItemListener<LocalCIBuildJobQueueItem> { @@ -113,19 +128,19 @@ private class BuildAgentListener implements EntryAddedListener<String, LocalCIBu @Override public void entryAdded(com.hazelcast.core.EntryEvent<String, LocalCIBuildAgentInformation> event) { log.debug("Build agent added: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(); + sendBuildAgentInformationOverWebsocket(event.getValue().name()); } @Override public void entryRemoved(com.hazelcast.core.EntryEvent<String, LocalCIBuildAgentInformation> event) { log.debug("Build agent removed: {}", event.getOldValue()); - sendBuildAgentInformationOverWebsocket(); + sendBuildAgentInformationOverWebsocket(event.getOldValue().name()); } @Override public void entryUpdated(com.hazelcast.core.EntryEvent<String, LocalCIBuildAgentInformation> event) { log.debug("Build agent updated: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(); + sendBuildAgentInformationOverWebsocket(event.getValue().name()); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java index 44228cc97328..564f31c82a56 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIResultProcessingService.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -73,7 +74,7 @@ public class LocalCIResultProcessingService { private UUID listenerId; - public LocalCIResultProcessingService(HazelcastInstance hazelcastInstance, ProgrammingExerciseGradingService programmingExerciseGradingService, + public LocalCIResultProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ProgrammingExerciseGradingService programmingExerciseGradingService, ProgrammingMessagingService programmingMessagingService, BuildJobRepository buildJobRepository, ProgrammingExerciseRepository programmingExerciseRepository, ParticipationRepository participationRepository, ProgrammingTriggerService programmingTriggerService, BuildLogEntryService buildLogEntryService) { this.hazelcastInstance = hazelcastInstance; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java index 0b4d956c0e80..a396aa1e74f8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCITriggerService.java @@ -12,6 +12,7 @@ import org.hibernate.Hibernate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -73,7 +74,7 @@ public class LocalCITriggerService implements ContinuousIntegrationTriggerServic private IMap<String, ZonedDateTime> dockerImageCleanupInfo; - public LocalCITriggerService(HazelcastInstance hazelcastInstance, AeolusTemplateService aeolusTemplateService, + public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, AeolusTemplateService aeolusTemplateService, ProgrammingLanguageConfiguration programmingLanguageConfiguration, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, LocalCIProgrammingLanguageFeatureService programmingLanguageFeatureService, Optional<VersionControlService> versionControlService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java index b3dd3b27a556..7384e88693d8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/SharedQueueManagementService.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -21,6 +22,7 @@ import com.hazelcast.topic.ITopic; import de.tum.in.www1.artemis.repository.BuildJobRepository; +import de.tum.in.www1.artemis.service.ProfileService; import de.tum.in.www1.artemis.service.connectors.localci.dto.DockerImageBuild; import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildAgentInformation; import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildJobQueueItem; @@ -38,6 +40,8 @@ public class SharedQueueManagementService { private final HazelcastInstance hazelcastInstance; + private final ProfileService profileService; + private IQueue<LocalCIBuildJobQueueItem> queue; /** @@ -56,9 +60,10 @@ public class SharedQueueManagementService { private ITopic<String> canceledBuildJobsTopic; - public SharedQueueManagementService(BuildJobRepository buildJobRepository, HazelcastInstance hazelcastInstance) { + public SharedQueueManagementService(BuildJobRepository buildJobRepository, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ProfileService profileService) { this.buildJobRepository = buildJobRepository; this.hazelcastInstance = hazelcastInstance; + this.profileService = profileService; } /** @@ -75,14 +80,19 @@ public void init() { } /** - * Pushes the last build dates for all docker images to the hazelcast map dockerImageCleanupInfo + * Pushes the last build dates for all docker images to the hazelcast map dockerImageCleanupInfo, only executed on the main node (with active scheduling) + * This method is scheduled to run every 5 minutes with an initial delay of 30 seconds. */ - @Scheduled(fixedRate = 90000, initialDelay = 1000 * 60 * 10) + @Scheduled(fixedRate = 5 * 60 * 1000, initialDelay = 30 * 1000) public void pushDockerImageCleanupInfo() { - dockerImageCleanupInfo.clear(); - Set<DockerImageBuild> lastBuildDatesForDockerImages = buildJobRepository.findAllLastBuildDatesForDockerImages(); - for (DockerImageBuild dockerImageBuild : lastBuildDatesForDockerImages) { - dockerImageCleanupInfo.put(dockerImageBuild.dockerImage(), dockerImageBuild.lastBuildCompletionDate()); + if (profileService.isSchedulingActive()) { + var startDate = System.currentTimeMillis(); + dockerImageCleanupInfo.clear(); + Set<DockerImageBuild> lastBuildDatesForDockerImages = buildJobRepository.findAllLastBuildDatesForDockerImages(); + for (DockerImageBuild dockerImageBuild : lastBuildDatesForDockerImages) { + dockerImageCleanupInfo.put(dockerImageBuild.dockerImage(), dockerImageBuild.lastBuildCompletionDate()); + } + log.info("pushDockerImageCleanupInfo took {}ms", System.currentTimeMillis() - startDate); } } @@ -106,6 +116,11 @@ public List<LocalCIBuildAgentInformation> getBuildAgentInformation() { return buildAgentInformation.values().stream().toList(); } + public List<LocalCIBuildAgentInformation> getBuildAgentInformationWithoutRecentBuildJobs() { + return buildAgentInformation.values().stream().map(agent -> new LocalCIBuildAgentInformation(agent.name(), agent.maxNumberOfConcurrentBuildJobs(), + agent.numberOfCurrentBuildJobs(), agent.runningBuildJobs(), agent.status(), null)).toList(); + } + /** * Cancel a build job by removing it from the queue or stopping the build process. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobContainerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobContainerService.java index 36162ede88db..7a136575cc3b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobContainerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobContainerService.java @@ -33,6 +33,7 @@ import com.github.dockerjava.api.command.ExecCreateCmdResponse; import com.github.dockerjava.api.exception.ConflictException; import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.exception.NotModifiedException; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.HostConfig; @@ -160,30 +161,29 @@ public TarArchiveInputStream getArchiveFromContainer(String containerId, String /** * Stops the container with the given name by creating a file "stop_container.txt" in its root directory. * The container must be created in such a way that it waits for this file to appear and then stops running, causing it to be removed at the same time. - * You could also use {@link DockerClient#stopContainerCmd(String)} to stop the container, but this takes significantly longer than using the approach with the file because of - * increased overhead for the stopContainerCmd() method. + * In case the container is not responding, we can force remove it using {@link DockerClient#removeContainerCmd(String)}. + * This takes significantly longer than using the approach with the file because of increased overhead for the removeContainerCmd() method. * * @param containerName The name of the container to stop. Cannot use the container ID, because this method might have to be called from the main thread (not the thread started * for the build job) where the container ID is not available. */ public void stopContainer(String containerName) { // List all containers, including the non-running ones. - List<Container> containers = dockerClient.listContainersCmd().withShowAll(true).exec(); + Container container = getContainerForName(containerName); - // Check if there's a container with the given name. - Optional<Container> containerOptional = containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst(); - if (containerOptional.isEmpty()) { + // Check if the container exists. Return if it does not. + if (container == null) { return; } // Check if the container is running. Return if it's not. - boolean isContainerRunning = "running".equals(containerOptional.get().getState()); + boolean isContainerRunning = "running".equals(container.getState()); if (!isContainerRunning) { return; } // Get the container ID. - String containerId = containerOptional.get().getId(); + String containerId = container.getId(); // Create a file "stop_container.txt" in the root directory of the container to indicate that the test results have been extracted or that the container should be stopped // for some other reason. @@ -191,6 +191,46 @@ public void stopContainer(String containerName) { executeDockerCommandWithoutAwaitingResponse(containerId, "touch", LOCALCI_WORKING_DIRECTORY + "/stop_container.txt"); } + /** + * Stops or kills a container in case a build job has failed or the container is unresponsive. + * Adding a file "stop_container.txt" like in {@link #stopContainer(String)} might not work for unresponsive containers, thus we use + * {@link DockerClient#stopContainerCmd(String)} and {@link DockerClient#killContainerCmd(String)} to stop or kill the container. + * + * @param containerId The ID of the container to stop or kill. + */ + public void stopUnresponsiveContainer(String containerId) { + try { + // Attempt to stop the container. It should stop the container and auto-remove it. + // {@link DockerClient#stopContainerCmd(String)} first sends a SIGTERM command to the container to gracefully stop it, + // and if it does not stop within the timeout, it sends a SIGKILL command to kill the container. + dockerClient.stopContainerCmd(containerId).withTimeout(5).exec(); + } + catch (NotFoundException | NotModifiedException e) { + log.debug("Container with id {} is already stopped: {}", containerId, e.getMessage()); + } + catch (Exception e) { + // In case the stopContainerCmd fails, we try to forcefully kill the container + try { + dockerClient.killContainerCmd(containerId).exec(); + } + catch (Exception killException) { + log.warn("Failed to kill container with id {}: {}", containerId, killException.getMessage()); + } + } + } + + /** + * Get the ID of a running container by its name. + * + * @param containerName The name of the container. + * @return The ID of the running container or null if no running container with the given name was found. + */ + public String getIDOfRunningContainer(String containerName) { + Container container = getContainerForName(containerName); + // Return id if container not null + return Optional.ofNullable(container).map(Container::getId).orElse(null); + } + /** * Prepares a Docker container for a build job by setting up the required directories and repositories within the container. * This includes setting up directories for assignment, tests, solutions, and any auxiliary repositories provided. @@ -234,8 +274,6 @@ public void populateBuildJobContainer(String buildJobContainerId, Path assignmen for (int i = 0; i < auxiliaryRepositoriesPaths.length; i++) { addAndPrepareDirectory(buildJobContainerId, auxiliaryRepositoriesPaths[i], LOCALCI_WORKING_DIRECTORY + "/testing-dir/" + auxiliaryRepositoryCheckoutDirectories[i]); } - // TODO: this might lead to issues in certain builds - // convertDosFilesToUnix(LOCALCI_WORKING_DIRECTORY + "/testing-dir/", buildJobContainerId); createScriptFile(buildJobContainerId); } @@ -259,10 +297,6 @@ private void addDirectory(String containerId, String directoryName, boolean crea executeDockerCommand(containerId, null, false, false, true, command); } - private void convertDosFilesToUnix(String path, String containerId) { - executeDockerCommand(containerId, null, false, false, true, "sh", "-c", "find " + path + " -type f ! -path '*/.git/*' -exec sed -i 's/\\r$//' {} \\;"); - } - private void copyToContainer(String sourcePath, String containerId) { try (InputStream uploadStream = new ByteArrayInputStream(createTarArchive(sourcePath).toByteArray())) { dockerClient.copyArchiveToContainerCmd(containerId).withRemotePath(LOCALCI_WORKING_DIRECTORY).withTarInputStream(uploadStream).exec(); @@ -365,4 +399,9 @@ private void checkPath(String path) { throw new LocalCIException("Invalid path: " + path); } } + + private Container getContainerForName(String containerName) { + List<Container> containers = dockerClient.listContainersCmd().withShowAll(true).exec(); + return containers.stream().filter(container -> container.getNames()[0].equals("/" + containerName)).findFirst().orElse(null); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobManagementService.java index 49db77c518b0..f132ef63496f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobManagementService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/BuildJobManagementService.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -79,8 +80,8 @@ public class BuildJobManagementService { */ private final Set<String> cancelledBuildJobs = new ConcurrentSkipListSet<>(); - public BuildJobManagementService(HazelcastInstance hazelcastInstance, BuildJobExecutionService buildJobExecutionService, ExecutorService localCIBuildExecutorService, - BuildJobContainerService buildJobContainerService, BuildLogsMap buildLogsMap) { + public BuildJobManagementService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, BuildJobExecutionService buildJobExecutionService, + ExecutorService localCIBuildExecutorService, BuildJobContainerService buildJobContainerService, BuildLogsMap buildLogsMap) { this.buildJobExecutionService = buildJobExecutionService; this.localCIBuildExecutorService = localCIBuildExecutorService; this.buildJobContainerService = buildJobContainerService; @@ -213,7 +214,10 @@ private void finishBuildJobExceptionally(String buildJobId, String containerName buildLogsMap.appendBuildLogEntry(buildJobId, new BuildLogEntry(ZonedDateTime.now(), msg + "\n" + stackTrace)); log.error(msg); - buildJobContainerService.stopContainer(containerName); + String containerId = buildJobContainerService.getIDOfRunningContainer(containerName); + if (containerId != null) { + buildJobContainerService.stopUnresponsiveContainer(containerId); + } } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/LocalCIDockerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/LocalCIDockerService.java index 664e38641f7f..ce568edc583a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/LocalCIDockerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/LocalCIDockerService.java @@ -2,24 +2,28 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_BUILDAGENT; +import java.io.File; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -53,6 +57,10 @@ public class LocalCIDockerService { private final HazelcastInstance hazelcastInstance; + private final BuildJobContainerService buildJobContainerService; + + private final TaskScheduler taskScheduler; + private boolean isFirstCleanup = true; @Value("${artemis.continuous-integration.image-cleanup.enabled:false}") @@ -61,6 +69,9 @@ public class LocalCIDockerService { @Value("${artemis.continuous-integration.image-cleanup.expiry-days:2}") private int imageExpiryDays; + @Value("${artemis.continuous-integration.image-cleanup.disk-space-threshold-mb:2000}") + private int imageCleanupDiskSpaceThresholdMb; + @Value("${artemis.continuous-integration.build-container-prefix:local-ci-}") private String buildContainerPrefix; @@ -77,16 +88,18 @@ public class LocalCIDockerService { @Value("${artemis.continuous-integration.image-architecture:amd64}") private String imageArchitecture; - public LocalCIDockerService(DockerClient dockerClient, HazelcastInstance hazelcastInstance) { + public LocalCIDockerService(DockerClient dockerClient, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, BuildJobContainerService buildJobContainerService, + @Qualifier("taskScheduler") TaskScheduler taskScheduler) { this.dockerClient = dockerClient; this.hazelcastInstance = hazelcastInstance; + this.buildJobContainerService = buildJobContainerService; + this.taskScheduler = taskScheduler; } @EventListener(ApplicationReadyEvent.class) public void applicationReady() { - // Schedule the cleanup of dangling build containers once 10 seconds after the application has started and then every containerCleanupScheduleHour hours - ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); - scheduledExecutorService.scheduleAtFixedRate(this::cleanUpContainers, 10, containerCleanupScheduleMinutes * 60L, TimeUnit.SECONDS); + // Schedule the cleanup of dangling build containers once 10 seconds after the application has started and then every containerCleanupScheduleMinutes minutes + taskScheduler.scheduleAtFixedRate(this::cleanUpContainers, Instant.now().plusSeconds(10), Duration.ofMinutes(containerCleanupScheduleMinutes)); } /** @@ -109,7 +122,7 @@ public void applicationReady() { */ public void cleanUpContainers() { List<Container> danglingBuildContainers; - log.debug("Start cleanup dangling build containers"); + log.info("Start cleanup dangling build containers"); if (isFirstCleanup) { // Cleanup all dangling build containers after the application has started try { @@ -135,9 +148,9 @@ public void cleanUpContainers() { if (!danglingBuildContainers.isEmpty()) { log.info("Found {} dangling build containers", danglingBuildContainers.size()); - danglingBuildContainers.forEach(container -> dockerClient.removeContainerCmd(container.getId()).withForce(true).exec()); + danglingBuildContainers.forEach(container -> buildJobContainerService.stopUnresponsiveContainer(container.getId())); } - log.debug("Cleanup dangling build containers done"); + log.info("Cleanup dangling build containers done"); } /** @@ -177,7 +190,9 @@ public void onComplete() { * <p> * The process includes: * - Checking if the Docker image is already available locally. - * - If not available, acquiring a lock and checking again to handle any race conditions. + * - If not available, acquiring a lock to prevent concurrent pulls. + * - Checking for usable disk space and triggering image cleanup if the threshold is exceeded. + * - Re-inspecting the image to confirm its absence after acquiring the lock. * - Pulling the image if both checks confirm its absence. * - Logging the operations and their outcomes to build logs for user visibility. * <p> @@ -211,6 +226,8 @@ public void pullDockerImage(LocalCIBuildJobQueueItem buildJob, BuildLogsMap buil checkImageArchitecture(imageName, inspectImageResponse, buildJob, buildLogsMap); } catch (NotFoundException | BadRequestException e2) { + checkUsableDiskSpaceThenCleanUp(); + long start = System.nanoTime(); String msg = "~~~~~~~~~~~~~~~~~~~~ Pulling docker image " + imageName + " with a lock after error " + e.getMessage() + " ~~~~~~~~~~~~~~~~~~~~"; log.info(msg); @@ -267,8 +284,7 @@ private void checkImageArchitecture(String imageName, InspectImageResponse inspe * The process involves: * - Checking if image cleanup is enabled; if disabled, the operation is aborted. * - Retrieving a map of Docker images and their last usage dates. - * - Listing all currently running containers to ensure their images are not deleted. - * - Identifying all Docker images that are not currently in use. + * - Getting a set of image names that are not associated with any running containers. * - Removing images that have exceeded the configured expiry days and are not associated with any running containers. * <p> * Exception handling includes catching NotFoundException for cases where images are already deleted or not found during the cleanup process. @@ -279,15 +295,102 @@ private void checkImageArchitecture(String imageName, InspectImageResponse inspe @Scheduled(cron = "${artemis.continuous-integration.image-cleanup.cleanup-schedule-time:0 0 3 * * *}") public void deleteOldDockerImages() { - if (!imageCleanupEnabled) { log.info("Docker image cleanup is disabled"); return; } + Set<String> imageNames = getUnusedDockerImages(); + // Get map of docker images and their last build dates IMap<String, ZonedDateTime> dockerImageCleanupInfo = hazelcastInstance.getMap("dockerImageCleanupInfo"); + // Delete images that have not been used for more than imageExpiryDays days + for (String dockerImage : dockerImageCleanupInfo.keySet()) { + if (imageNames.contains(dockerImage)) { + if (dockerImageCleanupInfo.get(dockerImage).isBefore(ZonedDateTime.now().minusDays(imageExpiryDays))) { + log.info("Deleting docker image {}", dockerImage); + try { + dockerClient.removeImageCmd(dockerImage).exec(); + } + catch (NotFoundException e) { + log.warn("Docker image {} not found during cleanup", dockerImage); + } + } + } + } + } + + /** + * Checks for available disk space and triggers the cleanup of old Docker images if the available space falls below + * {@link LocalCIDockerService#imageCleanupDiskSpaceThresholdMb}. + * + * @implNote - We use the Docker root directory to check disk space availability. This is in case the Docker images are stored on a separate partition. + * - We need to iterate over the map entries since don't remove the oldest image from the map. + */ + + @Scheduled(fixedRateString = "${artemis.continuous-integration.image-cleanup.disk-space-check-interval-minutes:60}", initialDelayString = "${artemis.continuous-integration.image-cleanup.disk-space-check-interval-minutes:60}", timeUnit = TimeUnit.MINUTES) + public void checkUsableDiskSpaceThenCleanUp() { + if (!imageCleanupEnabled) { + return; + } + try { + // Get the Docker root directory to check disk space. + File dockerRootDirectory = new File(Objects.requireNonNullElse(dockerClient.infoCmd().exec().getDockerRootDir(), "/")); + long usableSpace = dockerRootDirectory.getUsableSpace(); + + long threshold = convertMegabytesToBytes(imageCleanupDiskSpaceThresholdMb); + + if (usableSpace >= threshold) { + return; + } + + // Get map of docker images and their last build dates + IMap<String, ZonedDateTime> dockerImageCleanupInfo = hazelcastInstance.getMap("dockerImageCleanupInfo"); + + // Get unused images + Set<String> unusedImages = getUnusedDockerImages(); + + // Get a sorted list of images by last build date + // We cast to ArrayList since we need the list to be mutable + List<Map.Entry<String, ZonedDateTime>> sortedImagesByLastBuildDate = dockerImageCleanupInfo.entrySet().stream().sorted(Map.Entry.comparingByValue()).toList(); + List<Map.Entry<String, ZonedDateTime>> mutableSortedImagesByLastBuildDate = new java.util.ArrayList<>(sortedImagesByLastBuildDate); + + if (mutableSortedImagesByLastBuildDate.isEmpty()) { + return; + } + + int deleteAttempts = 5; + int totalAttempts = mutableSortedImagesByLastBuildDate.size(); // We limit the total number of attempts to avoid infinite loops + Map.Entry<String, ZonedDateTime> oldestImage = mutableSortedImagesByLastBuildDate.getFirst(); + while (oldestImage != null && usableSpace < threshold && deleteAttempts > 0 && totalAttempts > 0) { + if (unusedImages.contains(oldestImage.getKey())) { + log.info("Deleting docker image {}", oldestImage.getKey()); + try { + dockerClient.removeImageCmd(oldestImage.getKey()).exec(); + usableSpace = dockerRootDirectory.getUsableSpace(); + deleteAttempts--; + } + catch (NotFoundException e) { + log.warn("Docker image {} not found during cleanup", oldestImage.getKey()); + } + } + mutableSortedImagesByLastBuildDate.remove(oldestImage); + oldestImage = mutableSortedImagesByLastBuildDate.getFirst(); + totalAttempts--; + } + } + catch (Exception e) { + log.error("Error while checking disk space for Docker image cleanup: {}", e.getMessage(), e); + } + } + + /** + * Gets a set of Docker image names that are not associated with any running containers. + * + * @return a set of image names that are not associated with any running containers. + */ + private Set<String> getUnusedDockerImages() { // Get list of all running containers List<Container> containers = dockerClient.listContainersCmd().exec(); @@ -307,20 +410,11 @@ public void deleteOldDockerImages() { Collections.addAll(imageNames, imageRepoTags); } } + return imageNames; + } - // Delete images that have not been used for more than imageExpiryDays days - for (String dockerImage : dockerImageCleanupInfo.keySet()) { - if (imageNames.contains(dockerImage)) { - if (dockerImageCleanupInfo.get(dockerImage).isBefore(ZonedDateTime.now().minusDays(imageExpiryDays))) { - log.info("Deleting docker image {}", dockerImage); - try { - dockerClient.removeImageCmd(dockerImage).exec(); - } - catch (NotFoundException e) { - log.warn("Docker image {} not found during cleanup", dockerImage); - } - } - } - } + private long convertMegabytesToBytes(int mb) { + long byteConversionRate = 1024L; + return mb * byteConversionRate * byteConversionRate; } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/SharedQueueProcessingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/SharedQueueProcessingService.java index 27a5f73bd5b5..85ef140d4898 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/buildagent/SharedQueueProcessingService.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -82,8 +83,8 @@ public class SharedQueueProcessingService { private UUID listenerId; - public SharedQueueProcessingService(HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService, BuildJobManagementService buildJobManagementService, - BuildLogsMap buildLogsMap) { + public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService, + BuildJobManagementService buildJobManagementService, BuildLogsMap buildLogsMap) { this.hazelcastInstance = hazelcastInstance; this.localCIBuildExecutorService = (ThreadPoolExecutor) localCIBuildExecutorService; this.buildJobManagementService = buildJobManagementService; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java index 2708355d0721..fdf6dc77680a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/Lti13Service.java @@ -27,7 +27,9 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -125,7 +127,8 @@ public void performLaunch(OidcIdToken ltiIdToken, String clientRegistrationId) { throw new BadRequestAlertException("LTI is not configured for this course", "LTI", "ltiNotConfigured"); } - Optional<String> optionalUsername = artemisAuthenticationProvider.getUsernameForEmail(ltiIdToken.getEmail()); + Optional<String> optionalUsername = artemisAuthenticationProvider.getUsernameForEmail(ltiIdToken.getEmail()) + .or(() -> userRepository.findOneByEmailIgnoreCase(ltiIdToken.getEmail()).map(User::getLogin)); if (!onlineCourseConfiguration.isRequireExistingUser() && optionalUsername.isEmpty()) { SecurityContextHolder.getContext().setAuthentication(ltiService.createNewUserFromLaunchRequest(ltiIdToken.getEmail(), @@ -172,7 +175,7 @@ else if (!StringUtils.isEmpty(ltiIdToken.getGivenName()) && !StringUtils.isEmpty private Lti13LaunchRequest launchRequestFrom(OidcIdToken ltiIdToken, String clientRegistrationId) { try { - return new Lti13LaunchRequest(ltiIdToken, clientRegistrationId); + return Lti13LaunchRequest.from(ltiIdToken, clientRegistrationId); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException("Could not create LTI 1.3 launch request with provided idToken: " + ex.getMessage()); @@ -232,16 +235,16 @@ protected void submitScore(LtiResourceLaunch launch, ClientRegistration clientRe return; } - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.valueOf("application/vnd.ims.lis.v1.score+json")); - headers.setBearerAuth(token); - String body = getScoreBody(launch.getSub(), comment, score); - HttpEntity<String> httpRequest = new HttpEntity<>(body, headers); try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.valueOf("application/vnd.ims.lis.v1.score+json")); + headers.setBearerAuth(token); + String body = getScoreBody(launch.getSub(), comment, score); + HttpEntity<String> httpRequest = new HttpEntity<>(body, headers); restTemplate.postForEntity(scoreLineItemUrl, httpRequest, Object.class); log.info("Submitted score for {} to client {}", launch.getUser().getLogin(), clientRegistration.getClientId()); } - catch (HttpClientErrorException e) { + catch (HttpClientErrorException | JsonProcessingException e) { String message = "Could not submit score for " + launch.getUser().getLogin() + " to client " + clientRegistration.getClientId() + ": " + e.getMessage(); log.error(message); } @@ -259,16 +262,17 @@ private String getScoresUrl(String lineItemUrl) { return builder.insert(index, "/scores").toString(); // Adds "/scores" before the "?" in case there are query parameters } - private String getScoreBody(String userId, String comment, Double score) { - JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", userId); - requestBody.addProperty("timestamp", (new DateTime()).toString()); - requestBody.addProperty("activityProgress", "Submitted"); - requestBody.addProperty("gradingProgress", "FullyGraded"); - requestBody.addProperty("comment", comment); - requestBody.addProperty("scoreGiven", score); - requestBody.addProperty("scoreMaximum", 100D); - return requestBody.toString(); + private String getScoreBody(String userId, String comment, Double score) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("userId", userId); + requestBody.put("timestamp", new DateTime().toString()); + requestBody.put("activityProgress", "Submitted"); + requestBody.put("gradingProgress", "FullyGraded"); + requestBody.put("comment", comment); + requestBody.put("scoreGiven", score); + requestBody.put("scoreMaximum", 100D); + return new ObjectMapper().writeValueAsString(requestBody); } /** @@ -308,21 +312,21 @@ private Optional<Exercise> getExerciseFromTargetLink(String targetLinkUrl) { } private void createOrUpdateResourceLaunch(Lti13LaunchRequest launchRequest, User user, Exercise exercise) { - Optional<LtiResourceLaunch> launchOpt = launchRepository.findByIssAndSubAndDeploymentIdAndResourceLinkId(launchRequest.getIss(), launchRequest.getSub(), - launchRequest.getDeploymentId(), launchRequest.getResourceLinkId()); + Optional<LtiResourceLaunch> launchOpt = launchRepository.findByIssAndSubAndDeploymentIdAndResourceLinkId(launchRequest.iss(), launchRequest.sub(), + launchRequest.deploymentId(), launchRequest.resourceLinkId()); LtiResourceLaunch launch = launchOpt.orElse(LtiResourceLaunch.from(launchRequest)); - Lti13AgsClaim agsClaim = launchRequest.getAgsClaim(); + Lti13AgsClaim agsClaim = launchRequest.agsClaim(); // we do support LTI 1.3 Assigment and Grading Services SCORE publish service if (agsClaim != null) { - launch.setScoreLineItemUrl(agsClaim.getLineItem()); + launch.setScoreLineItemUrl(agsClaim.lineItem()); } launch.setExercise(exercise); launch.setUser(user); - Optional<LtiPlatformConfiguration> ltiPlatformConfiguration = ltiPlatformConfigurationRepository.findByRegistrationId(launchRequest.getClientRegistrationId()); + Optional<LtiPlatformConfiguration> ltiPlatformConfiguration = ltiPlatformConfigurationRepository.findByRegistrationId(launchRequest.clientRegistrationId()); ltiPlatformConfiguration.ifPresent(launch::setLtiPlatformConfiguration); launchRepository.save(launch); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingService.java index 144210503b59..946bb4be724d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingService.java @@ -59,10 +59,10 @@ public LtiDeepLinkingService(ExerciseRepository exerciseRepository, Lti13TokenRe */ public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrationId, Long courseId, Set<Long> exerciseIds) { // Initialize DeepLinkingResponse - Lti13DeepLinkingResponse lti13DeepLinkingResponse = new Lti13DeepLinkingResponse(ltiIdToken, clientRegistrationId); + Lti13DeepLinkingResponse lti13DeepLinkingResponse = Lti13DeepLinkingResponse.from(ltiIdToken, clientRegistrationId); // Fill selected exercise link into content items ArrayList<Map<String, Object>> contentItems = this.populateContentItems(String.valueOf(courseId), exerciseIds); - lti13DeepLinkingResponse.setContentItems(contentItems); + lti13DeepLinkingResponse = lti13DeepLinkingResponse.setContentItems(contentItems); // Prepare return url with jwt and id parameters return this.buildLtiDeepLinkResponse(clientRegistrationId, lti13DeepLinkingResponse); @@ -77,13 +77,13 @@ private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLi UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(this.artemisServerUrl + "/lti/select-content"); String jwt = tokenRetriever.createDeepLinkingJWT(clientRegistrationId, lti13DeepLinkingResponse.getClaims()); - String returnUrl = lti13DeepLinkingResponse.getReturnUrl(); + String returnUrl = lti13DeepLinkingResponse.returnUrl(); // Validate properties are set to create a response - validateDeepLinkingResponseSettings(returnUrl, jwt, lti13DeepLinkingResponse.getDeploymentId()); + validateDeepLinkingResponseSettings(returnUrl, jwt, lti13DeepLinkingResponse.deploymentId()); uriComponentsBuilder.queryParam("jwt", jwt); - uriComponentsBuilder.queryParam("id", lti13DeepLinkingResponse.getDeploymentId()); + uriComponentsBuilder.queryParam("id", lti13DeepLinkingResponse.deploymentId()); uriComponentsBuilder.queryParam("deepLinkUri", UriComponent.encode(returnUrl, UriComponent.Type.QUERY_PARAM)); return uriComponentsBuilder.build().toUriString(); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java index 8649a8760f3a..b6b36a36a9b4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDynamicRegistrationService.java @@ -56,12 +56,12 @@ public void performDynamicRegistration(String openIdConfigurationUrl, String reg String clientRegistrationId = "artemis-" + UUID.randomUUID(); - if (platformConfiguration.getAuthorizationEndpoint() == null || platformConfiguration.getTokenEndpoint() == null || platformConfiguration.getJwksUri() == null - || platformConfiguration.getRegistrationEndpoint() == null) { + if (platformConfiguration.authorizationEndpoint() == null || platformConfiguration.tokenEndpoint() == null || platformConfiguration.jwksUri() == null + || platformConfiguration.registrationEndpoint() == null) { throw new BadRequestAlertException("Invalid platform configuration", "LTI", "invalidPlatformConfiguration"); } - Lti13ClientRegistration clientRegistrationResponse = postClientRegistrationToPlatform(platformConfiguration.getRegistrationEndpoint(), clientRegistrationId, + Lti13ClientRegistration clientRegistrationResponse = postClientRegistrationToPlatform(platformConfiguration.registrationEndpoint(), clientRegistrationId, registrationToken); LtiPlatformConfiguration ltiPlatformConfiguration = updateLtiPlatformConfiguration(clientRegistrationId, platformConfiguration, clientRegistrationResponse); @@ -123,10 +123,10 @@ private LtiPlatformConfiguration updateLtiPlatformConfiguration(String registrat LtiPlatformConfiguration ltiPlatformConfiguration = new LtiPlatformConfiguration(); ltiPlatformConfiguration.setRegistrationId(registrationId); ltiPlatformConfiguration.setClientId(clientRegistrationResponse.getClientId()); - ltiPlatformConfiguration.setAuthorizationUri(platformConfiguration.getAuthorizationEndpoint()); - ltiPlatformConfiguration.setJwkSetUri(platformConfiguration.getJwksUri()); - ltiPlatformConfiguration.setTokenUri(platformConfiguration.getTokenEndpoint()); - ltiPlatformConfiguration.setOriginalUrl(platformConfiguration.getIssuer()); + ltiPlatformConfiguration.setAuthorizationUri(platformConfiguration.authorizationEndpoint()); + ltiPlatformConfiguration.setJwkSetUri(platformConfiguration.jwksUri()); + ltiPlatformConfiguration.setTokenUri(platformConfiguration.tokenEndpoint()); + ltiPlatformConfiguration.setOriginalUrl(platformConfiguration.issuer()); return ltiPlatformConfiguration; } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java index c161abc4d4f4..35dcc5f2a6f2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java @@ -98,8 +98,7 @@ public void authenticateLtiUser(String email, String username, String firstName, } // 2. Case: Lookup user with the LTI email address and make sure it's not in use - final var usernameLookupByEmail = artemisAuthenticationProvider.getUsernameForEmail(email); - if (usernameLookupByEmail.isPresent()) { + if (artemisAuthenticationProvider.getUsernameForEmail(email).isPresent() || userRepository.findOneByEmailIgnoreCase(email).isPresent()) { throw new LtiEmailAlreadyInUseException(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorException.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorException.java new file mode 100644 index 000000000000..8a0845ce1564 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorException.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +public class PyrisConnectorException extends RuntimeException { + + public PyrisConnectorException(String message) { + super(message); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorService.java new file mode 100644 index 000000000000..4857e5976f72 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisConnectorService.java @@ -0,0 +1,99 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisModelDTO; +import de.tum.in.www1.artemis.service.iris.exception.IrisException; +import de.tum.in.www1.artemis.service.iris.exception.IrisForbiddenException; +import de.tum.in.www1.artemis.service.iris.exception.IrisInternalPyrisErrorException; +import de.tum.in.www1.artemis.web.rest.open.PublicPyrisStatusUpdateResource; + +/** + * This service connects to the Python implementation of Iris (called Pyris). + * Pyris is responsible for executing the pipelines using (MM)LLMs and other tools asynchronously. + * Status updates are sent to Artemis via {@link PublicPyrisStatusUpdateResource} + */ +@Service +@Profile("iris") +public class PyrisConnectorService { + + private static final Logger log = LoggerFactory.getLogger(PyrisConnectorService.class); + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + @Value("${artemis.iris.url}") + private String pyrisUrl; + + public PyrisConnectorService(@Qualifier("pyrisRestTemplate") RestTemplate restTemplate, MappingJackson2HttpMessageConverter springMvcJacksonConverter) { + this.restTemplate = restTemplate; + this.objectMapper = springMvcJacksonConverter.getObjectMapper(); + } + + /** + * Requests all available models from Pyris + * + * @return A list of available Models as IrisModelDTO + */ + public List<PyrisModelDTO> getOfferedModels() throws PyrisConnectorException { + try { + var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/models", PyrisModelDTO[].class); + if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { + throw new PyrisConnectorException("Could not fetch offered models"); + } + return Arrays.asList(response.getBody()); + } + catch (HttpStatusCodeException e) { + log.error("Failed to fetch offered models from Pyris", e); + throw new PyrisConnectorException("Could not fetch offered models"); + } + } + + void executePipeline(String feature, String variant, Object executionDTO) { + var endpoint = "/api/v1/pipelines/" + feature + "/" + variant + "/run"; + try { + restTemplate.postForEntity(pyrisUrl + endpoint, objectMapper.valueToTree(executionDTO), Void.class); + } + catch (HttpStatusCodeException e) { + throw toIrisException(e); + } + catch (RestClientException | IllegalArgumentException e) { + log.error("Failed to send request to Pyris", e); + throw new PyrisConnectorException("Could not fetch response from Iris"); + } + } + + private IrisException toIrisException(HttpStatusCodeException e) { + return switch (e.getStatusCode().value()) { + case 401, 403 -> new IrisForbiddenException(); + case 400, 500 -> new IrisInternalPyrisErrorException(tryExtractErrorMessage(e)); + default -> new IrisInternalPyrisErrorException(e.getMessage()); + }; + } + + private String tryExtractErrorMessage(HttpStatusCodeException ex) { + try { + return objectMapper.readTree(ex.getResponseBodyAsString()).required("detail").required("errorMessage").asText(); + } + catch (JsonProcessingException | IllegalArgumentException e) { + log.error("Failed to parse error message from Pyris", e); + return ""; + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisDTOService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisDTOService.java new file mode 100644 index 000000000000..9beb87b556ac --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisDTOService.java @@ -0,0 +1,168 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import jakarta.annotation.Nullable; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingSubmission; +import de.tum.in.www1.artemis.domain.Repository; +import de.tum.in.www1.artemis.domain.iris.message.IrisJsonMessageContent; +import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; +import de.tum.in.www1.artemis.domain.iris.message.IrisTextMessageContent; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.service.RepositoryService; +import de.tum.in.www1.artemis.service.connectors.GitService; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisBuildLogEntryDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisFeedbackDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisJsonMessageContentDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisMessageContentDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisMessageDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisProgrammingExerciseDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisResultDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisSubmissionDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisTextMessageContentDTO; + +@Service +@Profile("iris") +public class PyrisDTOService { + + private static final Logger log = LoggerFactory.getLogger(PyrisDTOService.class); + + private final GitService gitService; + + private final RepositoryService repositoryService; + + public PyrisDTOService(GitService gitService, RepositoryService repositoryService) { + this.gitService = gitService; + this.repositoryService = repositoryService; + } + + /** + * Helper method to convert a ProgrammingExercise to a PyrisProgrammingExerciseDTO. + * This notably includes fetching the contents of the template, solution and test repositories, if they exist. + * + * @param exercise the programming exercise to convert + * @return the converted PyrisProgrammingExerciseDTO + */ + public PyrisProgrammingExerciseDTO toPyrisDTO(ProgrammingExercise exercise) { + var templateRepositoryContents = getRepository(exercise.getTemplateParticipation()).map(repositoryService::getFilesWithContent).orElse(Map.of()); + var solutionRepositoryContents = getRepository(exercise.getSolutionParticipation()).map(repositoryService::getFilesWithContent).orElse(Map.of()); + Optional<Repository> testRepo = Optional.empty(); + try { + testRepo = Optional.ofNullable(gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true)); + } + catch (GitAPIException e) { + log.error("Could not fetch existing test repository", e); + } + var testsRepositoryContents = testRepo.map(repositoryService::getFilesWithContent).orElse(Map.of()); + + return new PyrisProgrammingExerciseDTO(exercise.getId(), exercise.getTitle(), exercise.getProgrammingLanguage(), templateRepositoryContents, solutionRepositoryContents, + testsRepositoryContents, exercise.getProblemStatement(), toInstant(exercise.getReleaseDate()), toInstant(exercise.getDueDate())); + } + + /** + * Helper method to convert a ProgrammingSubmission to a PyrisSubmissionDTO. + * This notably includes fetching the contents of the student repository, if it exists. + * + * @param submission the students submission + * @return the converted PyrisSubmissionDTO + */ + public PyrisSubmissionDTO toPyrisDTO(ProgrammingSubmission submission) { + var buildLogEntries = submission.getBuildLogEntries().stream().map(buildLogEntry -> new PyrisBuildLogEntryDTO(toInstant(buildLogEntry.getTime()), buildLogEntry.getLog())) + .toList(); + var studentRepositoryContents = getRepository((ProgrammingExerciseParticipation) submission.getParticipation()).map(repositoryService::getFilesWithContent) + .orElse(Map.of()); + return new PyrisSubmissionDTO(submission.getId(), toInstant(submission.getSubmissionDate()), studentRepositoryContents, submission.getParticipation().isPracticeMode(), + submission.isBuildFailed(), buildLogEntries, getLatestResult(submission)); + } + + /** + * Helper method to convert a list of IrisMessages to a list of PyrisMessageDTOs. + * This needs separate handling for the different types of message content. + * + * @param messages the messages with contents to convert + * @return the converted list of PyrisMessageDTOs + */ + public List<PyrisMessageDTO> toPyrisDTO(List<IrisMessage> messages) { + return messages.stream().map(message -> { + var content = message.getContent().stream().map(messageContent -> { + PyrisMessageContentDTO result = null; + if (messageContent.getClass().equals(IrisTextMessageContent.class)) { + result = new PyrisTextMessageContentDTO(messageContent.getContentAsString()); + } + else if (messageContent.getClass().equals(IrisJsonMessageContent.class)) { + result = new PyrisJsonMessageContentDTO(messageContent.getContentAsString()); + } + return result; + }).filter(Objects::nonNull).toList(); + return new PyrisMessageDTO(toInstant(message.getSentAt()), message.getSender(), content); + }).toList(); + } + + /** + * Null safe conversion of ZonedDateTime to Instant + * + * @param zonedDateTime the ZonedDateTime to convert + * @return the Instant or null if the input was null + */ + @Nullable + private Instant toInstant(@Nullable ZonedDateTime zonedDateTime) { + if (zonedDateTime == null) { + return null; + } + return zonedDateTime.toInstant(); + } + + /** + * Helper method to convert the latest result of a submission to a PyrisResultDTO + * + * @param submission the submission + * @return the PyrisResultDTO or null if the submission has no result + */ + private PyrisResultDTO getLatestResult(ProgrammingSubmission submission) { + var latestResult = submission.getLatestResult(); + if (latestResult == null) { + return null; + } + var feedbacks = latestResult.getFeedbacks().stream().map(feedback -> { + var text = feedback.getDetailText(); + if (feedback.getHasLongFeedbackText()) { + text = feedback.getLongFeedback().get().getText(); + } + var testCaseName = feedback.getTestCase() == null ? feedback.getText() : feedback.getTestCase().getTestName(); + return new PyrisFeedbackDTO(text, testCaseName, Objects.requireNonNullElse(feedback.getCredits(), 0D)); + }).toList(); + + return new PyrisResultDTO(toInstant(latestResult.getCompletionDate()), latestResult.isSuccessful(), feedbacks); + } + + /** + * Helper method to get & checkout the repository for a participation. + * This is an exception safe way to fetch the repository, as it will return an empty optional if the repository could not be fetched. + * This is useful, as the Pyris call should not fail if the repository is not available. + * + * @param participation the participation + * @return the repository or empty if it could not be fetched + */ + private Optional<Repository> getRepository(ProgrammingExerciseParticipation participation) { + try { + return Optional.ofNullable(gitService.getOrCheckoutRepository(participation.getVcsRepositoryUri(), true)); + } + catch (GitAPIException e) { + log.error("Could not fetch repository", e); + return Optional.empty(); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisHealthIndicator.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisHealthIndicator.java new file mode 100644 index 000000000000..a53c23319438 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisHealthIndicator.java @@ -0,0 +1,47 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import java.net.URI; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisHealthStatusDTO; + +@Component +@Profile("iris") +public class PyrisHealthIndicator implements HealthIndicator { + + private final RestTemplate restTemplate; + + @Value("${artemis.iris.url}") + private URI irisUrl; + + public PyrisHealthIndicator(@Qualifier("shortTimeoutPyrisRestTemplate") RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + /** + * Ping Iris at /health and check if the service is available and what its status is. + */ + @Override + public Health health() { + ConnectorHealth health; + try { + PyrisHealthStatusDTO[] status = restTemplate.getForObject(irisUrl + "/api/v1/health/", PyrisHealthStatusDTO[].class); + var isUp = status != null; + health = new ConnectorHealth(isUp); + } + catch (Exception e) { + health = new ConnectorHealth(e); + health.setUp(false); + } + + return health.asActuatorHealth(); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisJobService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisJobService.java new file mode 100644 index 000000000000..a2309c8007a4 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisJobService.java @@ -0,0 +1,139 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import java.security.SecureRandom; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.map.IMap; + +import de.tum.in.www1.artemis.service.connectors.pyris.job.PyrisJob; +import de.tum.in.www1.artemis.service.connectors.pyris.job.TutorChatJob; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; + +/** + * The PyrisJobService class is responsible for managing Pyris jobs in the Artemis system. + * It provides methods for adding, removing, and retrieving Pyris jobs. + * The class also handles generating job ID tokens and validating tokens from request headers based ont these tokens. + * It uses Hazelcast to store the jobs in a distributed map. + */ +@Service +@Profile("iris") +public class PyrisJobService { + + private final HazelcastInstance hazelcastInstance; + + private IMap<String, PyrisJob> jobMap; + + @Value("${server.url}") + private String serverUrl; + + @Value("${eureka.instance.instanceId:unknown}") + private String instanceId; + + @Value("${artemis.iris.jobs.timeout:300}") + private int jobTimeout; // in seconds + + public PyrisJobService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { + this.hazelcastInstance = hazelcastInstance; + } + + /** + * Initializes the PyrisJobService by configuring the Hazelcast map for Pyris jobs. + * Sets the time-to-live for the map entries to the specified jobTimeout value. + */ + @PostConstruct + public void init() { + var mapConfig = hazelcastInstance.getConfig().getMapConfig("pyris-job-map"); + mapConfig.setTimeToLiveSeconds(jobTimeout); + jobMap = hazelcastInstance.getMap("pyris-job-map"); + } + + public String addJob(Long courseId, Long exerciseId, Long sessionId) { + var token = generateJobIdToken(); + var job = new TutorChatJob(token, courseId, exerciseId, sessionId); + jobMap.put(token, job); + return token; + } + + /** + * Remove a job from the job map. + * + * @param token the token + */ + public void removeJob(String token) { + jobMap.remove(token); + } + + /** + * Get the job of a token. + * + * @param token the token + * @return the job + */ + public PyrisJob getJob(String token) { + return jobMap.get(token); + } + + /** + * This method is used to authenticate an incoming request from Pyris. + * 1. Reads the authentication token from the request headers. + * 2. Retrieves the PyrisJob object associated with the provided token. + * 3. Throws an AccessForbiddenException if the token is invalid or not provided. + * <p> + * The token was previously generated via {@link #addJob(Long, Long, Long)} + * + * @param request the HttpServletRequest object representing the incoming request + * @return the PyrisJob object associated with the token + * @throws AccessForbiddenException if the token is invalid or not provided + */ + public PyrisJob getAndAuthenticateJobFromHeaderElseThrow(HttpServletRequest request) { + var authHeader = request.getHeader("Authorization"); + if (!authHeader.startsWith("Bearer ")) { + throw new AccessForbiddenException("No valid token provided"); + } + var token = authHeader.substring(7); + var job = getJob(token); + if (job == null) { + throw new AccessForbiddenException("No valid token provided"); + } + return job; + } + + /** + * Generates a unique job ID token. + * The token is generated by combining the server URL, instance ID, current timestamp, and a random string. + * + * @return the generated (URL-safe) job token + */ + private String generateJobIdToken() { + // Include instance name, node id, timestamp and random string + var randomStringBuilder = new StringBuilder(); + randomStringBuilder.append(serverUrl); + randomStringBuilder.append('-'); + randomStringBuilder.append(instanceId); + randomStringBuilder.append('-'); + randomStringBuilder.append(System.currentTimeMillis()); + randomStringBuilder.append('-'); + var secureRandom = new SecureRandom(); + for (int i = 0; i < 10; i++) { + var randomChar = secureRandom.nextInt(62); + if (randomChar < 10) { + randomStringBuilder.append(randomChar); + } + else if (randomChar < 36) { + randomStringBuilder.append((char) (randomChar - 10 + 'a')); + } + else { + randomStringBuilder.append((char) (randomChar - 36 + 'A')); + } + } + return randomStringBuilder.toString().replace("https://", "").replace("http://", "").replace(":", "_").replace(".", "_").replace("/", "_"); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisPipelineService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisPipelineService.java new file mode 100644 index 000000000000..ba349d939568 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisPipelineService.java @@ -0,0 +1,74 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingSubmission; +import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisPipelineExecutionSettingsDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisCourseDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisUserDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatPipelineExecutionDTO; +import de.tum.in.www1.artemis.service.iris.websocket.IrisChatWebsocketService; + +/** + * Service responsible for executing the various Pyris pipelines in a type-safe manner. + * Uses {@link PyrisConnectorService} to execute the pipelines and {@link PyrisJobService} to manage the jobs. + */ +@Service +@Profile("iris") +public class PyrisPipelineService { + + private final PyrisConnectorService pyrisConnectorService; + + private final PyrisJobService pyrisJobService; + + private final PyrisDTOService pyrisDTOService; + + private final IrisChatWebsocketService irisChatWebsocketService; + + @Value("${server.url}") + private String artemisBaseUrl; + + public PyrisPipelineService(PyrisConnectorService pyrisConnectorService, PyrisJobService pyrisJobService, PyrisDTOService pyrisDTOService, + IrisChatWebsocketService irisChatWebsocketService) { + this.pyrisConnectorService = pyrisConnectorService; + this.pyrisJobService = pyrisJobService; + this.pyrisDTOService = pyrisDTOService; + this.irisChatWebsocketService = irisChatWebsocketService; + } + + /** + * Execute the tutor chat pipeline for the given exercise and session. + * This method will create a new job, setup the DTOs and execution settings, and then execute the pipeline. + * + * @param variant the variant of the pipeline to execute + * @param latestSubmission the latest submission for the exercise + * @param exercise the programming exercise + * @param session the chat session + */ + public void executeTutorChatPipeline(String variant, Optional<ProgrammingSubmission> latestSubmission, ProgrammingExercise exercise, IrisChatSession session) { + var jobToken = pyrisJobService.addJob(exercise.getCourseViaExerciseGroupOrCourseMember().getId(), exercise.getId(), session.getId()); + var settingsDTO = new PyrisPipelineExecutionSettingsDTO(jobToken, List.of(), artemisBaseUrl); + var preparingRequestStageInProgress = new PyrisStageDTO("Preparing request", 10, PyrisStageStateDTO.IN_PROGRESS, "Checking out repositories and loading data"); + var preparingRequestStageDone = new PyrisStageDTO("Preparing request", 10, PyrisStageStateDTO.DONE, "Checking out repositories and loading data"); + var executingPipelineStageNotStarted = new PyrisStageDTO("Executing pipeline", 30, PyrisStageStateDTO.NOT_STARTED, null); + var executingPipelineStageInProgress = new PyrisStageDTO("Executing pipeline", 30, PyrisStageStateDTO.IN_PROGRESS, null); + irisChatWebsocketService.sendStatusUpdate(session, List.of(preparingRequestStageInProgress, executingPipelineStageNotStarted)); + + var executionDTO = new PyrisTutorChatPipelineExecutionDTO(latestSubmission.map(pyrisDTOService::toPyrisDTO).orElse(null), pyrisDTOService.toPyrisDTO(exercise), + new PyrisCourseDTO(exercise.getCourseViaExerciseGroupOrCourseMember()), pyrisDTOService.toPyrisDTO(session.getMessages()), new PyrisUserDTO(session.getUser()), + settingsDTO, List.of(preparingRequestStageDone)); + + irisChatWebsocketService.sendStatusUpdate(session, List.of(preparingRequestStageDone, executingPipelineStageInProgress)); + + pyrisConnectorService.executePipeline("tutor-chat", variant, executionDTO); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisStatusUpdateService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisStatusUpdateService.java new file mode 100644 index 000000000000..581fa720bf37 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/PyrisStatusUpdateService.java @@ -0,0 +1,40 @@ +package de.tum.in.www1.artemis.service.connectors.pyris; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatStatusUpdateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.job.TutorChatJob; +import de.tum.in.www1.artemis.service.iris.session.IrisChatSessionService; + +@Service +@Profile("iris") +public class PyrisStatusUpdateService { + + private final PyrisJobService pyrisJobService; + + private final IrisChatSessionService irisChatSessionService; + + public PyrisStatusUpdateService(PyrisJobService pyrisJobService, IrisChatSessionService irisChatSessionService) { + this.pyrisJobService = pyrisJobService; + this.irisChatSessionService = irisChatSessionService; + } + + /** + * Handles the status update of a tutor chat job and forwards it to {@link IrisChatSessionService#handleStatusUpdate(TutorChatJob, PyrisTutorChatStatusUpdateDTO)} + * + * @param job the job that is updated + * @param statusUpdate the status update + */ + public void handleStatusUpdate(TutorChatJob job, PyrisTutorChatStatusUpdateDTO statusUpdate) { + irisChatSessionService.handleStatusUpdate(job, statusUpdate); + + var isDone = statusUpdate.stages().stream().map(PyrisStageDTO::state) + .allMatch(state -> state == PyrisStageStateDTO.DONE || state == PyrisStageStateDTO.ERROR || state == PyrisStageStateDTO.SKIPPED); + if (isDone) { + pyrisJobService.removeJob(job.jobId()); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisErrorResponseDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisErrorResponseDTO.java new file mode 100644 index 000000000000..11b015cb837e --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisErrorResponseDTO.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto; + +/** + * A DTO representing an error response from Pyris. + */ +public record PyrisErrorResponseDTO(String errorMessage) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisHealthStatusDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisHealthStatusDTO.java new file mode 100644 index 000000000000..bca2ee1523b2 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisHealthStatusDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto; + +public record PyrisHealthStatusDTO(String model, ModelStatus status) { + + public enum ModelStatus { + UP, DOWN, NOT_AVAILABLE + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisModelDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisModelDTO.java new file mode 100644 index 000000000000..93e73527a9e2 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisModelDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto; + +public record PyrisModelDTO(String id, String name, String description) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisPipelineExecutionSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisPipelineExecutionSettingsDTO.java new file mode 100644 index 000000000000..df93ee3a3ffa --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/PyrisPipelineExecutionSettingsDTO.java @@ -0,0 +1,13 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto; + +import java.util.List; + +/** + * DTO representing the settings required to execute a Pyris pipeline. + * + * @param authenticationToken the authentication token to use for callbacks + * @param allowedModelIdentifiers the allowed model identifiers + * @param artemisBaseUrl the base URL of the Artemis instance + */ +public record PyrisPipelineExecutionSettingsDTO(String authenticationToken, List<String> allowedModelIdentifiers, String artemisBaseUrl) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisBuildLogEntryDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisBuildLogEntryDTO.java new file mode 100644 index 000000000000..3dab20412640 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisBuildLogEntryDTO.java @@ -0,0 +1,6 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; + +public record PyrisBuildLogEntryDTO(Instant timestamp, String message) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisCourseDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisCourseDTO.java new file mode 100644 index 000000000000..3b3e32d310a7 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisCourseDTO.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import de.tum.in.www1.artemis.domain.Course; + +public record PyrisCourseDTO(long id, String name, String description) { + + public PyrisCourseDTO(Course course) { + this(course.getId(), course.getTitle(), course.getDescription()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisFeedbackDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisFeedbackDTO.java new file mode 100644 index 000000000000..eea3a556fac2 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisFeedbackDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +public record PyrisFeedbackDTO(String text, String testCaseName, double credits) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisImageMessageContentDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisImageMessageContentDTO.java new file mode 100644 index 000000000000..424d001756d2 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisImageMessageContentDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +public record PyrisImageMessageContentDTO(String imageData) implements PyrisMessageContentDTO { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisJsonMessageContentDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisJsonMessageContentDTO.java new file mode 100644 index 000000000000..449919cb35a9 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisJsonMessageContentDTO.java @@ -0,0 +1,6 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import com.fasterxml.jackson.annotation.JsonRawValue; + +public record PyrisJsonMessageContentDTO(@JsonRawValue String jsonContent) implements PyrisMessageContentDTO { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisLectureUnitDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisLectureUnitDTO.java new file mode 100644 index 000000000000..b590a830182b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisLectureUnitDTO.java @@ -0,0 +1,6 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; + +public record PyrisLectureUnitDTO(long id, long lectureId, Instant releaseDate, String name, int attachmentVersion) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageContentDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageContentDTO.java new file mode 100644 index 000000000000..718658f85c54 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageContentDTO.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ @JsonSubTypes.Type(value = PyrisTextMessageContentDTO.class, name = "text"), @JsonSubTypes.Type(value = PyrisJsonMessageContentDTO.class, name = "json"), + @JsonSubTypes.Type(value = PyrisImageMessageContentDTO.class, name = "image"), }) +public interface PyrisMessageContentDTO { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageDTO.java new file mode 100644 index 000000000000..6f858a8859ae --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisMessageDTO.java @@ -0,0 +1,9 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; +import java.util.List; + +import de.tum.in.www1.artemis.domain.iris.message.IrisMessageSender; + +public record PyrisMessageDTO(Instant sentAt, IrisMessageSender sender, List<PyrisMessageContentDTO> contents) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisProgrammingExerciseDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisProgrammingExerciseDTO.java new file mode 100644 index 000000000000..195196452431 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisProgrammingExerciseDTO.java @@ -0,0 +1,11 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; +import java.util.Map; + +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; + +public record PyrisProgrammingExerciseDTO(long id, String name, ProgrammingLanguage programmingLanguage, Map<String, String> templateRepository, + Map<String, String> solutionRepository, Map<String, String> testRepository, String problemStatement, Instant startDate, Instant endDate) { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisResultDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisResultDTO.java new file mode 100644 index 000000000000..fb55656fb0b7 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisResultDTO.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; +import java.util.List; + +public record PyrisResultDTO(Instant completionDate, boolean successful, List<PyrisFeedbackDTO> feedbacks) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisSubmissionDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisSubmissionDTO.java new file mode 100644 index 000000000000..459cf340db7b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisSubmissionDTO.java @@ -0,0 +1,9 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +public record PyrisSubmissionDTO(long id, Instant date, Map<String, String> repository, boolean isPractice, boolean buildFailed, List<PyrisBuildLogEntryDTO> buildLogEntries, + PyrisResultDTO latestResult) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisTextMessageContentDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisTextMessageContentDTO.java new file mode 100644 index 000000000000..e24718fcfddc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisTextMessageContentDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +public record PyrisTextMessageContentDTO(String textContent) implements PyrisMessageContentDTO { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisUserDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisUserDTO.java new file mode 100644 index 000000000000..16e2fc44d87e --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/data/PyrisUserDTO.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.data; + +import de.tum.in.www1.artemis.domain.User; + +public record PyrisUserDTO(long id, String firstName, String lastName) { + + public PyrisUserDTO(User user) { + this(user.getId(), user.getFirstName(), user.getLastName()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageDTO.java new file mode 100644 index 000000000000..f3dc011063ab --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.status; + +public record PyrisStageDTO(String name, int weight, PyrisStageStateDTO state, String message) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageStateDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageStateDTO.java new file mode 100644 index 000000000000..6af213936c37 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/status/PyrisStageStateDTO.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.status; + +public enum PyrisStageStateDTO { + NOT_STARTED, IN_PROGRESS, DONE, SKIPPED, ERROR +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatPipelineExecutionDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatPipelineExecutionDTO.java new file mode 100644 index 000000000000..ed3dfc3b4892 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatPipelineExecutionDTO.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat; + +import java.util.List; + +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisPipelineExecutionSettingsDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisCourseDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisMessageDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisProgrammingExerciseDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisSubmissionDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.data.PyrisUserDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; + +public record PyrisTutorChatPipelineExecutionDTO(PyrisSubmissionDTO submission, PyrisProgrammingExerciseDTO exercise, PyrisCourseDTO course, List<PyrisMessageDTO> chatHistory, + PyrisUserDTO user, PyrisPipelineExecutionSettingsDTO settings, List<PyrisStageDTO> initialStages) { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatStatusUpdateDTO.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatStatusUpdateDTO.java new file mode 100644 index 000000000000..b01e4405c0c3 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/dto/tutorChat/PyrisTutorChatStatusUpdateDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat; + +import java.util.List; + +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; + +public record PyrisTutorChatStatusUpdateDTO(String result, List<PyrisStageDTO> stages) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/PyrisJob.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/PyrisJob.java new file mode 100644 index 000000000000..fe874989ad10 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/PyrisJob.java @@ -0,0 +1,27 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.job; + +import java.io.Serializable; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; + +/** + * This interface represents a single job that is executed by the Pyris system. + * This is used to reference the details of a job when Pyris sends a status update. + * As it is stored within Hazelcast, it must be serializable. + */ +public interface PyrisJob extends Serializable { + + long serialVersionUID = 1L; + + boolean canAccess(Course course); + + boolean canAccess(Exercise exercise); + + default boolean canAccess(LectureUnit lectureUnit) { + return this.canAccess(lectureUnit.getLecture().getCourse()); + } + + String jobId(); +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/TutorChatJob.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/TutorChatJob.java new file mode 100644 index 000000000000..3c941eb064fc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/pyris/job/TutorChatJob.java @@ -0,0 +1,21 @@ +package de.tum.in.www1.artemis.service.connectors.pyris.job; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; + +/** + * An implementation of a PyrisJob for tutor chat messages. + * This job is used to reference the details of a tutor chat session when Pyris sends a status update. + */ +public record TutorChatJob(String jobId, long courseId, long exerciseId, long sessionId) implements PyrisJob { + + @Override + public boolean canAccess(Course course) { + return course.getId().equals(courseId); + } + + @Override + public boolean canAccess(Exercise exercise) { + return exercise.getId().equals(exerciseId); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingFeedbackDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingFeedbackDTO.java index 0ff792a84e20..9bb157c1449b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingFeedbackDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/athena/ProgrammingFeedbackDTO.java @@ -11,12 +11,12 @@ public record ProgrammingFeedbackDTO(long id, long exerciseId, long submissionId String filePath, Integer lineStart, Integer lineEnd) implements FeedbackDTO { /** - * Creates a TextFeedbackDTO from a Feedback object + * Creates a ProgrammingFeedbackDTO from a Feedback object * * @param exerciseId the id of the exercise the feedback is given for * @param submissionId the id of the submission the feedback is given for * @param feedback the feedback object - * @return the TextFeedbackDTO + * @return the ProgrammingFeedbackDTO */ public static ProgrammingFeedbackDTO of(long exerciseId, long submissionId, @NotNull Feedback feedback) { // Referenced feedback has a reference looking like this: "file:src/main/java/SomeFile.java_line:42" diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java deleted file mode 100644 index f476a3ff9b67..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.tum.in.www1.artemis.service.dto.iris; - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.in.www1.artemis.domain.iris.IrisTemplate; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisCombinedChatSubSettingsDTO implements IrisCombinedSubSettingsInterface { - - private boolean enabled; - - private Integer rateLimit; - - private Integer rateLimitTimeframeHours; - - private Set<String> allowedModels; - - private String preferredModel; - - private IrisTemplate template; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Integer getRateLimit() { - return rateLimit; - } - - public void setRateLimit(Integer rateLimit) { - this.rateLimit = rateLimit; - } - - public Integer getRateLimitTimeframeHours() { - return rateLimitTimeframeHours; - } - - public void setRateLimitTimeframeHours(Integer rateLimitTimeframeHours) { - this.rateLimitTimeframeHours = rateLimitTimeframeHours; - } - - public Set<String> getAllowedModels() { - return allowedModels; - } - - public void setAllowedModels(Set<String> allowedModels) { - this.allowedModels = allowedModels; - } - - public String getPreferredModel() { - return preferredModel; - } - - public void setPreferredModel(String preferredModel) { - this.preferredModel = preferredModel; - } - - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(IrisTemplate template) { - this.template = template; - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCompetencyGenerationSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCompetencyGenerationSubSettingsDTO.java deleted file mode 100644 index 81d74d8943a1..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCompetencyGenerationSubSettingsDTO.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.tum.in.www1.artemis.service.dto.iris; - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.in.www1.artemis.domain.iris.IrisTemplate; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisCombinedCompetencyGenerationSubSettingsDTO implements IrisCombinedSubSettingsInterface { - - private boolean enabled; - - private Set<String> allowedModels; - - private String preferredModel; - - private IrisTemplate template; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Set<String> getAllowedModels() { - return allowedModels; - } - - public void setAllowedModels(Set<String> allowedModels) { - this.allowedModels = allowedModels; - } - - public String getPreferredModel() { - return preferredModel; - } - - public void setPreferredModel(String preferredModel) { - this.preferredModel = preferredModel; - } - - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(IrisTemplate template) { - this.template = template; - } - -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java deleted file mode 100644 index 4cc39b069e28..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.tum.in.www1.artemis.service.dto.iris; - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.in.www1.artemis.domain.iris.IrisTemplate; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisCombinedHestiaSubSettingsDTO implements IrisCombinedSubSettingsInterface { - - private boolean enabled; - - private Set<String> allowedModels; - - private String preferredModel; - - private IrisTemplate template; - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Set<String> getAllowedModels() { - return allowedModels; - } - - public void setAllowedModels(Set<String> allowedModels) { - this.allowedModels = allowedModels; - } - - public String getPreferredModel() { - return preferredModel; - } - - public void setPreferredModel(String preferredModel) { - this.preferredModel = preferredModel; - } - - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(IrisTemplate template) { - this.template = template; - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSubSettingsInterface.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSubSettingsInterface.java deleted file mode 100644 index 913e65748987..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSubSettingsInterface.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.tum.in.www1.artemis.service.dto.iris; - -import java.util.Set; - -public interface IrisCombinedSubSettingsInterface { - - boolean isEnabled(); - - void setEnabled(boolean enabled); - - Set<String> getAllowedModels(); - - void setAllowedModels(Set<String> allowedModels); - - String getPreferredModel(); - - void setPreferredModel(String preferredModel); -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java index a0e23aabf53b..0b8caa4f6350 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java @@ -36,11 +36,11 @@ import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.in.www1.artemis.service.FileUploadExerciseImportService; import de.tum.in.www1.artemis.service.ModelingExerciseImportService; -import de.tum.in.www1.artemis.service.QuizExerciseImportService; import de.tum.in.www1.artemis.service.TextExerciseImportService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseImportService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseImportService; import de.tum.in.www1.artemis.web.rest.errors.ExamConfigurationException; @Profile(PROFILE_CORE) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizService.java index b7b6e39c85c8..1c22317a4afe 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamQuizService.java @@ -31,8 +31,8 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.SubmittedAnswerRepository; -import de.tum.in.www1.artemis.service.QuizStatisticService; import de.tum.in.www1.artemis.service.ResultService; +import de.tum.in.www1.artemis.service.quiz.QuizStatisticService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index 6a23bc2cc2f8..c52509df2c8d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -91,13 +91,13 @@ import de.tum.in.www1.artemis.service.BonusService; import de.tum.in.www1.artemis.service.CourseScoreCalculationService; import de.tum.in.www1.artemis.service.ExerciseDeletionService; -import de.tum.in.www1.artemis.service.QuizPoolService; import de.tum.in.www1.artemis.service.TutorLeaderboardService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.export.CourseExamExportService; import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.plagiarism.PlagiarismCaseService.PlagiarismMapping; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.BonusExampleDTO; import de.tum.in.www1.artemis.web.rest.dto.BonusResultDTO; diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 9d27fed05254..38fdf510749d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -67,12 +67,12 @@ import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.service.ParticipationService; -import de.tum.in.www1.artemis.service.QuizPoolService; import de.tum.in.www1.artemis.service.SubmissionService; import de.tum.in.www1.artemis.service.SubmissionVersionService; import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService; import de.tum.in.www1.artemis.service.programming.ProgrammingTriggerService; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.service.util.ExamExerciseStartPreparationStatus; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java index cacedd53d18e..ed58a36c9ed7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/CourseExamExportService.java @@ -197,7 +197,7 @@ public Optional<Path> exportExam(Exam exam, Path outputDir, List<String> exportE // Export exam exercises notifyUserAboutExerciseExportState(notificationTopic, CourseExamExportState.RUNNING, List.of("Preparing to export exam exercises..."), null); - var exercises = examRepository.findAllExercisesByExamId(exam.getId()); + var exercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()); List<Path> exportedExercises = exportExercises(notificationTopic, exercises, tempExamsDir, 0, exercises.size(), exportErrors, reportData); // Write report and error file @@ -235,7 +235,8 @@ private List<Path> exportCourseAndExamExercises(String notificationTopic, Course List<Exam> courseExams = examRepository.findByCourseId(course.getId()); // Calculate the amount of exercises for all exams - var examExercises = courseExams.stream().map(exam -> examRepository.findAllExercisesByExamId(exam.getId())).flatMap(Collection::stream).collect(Collectors.toSet()); + var examExercises = courseExams.stream().map(exam -> examRepository.findAllExercisesWithDetailsByExamId(exam.getId())).flatMap(Collection::stream) + .collect(Collectors.toSet()); int totalExercises = courseExercises.size() + examExercises.size(); int progress = 0; @@ -318,7 +319,7 @@ private List<Path> exportExams(String notificationTopic, List<Exam> exams, Strin // Export each exam. We first fetch its exercises and then export them. var exportedExams = new ArrayList<Path>(); for (var exam : exams) { - var examExercises = examRepository.findAllExercisesByExamId(exam.getId()); + var examExercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()); var exportedExam = exportExam(notificationTopic, exam, examExercises, examsDir.toString(), currentProgress, totalExerciseCount, exportErrors, reportData); exportedExams.addAll(exportedExam); currentProgress += examExercises.size(); @@ -410,30 +411,23 @@ private List<Path> exportExercises(String notificationTopic, Set<Exercise> exerc submissionsExportOptions.setExportAllParticipants(true); try { // Export programming exercise - if (exercise instanceof ProgrammingExercise programmingExercise) { - // Download the repositories' template, solution, tests and students' repositories - programmingExerciseExportService.exportProgrammingExerciseForArchival(programmingExercise, exportErrors, Optional.of(exerciseExportDir), reportData) - .ifPresent(exportedExercises::add); - } - // Export the other exercises types - else if (exercise instanceof FileUploadExercise) { - exportedExercises.add(fileUploadExerciseWithSubmissionsExportService.exportFileUploadExerciseWithSubmissions(exercise, submissionsExportOptions, - exerciseExportDir, exportErrors, reportData)); - } - else if (exercise instanceof TextExercise) { - exportedExercises.add(textExerciseWithSubmissionsExportService.exportTextExerciseWithSubmissions(exercise, submissionsExportOptions, exerciseExportDir, - exportErrors, reportData)); - } - else if (exercise instanceof ModelingExercise) { - exportedExercises.add(modelingExerciseWithSubmissionsExportService.exportModelingExerciseWithSubmissions(exercise, submissionsExportOptions, exerciseExportDir, - exportErrors, reportData)); - } - else if (exercise instanceof QuizExercise quizExercise) { - exportedExercises.add(quizExerciseWithSubmissionsExportService.exportExerciseWithSubmissions(quizExercise, exerciseExportDir, exportErrors, reportData)); - - } - else { - // Exercise is not supported so skip + switch (exercise) { + case ProgrammingExercise programmingExercise -> + // Download the repositories' template, solution, tests and students' repositories + programmingExerciseExportService.exportProgrammingExerciseForArchival(programmingExercise, exportErrors, Optional.of(exerciseExportDir), reportData) + .ifPresent(exportedExercises::add); + case FileUploadExercise fileUploadExercise -> exportedExercises.add(fileUploadExerciseWithSubmissionsExportService + .exportFileUploadExerciseWithSubmissions(fileUploadExercise, submissionsExportOptions, exerciseExportDir, exportErrors, reportData)); + case TextExercise textExercise -> exportedExercises.add(textExerciseWithSubmissionsExportService.exportTextExerciseWithSubmissions(textExercise, + submissionsExportOptions, exerciseExportDir, exportErrors, reportData)); + case ModelingExercise modelingExercise -> exportedExercises.add(modelingExerciseWithSubmissionsExportService + .exportModelingExerciseWithSubmissions(modelingExercise, submissionsExportOptions, exerciseExportDir, exportErrors, reportData)); + case QuizExercise quizExercise -> + exportedExercises.add(quizExerciseWithSubmissionsExportService.exportExerciseWithSubmissions(quizExercise, exerciseExportDir, exportErrors, reportData)); + default -> { + logMessageAndAppendToList("Failed to export exercise '" + exercise.getTitle() + "' (id: " + exercise.getId() + "): Exercise type not supported for export", + exportErrors, null); + } } } catch (Exception e) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java index 2339b6d13a5b..6e2f8049118d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ExerciseWithSubmissionsExportService.java @@ -222,6 +222,7 @@ private void exportExerciseDetails(Exercise exercise, Path exportDir, List<Path> exercise.getCourseViaExerciseGroupOrCourseMember().setExams(null); // do not include related entities ids Optional.ofNullable(exercise.getPlagiarismDetectionConfig()).ifPresent(it -> it.setId(null)); + Optional.ofNullable(exercise.getTeamAssignmentConfig()).ifPresent(it -> it.setId(null)); pathsToBeZipped.add(fileService.writeObjectToJsonFile(exercise, this.objectMapper, exerciseDetailsExportPath)); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadExerciseWithSubmissionsExportService.java index 49ae27d8450d..b3023e22cc72 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/FileUploadExerciseWithSubmissionsExportService.java @@ -9,7 +9,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.FileUploadExercise; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry; import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO; @@ -36,7 +36,7 @@ public FileUploadExerciseWithSubmissionsExportService(FileService fileService, F * @param reportEntries report entries that are added during the export * @return the path to the exported file upload exercise */ - public Path exportFileUploadExerciseWithSubmissions(Exercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, + public Path exportFileUploadExerciseWithSubmissions(FileUploadExercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, List<ArchivalReportEntry> reportEntries) { return exportExerciseWithSubmissions(exercise, optionsDTO, exportDir, exportErrors, reportEntries); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ModelingExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ModelingExerciseWithSubmissionsExportService.java index 1c8407a5bdc5..87a84991c9e7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ModelingExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ModelingExerciseWithSubmissionsExportService.java @@ -9,7 +9,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry; import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO; @@ -37,7 +37,7 @@ public ModelingExerciseWithSubmissionsExportService(FileService fileService, Mod * @param reportEntries report entries that are added during the export * @return the path to the exported modeling exercise */ - public Path exportModelingExerciseWithSubmissions(Exercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, + public Path exportModelingExerciseWithSubmissions(ModelingExercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, List<ArchivalReportEntry> reportEntries) { return exportExerciseWithSubmissions(exercise, optionsDTO, exportDir, exportErrors, reportEntries); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java index fb541b123a88..9fc318f26f3a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/ProgrammingExerciseExportService.java @@ -439,6 +439,36 @@ public Optional<File> exportStudentRequestedRepository(long exerciseId, boolean } } + /** + * Exports the repository belonging to a student's programming exercise participation. + * + * @param exerciseId The ID of the programming exercise. + * @param participation The participation for which to export the repository. + * @param exportErrors A list in which to store errors that occur during the export. + * @return The zipped repository if the export was successful, otherwise an empty optional. + */ + public Optional<File> exportStudentRepository(long exerciseId, ProgrammingExerciseStudentParticipation participation, List<String> exportErrors) { + var exerciseOrEmpty = loadExerciseForRepoExport(exerciseId, exportErrors); + if (exerciseOrEmpty.isEmpty()) { + return Optional.empty(); + } + var programmingExercise = exerciseOrEmpty.get(); + var blankExportOptions = new RepositoryExportOptionsDTO(); + Path outputDirectory = fileService.getTemporaryUniquePathWithoutPathCreation(repoDownloadClonePath, 5); + try { + Path zipFile = createZipForRepositoryWithParticipation(programmingExercise, participation, blankExportOptions, outputDirectory, outputDirectory); + if (zipFile != null) { + return Optional.of(zipFile.toFile()); + } + } + catch (IOException e) { + String error = String.format("Failed to export the student repository of programming exercise %d and participation %d", exerciseId, participation.getId()); + log.error(error); + exportErrors.add(error); + } + return Optional.empty(); + } + private Optional<ProgrammingExercise> loadExerciseForRepoExport(long exerciseId, List<String> exportErrors) { var exerciseOrEmpty = programmingExerciseRepository.findWithTemplateAndSolutionParticipationAndAuxiliaryRepositoriesById(exerciseId); if (exerciseOrEmpty.isEmpty()) { @@ -708,6 +738,9 @@ public Path createZipForRepositoryWithParticipation(final ProgrammingExercise pr log.debug("Anonymizing commits for participation {}", participation); gitService.anonymizeStudentCommits(repository, programmingExercise); } + else { + gitService.removeRemotesFromRepository(repository); + } if (repositoryExportOptions.isNormalizeCodeStyle()) { try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/export/TextExerciseWithSubmissionsExportService.java b/src/main/java/de/tum/in/www1/artemis/service/export/TextExerciseWithSubmissionsExportService.java index ffc2e39a43cd..6c2a991996cf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/export/TextExerciseWithSubmissionsExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/export/TextExerciseWithSubmissionsExportService.java @@ -9,7 +9,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.archival.ArchivalReportEntry; import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO; @@ -37,7 +37,7 @@ public TextExerciseWithSubmissionsExportService(FileService fileService, TextSub * @param reportEntries report entries that are added during the export * @return the path to the exported text exercise */ - public Path exportTextExerciseWithSubmissions(Exercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, + public Path exportTextExerciseWithSubmissions(TextExercise exercise, SubmissionExportOptionsDTO optionsDTO, Path exportDir, List<String> exportErrors, List<ArchivalReportEntry> reportEntries) { return exportExerciseWithSubmissions(exercise, optionsDTO, exportDir, exportErrors, reportEntries); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java b/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java index 9dc8c5a89e55..b5f42b1f9d52 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/feature/FeatureToggleService.java @@ -7,6 +7,7 @@ import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -30,7 +31,7 @@ public class FeatureToggleService { private Map<Feature, Boolean> features; - public FeatureToggleService(WebsocketMessagingService websocketMessagingService, HazelcastInstance hazelcastInstance) { + public FeatureToggleService(WebsocketMessagingService websocketMessagingService, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { this.websocketMessagingService = websocketMessagingService; this.hazelcastInstance = hazelcastInstance; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedChatSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedChatSubSettingsDTO.java new file mode 100644 index 000000000000..2bef37bda870 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedChatSubSettingsDTO.java @@ -0,0 +1,15 @@ +package de.tum.in.www1.artemis.service.iris.dto; + +import java.util.Set; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set<String> allowedModels, + @Nullable String preferredModel, @Nullable IrisTemplate template) implements IrisCombinedSubSettingsInterface { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java new file mode 100644 index 000000000000..7e2e399e530e --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java @@ -0,0 +1,15 @@ +package de.tum.in.www1.artemis.service.iris.dto; + +import java.util.Set; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisCombinedCompetencyGenerationSubSettingsDTO(boolean enabled, @Nullable Set<String> allowedModels, @Nullable String preferredModel, @Nullable IrisTemplate template) + implements IrisCombinedSubSettingsInterface { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedHestiaSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedHestiaSubSettingsDTO.java new file mode 100644 index 000000000000..47861b16ebbb --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedHestiaSubSettingsDTO.java @@ -0,0 +1,14 @@ +package de.tum.in.www1.artemis.service.iris.dto; + +import java.util.Set; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisCombinedHestiaSubSettingsDTO(boolean enabled, @Nullable Set<String> allowedModels, @Nullable String preferredModel, @Nullable IrisTemplate template) + implements IrisCombinedSubSettingsInterface { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSettingsDTO.java similarity index 87% rename from src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java rename to src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSettingsDTO.java index 34a00c74b0ac..e38047c34a5c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSettingsDTO.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service.dto.iris; +package de.tum.in.www1.artemis.service.iris.dto; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSubSettingsInterface.java b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSubSettingsInterface.java new file mode 100644 index 000000000000..79d23120b4fc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/dto/IrisCombinedSubSettingsInterface.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.artemis.service.iris.dto; + +import java.util.Set; + +import jakarta.annotation.Nullable; + +public interface IrisCombinedSubSettingsInterface { + + boolean enabled(); + + @Nullable + Set<String> allowedModels(); + + @Nullable + String preferredModel(); +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisInvalidTemplateException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisInvalidTemplateException.java deleted file mode 100644 index 007a92cab486..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisInvalidTemplateException.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.tum.in.www1.artemis.service.iris.exception; - -import java.util.Map; - -public class IrisInvalidTemplateException extends IrisException { - - public IrisInvalidTemplateException(String pyrisErrorMessage) { - super("artemisApp.exerciseChatbot.errors.invalidTemplate", Map.of("pyrisErrorMessage", pyrisErrorMessage)); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisModelNotAvailableException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisModelNotAvailableException.java deleted file mode 100644 index 78554ac2f1af..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisModelNotAvailableException.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.tum.in.www1.artemis.service.iris.exception; - -import java.util.Map; - -public class IrisModelNotAvailableException extends IrisException { - - public IrisModelNotAvailableException(String model, String pyrisErrorMessage) { - super("artemisApp.exerciseChatbot.errors.noModelAvailable", Map.of("model", model, "pyrisErrorMessage", pyrisErrorMessage)); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisNoResponseException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisNoResponseException.java deleted file mode 100644 index f477dd04af2d..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisNoResponseException.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.tum.in.www1.artemis.service.iris.exception; - -import java.util.Map; - -public class IrisNoResponseException extends IrisException { - - public IrisNoResponseException() { - super("artemisApp.exerciseChatbot.errors.noResponse", Map.of()); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisParseResponseException.java b/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisParseResponseException.java deleted file mode 100644 index c4fda6923159..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/exception/IrisParseResponseException.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.tum.in.www1.artemis.service.iris.exception; - -import java.util.Map; - -public class IrisParseResponseException extends IrisException { - - public IrisParseResponseException(String message) { - super("artemisApp.exerciseChatbot.errors.parseResponse", Map.of("cause", message)); - } - - public IrisParseResponseException(Throwable cause) { - this(cause.getMessage()); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatBasedFeatureInterface.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatBasedFeatureInterface.java index 7d3248002abf..db345e973fdb 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatBasedFeatureInterface.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatBasedFeatureInterface.java @@ -17,5 +17,5 @@ public interface IrisChatBasedFeatureInterface<S extends IrisSession> extends Ir * * @param irisSession The session to get a message for */ - void requestAndHandleResponse(IrisSession irisSession); + void requestAndHandleResponse(S irisSession); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java index 4f2257a8f201..86242f00c1e8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java @@ -1,53 +1,37 @@ package de.tum.in.www1.artemis.service.iris.session; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import jakarta.ws.rs.BadRequestException; - -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.treewalk.FileTreeIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import de.tum.in.www1.artemis.domain.BuildLogEntry; -import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; -import de.tum.in.www1.artemis.domain.Repository; import de.tum.in.www1.artemis.domain.Submission; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; import de.tum.in.www1.artemis.domain.iris.message.IrisMessageSender; import de.tum.in.www1.artemis.domain.iris.message.IrisTextMessageContent; import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; -import de.tum.in.www1.artemis.domain.iris.session.IrisSession; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; -import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.RepositoryService; -import de.tum.in.www1.artemis.service.connectors.GitService; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisPipelineService; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatStatusUpdateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.job.TutorChatJob; import de.tum.in.www1.artemis.service.iris.IrisMessageService; import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; -import de.tum.in.www1.artemis.service.iris.exception.IrisNoResponseException; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.service.iris.websocket.IrisChatWebsocketService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; -import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** * Service to handle the chat subsystem of Iris. @@ -58,8 +42,6 @@ public class IrisChatSessionService implements IrisChatBasedFeatureInterface<Iri private static final Logger log = LoggerFactory.getLogger(IrisChatSessionService.class); - private final IrisConnectorService irisConnectorService; - private final IrisMessageService irisMessageService; private final IrisSettingsService irisSettingsService; @@ -70,35 +52,30 @@ public class IrisChatSessionService implements IrisChatBasedFeatureInterface<Iri private final IrisSessionRepository irisSessionRepository; - private final GitService gitService; - - private final RepositoryService repositoryService; - - private final TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository; - private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository; private final ProgrammingSubmissionRepository programmingSubmissionRepository; private final IrisRateLimitService rateLimitService; - public IrisChatSessionService(IrisConnectorService irisConnectorService, IrisMessageService irisMessageService, IrisSettingsService irisSettingsService, - IrisChatWebsocketService irisChatWebsocketService, AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, GitService gitService, - RepositoryService repositoryService, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, + private final PyrisPipelineService pyrisPipelineService; + + private final ProgrammingExerciseRepository programmingExerciseRepository; + + public IrisChatSessionService(IrisMessageService irisMessageService, IrisSettingsService irisSettingsService, IrisChatWebsocketService irisChatWebsocketService, + AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, - IrisRateLimitService rateLimitService) { - this.irisConnectorService = irisConnectorService; + IrisRateLimitService rateLimitService, PyrisPipelineService pyrisPipelineService, ProgrammingExerciseRepository programmingExerciseRepository) { this.irisMessageService = irisMessageService; this.irisSettingsService = irisSettingsService; this.irisChatWebsocketService = irisChatWebsocketService; this.authCheckService = authCheckService; this.irisSessionRepository = irisSessionRepository; - this.gitService = gitService; - this.repositoryService = repositoryService; - this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository; this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; this.programmingSubmissionRepository = programmingSubmissionRepository; this.rateLimitService = rateLimitService; + this.pyrisPipelineService = pyrisPipelineService; + this.programmingExerciseRepository = programmingExerciseRepository; } /** @@ -143,7 +120,7 @@ public void checkIsFeatureActivatedFor(IrisChatSession session) { @Override public void sendOverWebsocket(IrisMessage message) { - irisChatWebsocketService.sendMessage(message); + irisChatWebsocketService.sendMessage(message, null); } @Override @@ -151,20 +128,6 @@ public void checkRateLimit(User user) { rateLimitService.checkRateLimitElseThrow(user); } - // @formatter:off - record IrisChatRequestDTO( - ProgrammingExercise exercise, - Course course, - Optional<ProgrammingSubmission> latestSubmission, - boolean buildFailed, - List<BuildLogEntry> buildLog, - IrisChatSession session, - String gitDiff, - Map<String, String> templateRepository, - Map<String, String> studentRepository - ) {} - // @formatter:on - /** * Sends all messages of the session to an LLM and handles the response by saving the message * and sending it to the student via the Websocket. @@ -172,52 +135,17 @@ record IrisChatRequestDTO( * @param session The chat session to send to the LLM */ @Override - public void requestAndHandleResponse(IrisSession session) { - var fullSession = irisSessionRepository.findByIdWithMessagesAndContents(session.getId()); - if (!(fullSession instanceof IrisChatSession chatSession)) { - throw new BadRequestException("Trying to get Iris response for session " + session.getId() + " without exercise"); - } + public void requestAndHandleResponse(IrisChatSession session) { + var chatSession = (IrisChatSession) irisSessionRepository.findByIdWithMessagesAndContents(session.getId()); if (chatSession.getExercise().isExamExercise()) { throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); } + var exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(chatSession.getExercise().getId()); + var latestSubmission = getLatestSubmissionIfExists(exercise, chatSession.getUser()); - var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(chatSession.getExercise(), false); - String template = irisSettings.irisChatSettings().getTemplate().getContent(); - String preferredModel = irisSettings.irisChatSettings().getPreferredModel(); - var dto = createRequestArgumentsDTO(chatSession); - irisConnectorService.sendRequestV2(template, preferredModel, dto).handleAsync((response, throwable) -> { - if (throwable != null) { - log.error("Error while getting response from Iris model", throwable); - irisChatWebsocketService.sendException(chatSession, throwable.getCause()); - } - else if (response != null && response.content().hasNonNull("response")) { - String responseText = response.content().get("response").asText(); - IrisMessage responseMessage = new IrisMessage(); - responseMessage.addContent(new IrisTextMessageContent(responseText)); - var irisMessageSaved = irisMessageService.saveMessage(responseMessage, chatSession, IrisMessageSender.LLM); - irisChatWebsocketService.sendMessage(irisMessageSaved); - } - else { - log.error("No response from Iris model"); - irisChatWebsocketService.sendException(chatSession, new IrisNoResponseException()); - } - return null; - }); - } - - private IrisChatRequestDTO createRequestArgumentsDTO(IrisChatSession session) { - final ProgrammingExercise exercise = session.getExercise(); - final Course course = exercise.getCourseViaExerciseGroupOrCourseMember(); - final Optional<ProgrammingSubmission> latestSubmission = getLatestSubmissionIfExists(exercise, session.getUser()); - final boolean buildFailed = latestSubmission.map(ProgrammingSubmission::isBuildFailed).orElse(false); - final List<BuildLogEntry> buildLog = latestSubmission.map(ProgrammingSubmission::getBuildLogEntries).orElse(List.of()); - final Repository templateRepository = templateRepository(exercise); - final Optional<Repository> studentRepository = studentRepository(latestSubmission); - final Map<String, String> templateRepositoryContents = repositoryService.getFilesWithContent(templateRepository); - final Map<String, String> studentRepositoryContents = studentRepository.map(repositoryService::getFilesWithContent).orElse(Map.of()); - final String gitDiff = studentRepository.map(repo -> getGitDiff(templateRepository, repo)).orElse(""); - - return new IrisChatRequestDTO(exercise, course, latestSubmission, buildFailed, buildLog, session, gitDiff, templateRepositoryContents, studentRepositoryContents); + // TODO: Use settings to determine the variant + // var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(chatSession.getExercise(), false); + pyrisPipelineService.executeTutorChatPipeline("default", latestSubmission, exercise, chatSession); } private Optional<ProgrammingSubmission> getLatestSubmissionIfExists(ProgrammingExercise exercise, User user) { @@ -226,48 +154,25 @@ private Optional<ProgrammingSubmission> getLatestSubmissionIfExists(ProgrammingE return Optional.empty(); } return participations.getLast().getSubmissions().stream().max(Submission::compareTo) - .flatMap(sub -> programmingSubmissionRepository.findWithEagerBuildLogEntriesById(sub.getId())); - } - - private Repository templateRepository(ProgrammingExercise exercise) { - return templateProgrammingExerciseParticipationRepository.findByProgrammingExerciseId(exercise.getId()).map(participation -> { - try { - return gitService.getOrCheckoutRepository(participation.getVcsRepositoryUri(), true); - } - catch (GitAPIException e) { - return null; - } - }).orElseThrow(() -> new InternalServerErrorException("Iris cannot function without template participation")); - } - - private Optional<Repository> studentRepository(Optional<ProgrammingSubmission> latestSubmission) { - return latestSubmission.map(sub -> (ProgrammingExerciseParticipation) sub.getParticipation()).map(participation -> { - try { - return gitService.getOrCheckoutRepository(participation.getVcsRepositoryUri(), true); - } - catch (GitAPIException e) { - log.error("Could not fetch existing student participation repository", e); - return null; - } - }); + .flatMap(sub -> programmingSubmissionRepository.findWithEagerResultsAndFeedbacksAndBuildLogsById(sub.getId())); } - private String getGitDiff(Repository from, Repository to) { - var oldTreeParser = new FileTreeIterator(from); - var newTreeParser = new FileTreeIterator(to); - - gitService.resetToOriginHead(from); - gitService.pullIgnoreConflicts(from); - gitService.resetToOriginHead(to); - gitService.pullIgnoreConflicts(to); - - try (ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream(); Git git = Git.wrap(from)) { - git.diff().setOldTree(oldTreeParser).setNewTree(newTreeParser).setOutputStream(diffOutputStream).call(); - return diffOutputStream.toString(); + /** + * Handles the status update of a TutorChatJob by sending the result to the student via the Websocket. + * + * @param job The job that was executed + * @param statusUpdate The status update of the job + */ + public void handleStatusUpdate(TutorChatJob job, PyrisTutorChatStatusUpdateDTO statusUpdate) { + var session = (IrisChatSession) irisSessionRepository.findByIdWithMessagesAndContents(job.sessionId()); + if (statusUpdate.result() != null) { + var message = new IrisMessage(); + message.addContent(new IrisTextMessageContent(statusUpdate.result())); + var savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.LLM); + irisChatWebsocketService.sendMessage(savedMessage, statusUpdate.stages()); } - catch (GitAPIException | IOException e) { - log.error("Could not generate diff from existing template and student participation", e); - return ""; + else { + irisChatWebsocketService.sendStatusUpdate(session, statusUpdate.stages()); } } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCompetencyGenerationSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCompetencyGenerationSessionService.java index a4834111859e..998fefddb642 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCompetencyGenerationSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisCompetencyGenerationSessionService.java @@ -3,7 +3,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +15,6 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.competency.CompetencyTaxonomy; -import de.tum.in.www1.artemis.domain.iris.message.IrisJsonMessageContent; import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; import de.tum.in.www1.artemis.domain.iris.message.IrisMessageSender; import de.tum.in.www1.artemis.domain.iris.message.IrisTextMessageContent; @@ -27,11 +25,9 @@ import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorService; import de.tum.in.www1.artemis.service.iris.IrisMessageService; -import de.tum.in.www1.artemis.service.iris.exception.IrisParseResponseException; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; -import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** * Service to handle the Competency generation subsytem of Iris. @@ -42,7 +38,7 @@ public class IrisCompetencyGenerationSessionService implements IrisButtonBasedFe private static final Logger log = LoggerFactory.getLogger(IrisCompetencyGenerationSessionService.class); - private final IrisConnectorService irisConnectorService; + private final PyrisConnectorService pyrisConnectorService; private final IrisSettingsService irisSettingsService; @@ -56,10 +52,10 @@ public class IrisCompetencyGenerationSessionService implements IrisButtonBasedFe private final IrisMessageRepository irisMessageRepository; - public IrisCompetencyGenerationSessionService(IrisConnectorService irisConnectorService, IrisSettingsService irisSettingsService, IrisSessionRepository irisSessionRepository, + public IrisCompetencyGenerationSessionService(PyrisConnectorService pyrisConnectorService, IrisSettingsService irisSettingsService, IrisSessionRepository irisSessionRepository, AuthorizationCheckService authCheckService, IrisCompetencyGenerationSessionRepository irisCompetencyGenerationSessionRepository, IrisMessageService irisMessageService, IrisMessageRepository irisMessageRepository) { - this.irisConnectorService = irisConnectorService; + this.pyrisConnectorService = pyrisConnectorService; this.irisSettingsService = irisSettingsService; this.irisSessionRepository = irisSessionRepository; this.authCheckService = authCheckService; @@ -115,29 +111,8 @@ record CompetencyGenerationDTO( @Override public List<Competency> executeRequest(IrisCompetencyGenerationSession session) { - var userMessageContent = irisMessageRepository.findFirstWithContentBySessionIdAndSenderOrderBySentAtDesc(session.getId(), IrisMessageSender.USER).getContent().getFirst(); - if (!(userMessageContent instanceof IrisTextMessageContent) || userMessageContent.getContentAsString() == null) { - throw new InternalServerErrorException("Unable to get last user message!"); - } - var courseDescription = userMessageContent.getContentAsString(); - - var parameters = new CompetencyGenerationDTO(courseDescription, CompetencyTaxonomy.values()); - var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(session.getCourse(), false); - try { - var response = irisConnectorService.sendRequestV2(irisSettings.irisCompetencyGenerationSettings().getTemplate().getContent(), - irisSettings.irisCompetencyGenerationSettings().getPreferredModel(), parameters).get(); - var llmMessage = session.newMessage(); - llmMessage.setSender(IrisMessageSender.LLM); - llmMessage.addContent(new IrisJsonMessageContent(response.content())); - - irisSessionRepository.save(session); - - return toCompetencies(response.content()); - } - catch (InterruptedException | ExecutionException e) { - log.error("Unable to generate competencies", e); - throw new InternalServerErrorException("Unable to generate competencies: " + e.getMessage()); - } + // TODO: Re-add in a future PR. Remember to reenable the test cases! + return null; } @Override @@ -150,7 +125,7 @@ public void checkIsFeatureActivatedFor(IrisCompetencyGenerationSession irisSessi irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.COMPETENCY_GENERATION, irisSession.getCourse()); } - private List<Competency> toCompetencies(JsonNode content) throws IrisParseResponseException { + private List<Competency> toCompetencies(JsonNode content) { List<Competency> competencies = new ArrayList<>(); for (JsonNode node : content.get("competencies")) { try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java index 105ba041072e..0627843e1c59 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.service.iris.session; import java.time.ZonedDateTime; -import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,17 +10,14 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.hestia.CodeHint; -import de.tum.in.www1.artemis.domain.iris.message.IrisJsonMessageContent; -import de.tum.in.www1.artemis.domain.iris.message.IrisMessageSender; import de.tum.in.www1.artemis.domain.iris.session.IrisHestiaSession; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.iris.IrisHestiaSessionRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorService; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; -import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** * Service to handle the Hestia integration of Iris. @@ -32,7 +28,7 @@ public class IrisHestiaSessionService implements IrisButtonBasedFeatureInterface private static final Logger log = LoggerFactory.getLogger(IrisHestiaSessionService.class); - private final IrisConnectorService irisConnectorService; + private final PyrisConnectorService pyrisConnectorService; private final IrisSettingsService irisSettingsService; @@ -42,9 +38,9 @@ public class IrisHestiaSessionService implements IrisButtonBasedFeatureInterface private final IrisHestiaSessionRepository irisHestiaSessionRepository; - public IrisHestiaSessionService(IrisConnectorService irisConnectorService, IrisSettingsService irisSettingsService, AuthorizationCheckService authCheckService, + public IrisHestiaSessionService(PyrisConnectorService pyrisConnectorService, IrisSettingsService irisSettingsService, AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, IrisHestiaSessionRepository irisHestiaSessionRepository) { - this.irisConnectorService = irisConnectorService; + this.pyrisConnectorService = pyrisConnectorService; this.irisSettingsService = irisSettingsService; this.authCheckService = authCheckService; this.irisSessionRepository = irisSessionRepository; @@ -92,28 +88,8 @@ record HestiaDTO( */ @Override public CodeHint executeRequest(IrisHestiaSession session) { - var irisSession = irisHestiaSessionRepository.findWithMessagesAndContentsAndCodeHintById(session.getId()); - var codeHint = irisSession.getCodeHint(); - var parameters = new HestiaDTO(irisSession.getCodeHint(), irisSession, codeHint.getExercise()); - var settings = irisSettingsService.getCombinedIrisSettingsFor(irisSession.getCodeHint().getExercise(), false).irisHestiaSettings(); - try { - var response = irisConnectorService.sendRequestV2(settings.getTemplate().getContent(), settings.getPreferredModel(), parameters).get(); - var shortDescription = response.content().get("shortDescription").asText(); - var longDescription = response.content().get("longDescription").asText(); - var llmMessage = irisSession.newMessage(); - llmMessage.setSender(IrisMessageSender.LLM); - llmMessage.addContent(new IrisJsonMessageContent(response.content())); - - irisSessionRepository.save(irisSession); - - codeHint.setDescription(shortDescription); - codeHint.setContent(longDescription); - return codeHint; - } - catch (InterruptedException | ExecutionException e) { - log.error("Unable to generate description", e); - throw new InternalServerErrorException("Unable to generate description: " + e.getMessage()); - } + // TODO: Re-add in a future PR. Remember to reenable the test cases! + return null; } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java index a74029be2cb0..287516a98d1c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java @@ -27,8 +27,8 @@ import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettings; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; import de.tum.in.www1.artemis.service.iris.IrisDefaultTemplateService; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSettingsDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenAlertException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; @@ -484,9 +484,9 @@ public void deleteSettingsFor(Exercise exercise) { */ private boolean isFeatureEnabledInSettings(IrisCombinedSettingsDTO settings, IrisSubSettingsType type) { return switch (type) { - case CHAT -> settings.irisChatSettings().isEnabled(); - case HESTIA -> settings.irisHestiaSettings().isEnabled(); - case COMPETENCY_GENERATION -> settings.irisCompetencyGenerationSettings().isEnabled(); + case CHAT -> settings.irisChatSettings().enabled(); + case HESTIA -> settings.irisHestiaSettings().enabled(); + case COMPETENCY_GENERATION -> settings.irisCompetencyGenerationSettings().enabled(); }; } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java index c3fec9a2b920..6631a7ee58f0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Function; @@ -20,9 +21,9 @@ import de.tum.in.www1.artemis.domain.iris.settings.IrisSettingsType; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettings; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedChatSubSettingsDTO; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedCompetencyGenerationSubSettingsDTO; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedHestiaSubSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedChatSubSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedCompetencyGenerationSubSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedHestiaSubSettingsDTO; /** * Service for handling {@link IrisSubSettings} objects. @@ -73,7 +74,7 @@ public IrisChatSubSettings update(IrisChatSubSettings currentSettings, IrisChatS } currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.getAllowedModels() : null)); + parentSettings != null ? parentSettings.allowedModels() : null)); currentSettings.setTemplate(newSettings.getTemplate()); return currentSettings; } @@ -107,7 +108,7 @@ public IrisHestiaSubSettings update(IrisHestiaSubSettings currentSettings, IrisH } currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.getAllowedModels() : null)); + parentSettings != null ? parentSettings.allowedModels() : null)); currentSettings.setTemplate(newSettings.getTemplate()); return currentSettings; } @@ -141,7 +142,7 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); } currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.getAllowedModels() : null)); + parentSettings != null ? parentSettings.allowedModels() : null)); currentSettings.setTemplate(newSettings.getTemplate()); return currentSettings; } @@ -155,7 +156,7 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet * @param updatedAllowedModels The allowed models of the updated settings. * @return The filtered allowed models. */ - private Set<String> selectAllowedModels(Set<String> allowedModels, Set<String> updatedAllowedModels) { + private SortedSet<String> selectAllowedModels(SortedSet<String> allowedModels, SortedSet<String> updatedAllowedModels) { return authCheckService.isAdmin() ? updatedAllowedModels : allowedModels; } @@ -193,15 +194,12 @@ private String validatePreferredModel(String preferredModel, String newPreferred * @return Combined chat settings. */ public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList<IrisSettings> settingsList, boolean minimal) { - var combinedChatSettings = new IrisCombinedChatSubSettingsDTO(); - combinedChatSettings.setEnabled(getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings)); - combinedChatSettings.setRateLimit(getCombinedRateLimit(settingsList)); - if (!minimal) { - combinedChatSettings.setAllowedModels(getCombinedAllowedModels(settingsList, IrisSettings::getIrisChatSettings)); - combinedChatSettings.setPreferredModel(getCombinedPreferredModel(settingsList, IrisSettings::getIrisChatSettings)); - combinedChatSettings.setTemplate(getCombinedTemplate(settingsList, IrisSettings::getIrisChatSettings, IrisChatSubSettings::getTemplate)); - } - return combinedChatSettings; + var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); + var rateLimit = getCombinedRateLimit(settingsList); + var allowedModels = minimal ? getCombinedAllowedModels(settingsList, IrisSettings::getIrisChatSettings) : null; + var preferredModel = minimal ? getCombinedPreferredModel(settingsList, IrisSettings::getIrisChatSettings) : null; + var template = minimal ? getCombinedTemplate(settingsList, IrisSettings::getIrisChatSettings, IrisChatSubSettings::getTemplate) : null; + return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedModels, preferredModel, template); } /** @@ -215,14 +213,11 @@ public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList<IrisSettings */ public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList<IrisSettings> settingsList, boolean minimal) { var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); - var combinedHestiaSettings = new IrisCombinedHestiaSubSettingsDTO(); - combinedHestiaSettings.setEnabled(getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings)); - if (!minimal) { - combinedHestiaSettings.setAllowedModels(getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings)); - combinedHestiaSettings.setPreferredModel(getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings)); - combinedHestiaSettings.setTemplate(getCombinedTemplate(actualSettingsList, IrisSettings::getIrisHestiaSettings, IrisHestiaSubSettings::getTemplate)); - } - return combinedHestiaSettings; + var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings); + var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; + var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; + var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisHestiaSettings, IrisHestiaSubSettings::getTemplate) : null; + return new IrisCombinedHestiaSubSettingsDTO(enabled, allowedModels, preferredModel, template); } /** @@ -236,15 +231,12 @@ public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList<IrisSett */ public IrisCombinedCompetencyGenerationSubSettingsDTO combineCompetencyGenerationSettings(ArrayList<IrisSettings> settingsList, boolean minimal) { var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); - var combinedCompetencyGenerationSettings = new IrisCombinedCompetencyGenerationSubSettingsDTO(); - combinedCompetencyGenerationSettings.setEnabled(getCombinedEnabled(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings)); - if (!minimal) { - combinedCompetencyGenerationSettings.setAllowedModels(getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings)); - combinedCompetencyGenerationSettings.setPreferredModel(getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings)); - combinedCompetencyGenerationSettings - .setTemplate(getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::getTemplate)); - } - return combinedCompetencyGenerationSettings; + var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings); + var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::getTemplate) + : null; + return new IrisCombinedCompetencyGenerationSubSettingsDTO(enabled, allowedModels, preferredModel, template); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisChatWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisChatWebsocketService.java index d3dd8cca90f9..fdd243df64ac 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisChatWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisChatWebsocketService.java @@ -1,21 +1,17 @@ package de.tum.in.www1.artemis.service.iris.websocket; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; +import java.util.List; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import com.fasterxml.jackson.annotation.JsonInclude; - import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; import de.tum.in.www1.artemis.service.WebsocketMessagingService; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; -import de.tum.in.www1.artemis.service.iris.exception.IrisException; @Service @Profile("iris") @@ -41,12 +37,13 @@ private User checkSessionTypeAndGetUser(IrisSession irisSession) { * Sends a message over the websocket to a specific user * * @param irisMessage that should be sent over the websocket + * @param stages that should be sent over the websocket */ - public void sendMessage(IrisMessage irisMessage) { + public void sendMessage(IrisMessage irisMessage, List<PyrisStageDTO> stages) { var session = irisMessage.getSession(); var user = checkSessionTypeAndGetUser(session); var rateLimitInfo = rateLimitService.getRateLimitInformation(user); - super.send(user, WEBSOCKET_TOPIC_SESSION_TYPE, session.getId(), new IrisWebsocketDTO(irisMessage, rateLimitInfo)); + super.send(user, WEBSOCKET_TOPIC_SESSION_TYPE, session.getId(), new IrisWebsocketDTO(irisMessage, null, rateLimitInfo, stages)); } /** @@ -54,97 +51,16 @@ public void sendMessage(IrisMessage irisMessage) { * * @param session to which the exception belongs * @param throwable that should be sent over the websocket + * @param stages that should be sent over the websocket */ - public void sendException(IrisSession session, Throwable throwable) { + public void sendException(IrisSession session, Throwable throwable, List<PyrisStageDTO> stages) { User user = checkSessionTypeAndGetUser(session); var rateLimitInfo = rateLimitService.getRateLimitInformation(user); - super.send(user, WEBSOCKET_TOPIC_SESSION_TYPE, session.getId(), new IrisWebsocketDTO(throwable, rateLimitInfo)); + super.send(user, WEBSOCKET_TOPIC_SESSION_TYPE, session.getId(), new IrisWebsocketDTO(null, throwable, rateLimitInfo, stages)); } - @JsonInclude(JsonInclude.Include.NON_EMPTY) - public static class IrisWebsocketDTO { - - private final IrisWebsocketMessageType type; - - private final IrisMessage message; - - private final String errorMessage; - - private final String errorTranslationKey; - - private final Map<String, Object> translationParams; - - private final IrisRateLimitService.IrisRateLimitInformation rateLimitInfo; - - public IrisWebsocketDTO(IrisMessage message, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { - this.rateLimitInfo = rateLimitInfo; - this.type = IrisWebsocketMessageType.MESSAGE; - this.message = message; - this.errorMessage = null; - this.errorTranslationKey = null; - this.translationParams = null; - } - - public IrisWebsocketDTO(Throwable throwable, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { - this.rateLimitInfo = rateLimitInfo; - this.type = IrisWebsocketMessageType.ERROR; - this.message = null; - this.errorMessage = throwable.getMessage(); - this.errorTranslationKey = throwable instanceof IrisException irisException ? irisException.getTranslationKey() : null; - this.translationParams = throwable instanceof IrisException irisException ? irisException.getTranslationParams() : null; - } - - public IrisWebsocketMessageType getType() { - return type; - } - - public IrisMessage getMessage() { - return message; - } - - public String getErrorMessage() { - return errorMessage; - } - - public String getErrorTranslationKey() { - return errorTranslationKey; - } - - public Map<String, Object> getTranslationParams() { - return translationParams != null ? Collections.unmodifiableMap(translationParams) : null; - } - - public IrisRateLimitService.IrisRateLimitInformation getRateLimitInfo() { - return rateLimitInfo; - } - - public enum IrisWebsocketMessageType { - MESSAGE, ERROR - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - IrisWebsocketDTO that = (IrisWebsocketDTO) other; - return type == that.type && Objects.equals(message, that.message) && Objects.equals(errorMessage, that.errorMessage) - && Objects.equals(errorTranslationKey, that.errorTranslationKey) && Objects.equals(translationParams, that.translationParams); - } - - @Override - public int hashCode() { - return Objects.hash(type, message, errorMessage, errorTranslationKey, translationParams); - } - - @Override - public String toString() { - return "IrisWebsocketDTO{" + "type=" + type + ", message=" + message + ", errorMessage='" + errorMessage + '\'' + ", errorTranslationKey='" + errorTranslationKey + '\'' - + ", translationParams=" + translationParams + '}'; - } + public void sendStatusUpdate(IrisSession session, List<PyrisStageDTO> stages) { + var user = checkSessionTypeAndGetUser(session); + super.send(user, WEBSOCKET_TOPIC_SESSION_TYPE, session.getId(), new IrisWebsocketDTO(null, null, rateLimitService.getRateLimitInformation(user), stages)); } - } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisWebsocketDTO.java b/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisWebsocketDTO.java new file mode 100644 index 000000000000..3d7543471893 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/websocket/IrisWebsocketDTO.java @@ -0,0 +1,80 @@ +package de.tum.in.www1.artemis.service.iris.websocket; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; +import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; +import de.tum.in.www1.artemis.service.iris.exception.IrisException; + +/** + * A DTO for sending status updates of Iris to the client via the websocket + * + * @param type the type of the message + * @param message an IrisMessage instance if the type is MESSAGE + * @param errorMessage the error message if the type is ERROR + * @param errorTranslationKey the translation key for the error message if the type is ERROR + * @param translationParams the translation parameters for the error message if the type is ERROR + * @param rateLimitInfo the rate limit information + * @param stages the stages of the Pyris pipeline + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record IrisWebsocketDTO(IrisWebsocketMessageType type, IrisMessage message, String errorMessage, String errorTranslationKey, Map<String, Object> translationParams, + IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, List<PyrisStageDTO> stages) { + + /** + * Creates a new IrisWebsocketDTO instance with the given parameters + * Takes care of setting the type correctly and also extracts the error message and translation key from the throwable (if present) + * + * @param message the IrisMessage (optional) + * @param throwable the Throwable (optional) + * @param rateLimitInfo the rate limit information + * @param stages the stages of the Pyris pipeline + */ + public IrisWebsocketDTO(@Nullable IrisMessage message, @Nullable Throwable throwable, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, List<PyrisStageDTO> stages) { + this(determineType(message, throwable), message, throwable == null ? null : throwable.getMessage(), + throwable instanceof IrisException irisException ? irisException.getTranslationKey() : null, + throwable instanceof IrisException irisException ? irisException.getTranslationParams() : null, rateLimitInfo, stages); + } + + /** + * Determines the type of WebSocket message based on the presence of a message or throwable. + * <p> + * This method categorizes the WebSocket message type as follows: + * <ul> + * <li>{@link IrisWebsocketMessageType#MESSAGE} if the {@code message} parameter is not null.</li> + * <li>{@link IrisWebsocketMessageType#ERROR} if the {@code message} is null and {@code throwable} is not null.</li> + * <li>{@link IrisWebsocketMessageType#STATUS} if both {@code message} and {@code throwable} are null.</li> + * </ul> + * + * @param message The message associated with the WebSocket, which may be null. + * @param throwable The throwable associated with the WebSocket, which may also be null. + * @return The {@link IrisWebsocketMessageType} indicating the type of the message based on the given parameters. + */ + private static IrisWebsocketMessageType determineType(@Nullable IrisMessage message, @Nullable Throwable throwable) { + if (message != null) { + return IrisWebsocketMessageType.MESSAGE; + } + else if (throwable != null) { + return IrisWebsocketMessageType.ERROR; + } + else { + return IrisWebsocketMessageType.STATUS; + } + } + + @Override + public Map<String, Object> translationParams() { + return translationParams != null ? Collections.unmodifiableMap(translationParams) : null; + } + + public enum IrisWebsocketMessageType { + MESSAGE, STATUS, ERROR + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/messaging/DistributedInstanceMessageSendService.java b/src/main/java/de/tum/in/www1/artemis/service/messaging/DistributedInstanceMessageSendService.java index 826541c1c1db..5b83f9b5486e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/messaging/DistributedInstanceMessageSendService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/messaging/DistributedInstanceMessageSendService.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -25,7 +26,7 @@ public class DistributedInstanceMessageSendService implements InstanceMessageSen private final HazelcastInstance hazelcastInstance; - public DistributedInstanceMessageSendService(HazelcastInstance hazelcastInstance) { + public DistributedInstanceMessageSendService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { this.hazelcastInstance = hazelcastInstance; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/messaging/InstanceMessageReceiveService.java b/src/main/java/de/tum/in/www1/artemis/service/messaging/InstanceMessageReceiveService.java index 0bb9114d34d1..e11c25715caa 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/messaging/InstanceMessageReceiveService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/messaging/InstanceMessageReceiveService.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -61,8 +62,8 @@ public class InstanceMessageReceiveService { public InstanceMessageReceiveService(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseScheduleService programmingExerciseScheduleService, ModelingExerciseRepository modelingExerciseRepository, ModelingExerciseScheduleService modelingExerciseScheduleService, ExerciseRepository exerciseRepository, - Optional<AthenaScheduleService> athenaScheduleService, HazelcastInstance hazelcastInstance, UserRepository userRepository, UserScheduleService userScheduleService, - NotificationScheduleService notificationScheduleService, ParticipantScoreScheduleService participantScoreScheduleService) { + Optional<AthenaScheduleService> athenaScheduleService, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, UserRepository userRepository, + UserScheduleService userScheduleService, NotificationScheduleService notificationScheduleService, ParticipantScoreScheduleService participantScoreScheduleService) { this.programmingExerciseRepository = programmingExerciseRepository; this.programmingExerciseScheduleService = programmingExerciseScheduleService; this.athenaScheduleService = athenaScheduleService; diff --git a/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java b/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java index 0c7096c51524..c6771f9aae2f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/metis/conversation/ConversationService.java @@ -373,7 +373,7 @@ private void sendToConversationMembershipChannel(MetisCrudAction metisCrudAction public Page<User> searchMembersOfConversation(Course course, Conversation conversation, Pageable pageable, String searchTerm, Optional<ConversationMemberSearchFilters> filter) { if (filter.isEmpty()) { - if (conversation instanceof Channel && ((Channel) conversation).getIsCourseWide()) { + if (conversation instanceof Channel channel && channel.getIsCourseWide()) { return userRepository.searchAllByLoginOrNameInCourse(pageable, searchTerm, course.getId()); } return userRepository.searchAllByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); @@ -389,13 +389,15 @@ public Page<User> searchMembersOfConversation(Course course, Conversation conver } case STUDENT -> groups.add(course.getStudentGroupName()); case CHANNEL_MODERATOR -> { - assert conversation instanceof Channel : "The filter CHANNEL_MODERATOR is only allowed for channels!"; + if (!(conversation instanceof Channel)) { + throw new IllegalArgumentException("The filter CHANNEL_MODERATOR is only allowed for channels!"); + } return userRepository.searchChannelModeratorsByLoginOrNameInConversation(pageable, searchTerm, conversation.getId()); } default -> throw new IllegalArgumentException("The filter is not supported."); } - if (conversation instanceof Channel && ((Channel) conversation).getIsCourseWide()) { + if (conversation instanceof Channel channel && channel.getIsCourseWide()) { return userRepository.searchAllByLoginOrNameInGroups(pageable, searchTerm, groups); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/notifications/MailService.java b/src/main/java/de/tum/in/www1/artemis/service/notifications/MailService.java index ddd15f3dda06..3abe2ae70d73 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/notifications/MailService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/notifications/MailService.java @@ -35,7 +35,6 @@ import de.tum.in.www1.artemis.domain.notification.NotificationConstants; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; -import de.tum.in.www1.artemis.exception.ArtemisMailException; import de.tum.in.www1.artemis.service.TimeService; import tech.jhipster.config.JHipsterProperties; @@ -133,7 +132,7 @@ public void sendEmail(User recipient, String subject, String content, boolean is } catch (MailException | MessagingException e) { log.error("Email could not be sent to user '{}'", recipient, e); - throw new ArtemisMailException("Email could not be sent to user", e); + // Note: we should not rethrow the exception here, as this would prevent sending out other emails in case multiple users are affected } } @@ -246,7 +245,16 @@ private String createAnnouncementText(Object notificationSubject, Locale locale) @Override @Async public void sendNotification(Notification notification, Set<User> users, Object notificationSubject) { - users.forEach(user -> sendNotification(notification, user, notificationSubject)); + // TODO: we should track how many emails could not be sent and notify the instructors in case of announcements or other important notifications + users.forEach(user -> { + try { + sendNotification(notification, user, notificationSubject); + } + catch (Exception ex) { + // Note: we should not rethrow the exception here, as this would prevent sending out other emails in case multiple users are affected + log.error("Error while sending notification email to user '{}'", user.getLogin(), ex); + } + }); } /** @@ -259,12 +267,14 @@ public void sendNotification(Notification notification, Set<User> users, Object @Override public void sendNotification(Notification notification, User user, Object notificationSubject) { NotificationType notificationType = NotificationConstants.findCorrespondingNotificationType(notification.getTitle()); - log.debug("Sending \"{}\" notification email to '{}'", notificationType.name(), user.getEmail()); + log.debug("Sending '{}' notification email to '{}'", notificationType.name(), user.getEmail()); String localeKey = user.getLangKey(); if (localeKey == null) { - throw new IllegalArgumentException( - "The user object has no language key defined. This can happen if you do not load the user object from the database but take it straight from the client"); + log.error("The user '{}' object has no language key defined. This can happen if you do not load the user object from the database but take it straight from the client", + user.getLogin()); + // use the default locale + localeKey = "en"; } Locale locale = Locale.forLanguageTag(localeKey); diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismAnswerPostService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismAnswerPostService.java index 09afc3f5ee29..c4deff573c33 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismAnswerPostService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismAnswerPostService.java @@ -106,13 +106,13 @@ public AnswerPost updateAnswerPost(Long courseId, Long answerPostId, AnswerPost } AnswerPost existingAnswerPost = this.findById(answerPostId); final Course course = courseRepository.findByIdElseThrow(courseId); - authorizationCheckService.isAtLeastStudentInCourse(course, user); + authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); parseUserMentions(course, answerPost.getContent()); AnswerPost updatedAnswerPost; // determine if the update operation is to mark the answer post as resolving the original post - if (existingAnswerPost.doesResolvePost() != answerPost.doesResolvePost()) { + if (!Objects.equals(existingAnswerPost.doesResolvePost(), answerPost.doesResolvePost())) { // check if requesting user is allowed to mark this answer post as resolving, i.e. if user is author or original post or at least tutor mayMarkAnswerPostAsResolvingElseThrow(existingAnswerPost, user, course); existingAnswerPost.setResolvesPost(answerPost.doesResolvePost()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/cache/PlagiarismCacheService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/cache/PlagiarismCacheService.java index 4a4f0a63c72d..97da6c76dfb9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/cache/PlagiarismCacheService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/cache/PlagiarismCacheService.java @@ -5,6 +5,7 @@ import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -20,7 +21,7 @@ public class PlagiarismCacheService { // Every course in this set is currently doing a plagiarism check private ISet<Long> activePlagiarismChecksPerCourse; - public PlagiarismCacheService(HazelcastInstance hazelcastInstance) { + public PlagiarismCacheService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { this.hazelcastInstance = hazelcastInstance; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java index 5533116f5b2b..e39e2ebfcd03 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java @@ -148,7 +148,7 @@ private void sendFeedbackToAthena(final ProgrammingExercise exercise, final Prog private void handleResolvedFeedbackRequest(StudentParticipation participation) { var exercise = participation.getExercise(); - var isManualFeedbackRequest = exercise.getAllowManualFeedbackRequests() && participation.getIndividualDueDate() != null + var isManualFeedbackRequest = exercise.getAllowFeedbackRequests() && participation.getIndividualDueDate() != null && participation.getIndividualDueDate().isBefore(ZonedDateTime.now()); // We need to use the general exercise due date here and not the individual participation due date. // This feature temporarily locks the repository by setting the individual due date to the past. diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseCodeReviewFeedbackService.java new file mode 100644 index 000000000000..ce00d6c1cbf5 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseCodeReviewFeedbackService.java @@ -0,0 +1,239 @@ +package de.tum.in.www1.artemis.service.programming; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static java.time.ZonedDateTime.now; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Feedback; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingSubmission; +import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; +import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; +import de.tum.in.www1.artemis.repository.ResultRepository; +import de.tum.in.www1.artemis.service.ResultService; +import de.tum.in.www1.artemis.service.SubmissionService; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaFeedbackSuggestionsService; +import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; + +/** + * Service class for managing code review feedback on programming exercises. + * This service handles the processing of feedback requests for programming exercises, + * including automatic generation of feedback through integration with external services + * such as Athena. + */ +@Profile(PROFILE_CORE) +@Service +public class ProgrammingExerciseCodeReviewFeedbackService { + + private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseCodeReviewFeedbackService.class); + + public static final String NON_GRADED_FEEDBACK_SUGGESTION = "NonGradedFeedbackSuggestion:"; + + private final GroupNotificationService groupNotificationService; + + private final Optional<AthenaFeedbackSuggestionsService> athenaFeedbackSuggestionsService; + + private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository; + + private final SubmissionService submissionService; + + private final ResultService resultService; + + private final ResultRepository resultRepository; + + private final ProgrammingExerciseParticipationService programmingExerciseParticipationService; + + private final ProgrammingMessagingService programmingMessagingService; + + public ProgrammingExerciseCodeReviewFeedbackService(GroupNotificationService groupNotificationService, + Optional<AthenaFeedbackSuggestionsService> athenaFeedbackSuggestionsService, SubmissionService submissionService, ResultService resultService, + ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ResultRepository resultRepository, + ProgrammingExerciseParticipationService programmingExerciseParticipationService1, ProgrammingMessagingService programmingMessagingService) { + this.groupNotificationService = groupNotificationService; + this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService; + this.submissionService = submissionService; + this.resultService = resultService; + this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; + this.resultRepository = resultRepository; + this.programmingExerciseParticipationService = programmingExerciseParticipationService1; + this.programmingMessagingService = programmingMessagingService; + } + + /** + * Handles the request for generating feedback for a programming exercise. + * This method decides whether to generate feedback automatically using Athena, + * or notify a tutor to manually process the feedback. + * + * @param exerciseId the id of the programming exercise. + * @param participation the student participation associated with the exercise. + * @param programmingExercise the programming exercise object. + * @return ProgrammingExerciseStudentParticipation updated programming exercise for a tutor assessment + */ + public ProgrammingExerciseStudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, ProgrammingExerciseStudentParticipation participation, + ProgrammingExercise programmingExercise) { + if (this.athenaFeedbackSuggestionsService.isPresent()) { + this.checkRateLimitOrThrow(participation); + CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, programmingExercise)); + return participation; + } + else { + log.debug("tutor is responsible to process feedback request: {}", exerciseId); + groupNotificationService.notifyTutorGroupAboutNewFeedbackRequest(programmingExercise); + return setIndividualDueDateAndLockRepository(participation, programmingExercise, true); + } + } + + /** + * Generates automatic non-graded feedback for a programming exercise submission. + * This method leverages the Athena service to generate feedback based on the latest submission. + * + * @param participation the student participation associated with the exercise. + * @param programmingExercise the programming exercise object. + */ + public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentParticipation participation, ProgrammingExercise programmingExercise) { + log.debug("Using athena to generate feedback request: {}", programmingExercise.getId()); + + // athena takes over the control here + var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) + .findLatestSubmission(); + if (submissionOptional.isEmpty()) { + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + } + var submission = submissionOptional.get(); + + // save result and transmit it over websockets to notify the client about the status + var automaticResult = this.submissionService.saveNewEmptyResult(submission); + automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + automaticResult.setRated(false); + automaticResult.setScore(100.0); + automaticResult.setSuccessful(null); + automaticResult.setCompletionDate(ZonedDateTime.now().plusMinutes(5)); // we do not want to show dates without a completion date, but we want the students to know their + // feedback request is in work + automaticResult = this.resultRepository.save(automaticResult); + + try { + + setIndividualDueDateAndLockRepository(participation, programmingExercise, false); + this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); + // now the client should be able to see new result + + log.debug("Submission id: {}", submission.getId()); + + var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getProgrammingFeedbackSuggestions(programmingExercise, (ProgrammingSubmission) submission, + false); + + List<Feedback> feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.filePath() != null) + .filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> { + var feedback = new Feedback(); + String feedbackText; + if (Objects.nonNull(individualFeedbackItem.lineStart())) { + if (Objects.nonNull(individualFeedbackItem.lineEnd()) && !individualFeedbackItem.lineStart().equals(individualFeedbackItem.lineEnd())) { + feedbackText = String.format(NON_GRADED_FEEDBACK_SUGGESTION + "File %s at lines %d-%d", individualFeedbackItem.filePath(), + individualFeedbackItem.lineStart(), individualFeedbackItem.lineEnd()); + } + else { + feedbackText = String.format(NON_GRADED_FEEDBACK_SUGGESTION + "File %s at line %d", individualFeedbackItem.filePath(), + individualFeedbackItem.lineStart()); + } + feedback.setReference(String.format("file:%s_line:%d", individualFeedbackItem.filePath(), individualFeedbackItem.lineStart())); + } + else { + feedbackText = String.format(NON_GRADED_FEEDBACK_SUGGESTION + "File %s", individualFeedbackItem.filePath()); + } + feedback.setText(feedbackText); + feedback.setDetailText(individualFeedbackItem.description()); + feedback.setHasLongFeedbackText(false); + feedback.setType(FeedbackType.AUTOMATIC); + feedback.setCredits(0.0); + return feedback; + }).toList(); + + automaticResult.setSuccessful(true); + automaticResult.setCompletionDate(ZonedDateTime.now()); + + this.resultService.storeFeedbackInResult(automaticResult, feedbacks, true); + + this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); + } + catch (Exception e) { + log.error("Could not generate feedback", e); + automaticResult.setSuccessful(false); + automaticResult.setCompletionDate(ZonedDateTime.now()); + this.resultRepository.save(automaticResult); + this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); + } + finally { + unlockRepository(participation, programmingExercise); + } + } + + /** + * Sets an individual due date for a participation, locks the repository, + * and invalidates previous results to prepare for new feedback. + * + * @param participation the programming exercise student participation. + * @param programmingExercise the associated programming exercise. + */ + private ProgrammingExerciseStudentParticipation setIndividualDueDateAndLockRepository(ProgrammingExerciseStudentParticipation participation, + ProgrammingExercise programmingExercise, boolean invalidatePreviousResults) { + // The participations due date is a flag showing that a feedback request is sent + participation.setIndividualDueDate(now()); + + participation = programmingExerciseStudentParticipationRepository.save(participation); + // Circumvent lazy loading after save + participation.setParticipant(participation.getParticipant()); + programmingExerciseParticipationService.lockStudentRepositoryAndParticipation(programmingExercise, participation); + + if (invalidatePreviousResults) { + var participationResults = participation.getResults(); + participationResults.forEach(participationResult -> participationResult.setRated(false)); + this.resultRepository.saveAll(participationResults); + } + + return participation; + } + + /** + * Removes the individual due date for a participation. If the due date for an exercise is empty or is in the future, unlocks the repository, + * + * @param participation the programming exercise student participation. + * @param programmingExercise the associated programming exercise. + */ + private void unlockRepository(ProgrammingExerciseStudentParticipation participation, ProgrammingExercise programmingExercise) { + if (programmingExercise.getDueDate() == null || now().isBefore(programmingExercise.getDueDate())) { + programmingExerciseParticipationService.unlockStudentRepositoryAndParticipation(participation); + participation.setIndividualDueDate(null); + this.programmingExerciseStudentParticipationRepository.save(participation); + } + } + + private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation participation) { + + List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + + long countOfAthenaResultsInProcessOrSuccessful = athenaResults.stream().filter(result -> result.isSuccessful() == null || result.isSuccessful() == Boolean.TRUE).count(); + + long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); + + if (countOfAthenaResultsInProcessOrSuccessful >= 3) { + throw new BadRequestAlertException("Cannot send additional AI feedback requests now. Try again later!", "participation", "preconditions not met"); + } + if (countOfSuccessfulRequests >= 3) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index 4e97095c69d8..0819be9ea17b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -343,7 +343,7 @@ public void validateNewProgrammingExerciseSettings(ProgrammingExercise programmi programmingExercise.validateGeneralSettings(); programmingExercise.validateProgrammingSettings(); - programmingExercise.validateManualFeedbackSettings(); + programmingExercise.validateSettingsForFeedbackRequest(); auxiliaryRepositoryService.validateAndAddAuxiliaryRepositoriesOfProgrammingExercise(programmingExercise, programmingExercise.getAuxiliaryRepositories()); submissionPolicyService.validateSubmissionPolicyCreation(programmingExercise); diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizBatchService.java similarity index 99% rename from src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizBatchService.java index 9e9c6383f7cf..c1c79fe2e517 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizBatchService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizBatchService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java similarity index 97% rename from src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java index 87ef371db0d8..3c328b89e72d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -32,6 +32,10 @@ import de.tum.in.www1.artemis.repository.ExampleSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; +import de.tum.in.www1.artemis.service.ExerciseImportService; +import de.tum.in.www1.artemis.service.FeedbackService; +import de.tum.in.www1.artemis.service.FilePathService; +import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; @Profile(PROFILE_CORE) diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseService.java similarity index 99% rename from src/main/java/de/tum/in/www1/artemis/service/QuizExerciseService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseService.java index d4626b3184c6..a9b445815b80 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -49,6 +49,9 @@ import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.ShortAnswerMappingRepository; +import de.tum.in.www1.artemis.service.ExerciseSpecificationService; +import de.tum.in.www1.artemis.service.FilePathService; +import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizMessagingService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizMessagingService.java similarity index 96% rename from src/main/java/de/tum/in/www1/artemis/service/QuizMessagingService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizMessagingService.java index a09c049c8b7d..c1510bc26844 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizMessagingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizMessagingService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -16,6 +16,7 @@ import de.tum.in.www1.artemis.domain.quiz.QuizBatch; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; +import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; @Profile(PROFILE_CORE) diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizPoolService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizPoolService.java similarity index 99% rename from src/main/java/de/tum/in/www1/artemis/service/QuizPoolService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizPoolService.java index af19c7f78ea9..fa47d0cd568e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizPoolService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizPoolService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizService.java similarity index 99% rename from src/main/java/de/tum/in/www1/artemis/service/QuizService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizService.java index 81bd0b0908bc..5b4d0f967091 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizStatisticService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizStatisticService.java similarity index 98% rename from src/main/java/de/tum/in/www1/artemis/service/QuizStatisticService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizStatisticService.java index ed84f9949eac..c892faf61d45 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizStatisticService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizStatisticService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -23,6 +23,7 @@ import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; +import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.connectors.lti.LtiNewResultService; @Profile(PROFILE_CORE) diff --git a/src/main/java/de/tum/in/www1/artemis/service/QuizSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizSubmissionService.java similarity index 98% rename from src/main/java/de/tum/in/www1/artemis/service/QuizSubmissionService.java rename to src/main/java/de/tum/in/www1/artemis/service/quiz/QuizSubmissionService.java index ebf7e60e91cc..bc6672d45d69 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/QuizSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizSubmissionService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service; +package de.tum.in.www1.artemis.service.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; @@ -27,6 +27,9 @@ import de.tum.in.www1.artemis.repository.QuizExerciseRepository; import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; +import de.tum.in.www1.artemis.service.AbstractQuizSubmissionService; +import de.tum.in.www1.artemis.service.ParticipationService; +import de.tum.in.www1.artemis.service.SubmissionVersionService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java index eca7152a5f03..b08f6d4e2c94 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java @@ -1,17 +1,23 @@ package de.tum.in.www1.artemis.service.scheduled; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.ATHENA_SCHEDULE_DELAY_SEC; + +import java.time.Instant; import java.time.ZonedDateTime; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; -import jakarta.annotation.PostConstruct; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Exercise; @@ -34,23 +40,31 @@ public class AthenaScheduleService { private final ProfileService profileService; + private final TaskScheduler taskScheduler; + private final Map<Long, ScheduledFuture<?>> scheduledAthenaTasks = new HashMap<>(); private final AthenaSubmissionSendingService athenaSubmissionSendingService; public AthenaScheduleService(ExerciseLifecycleService exerciseLifecycleService, ExerciseRepository exerciseRepository, ProfileService profileService, - AthenaSubmissionSendingService athenaSubmissionSendingService) { + @Qualifier("taskScheduler") TaskScheduler taskScheduler, AthenaSubmissionSendingService athenaSubmissionSendingService) { this.exerciseLifecycleService = exerciseLifecycleService; this.exerciseRepository = exerciseRepository; this.profileService = profileService; + this.taskScheduler = taskScheduler; this.athenaSubmissionSendingService = athenaSubmissionSendingService; } + @EventListener(ApplicationReadyEvent.class) + public void startup() { + // schedule the task after the application has started to avoid delaying the start of the application + taskScheduler.schedule(this::scheduleRunningExercisesOnStartup, Instant.now().plusSeconds(ATHENA_SCHEDULE_DELAY_SEC)); + } + /** * Schedule Athena tasks for all exercises with future due dates on startup. */ - @PostConstruct - public void scheduleRunningExercisesOnStartup() { + private void scheduleRunningExercisesOnStartup() { if (profileService.isDevActive()) { // only execute this on production server, i.e. when the prod profile is active // NOTE: if you want to test this locally, please comment it out, but do not commit the changes diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ModelingExerciseScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ModelingExerciseScheduleService.java index 7aa0c64011d0..a35cd22423d8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ModelingExerciseScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ModelingExerciseScheduleService.java @@ -1,21 +1,24 @@ package de.tum.in.www1.artemis.service.scheduled; import static de.tum.in.www1.artemis.config.Constants.EXAM_END_WAIT_TIME_FOR_COMPASS_MINUTES; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.MODELING_EXERCISE_SCHEDULE_DELAY_SEC; import static java.time.Instant.now; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; -import jakarta.annotation.PostConstruct; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -58,7 +61,12 @@ public ModelingExerciseScheduleService(ScheduleService scheduleService, Modeling this.examDateService = examDateService; } - @PostConstruct + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + // schedule the task after the application has started to avoid delaying the start of the application + scheduler.schedule(this::scheduleRunningExercisesOnStartup, Instant.now().plusSeconds(MODELING_EXERCISE_SCHEDULE_DELAY_SEC)); + } + @Override public void scheduleRunningExercisesOnStartup() { try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java index 202289ec2b72..d3b53ce88fc6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/NotificationScheduleService.java @@ -1,13 +1,18 @@ package de.tum.in.www1.artemis.service.scheduled; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.NOTIFICATION_SCHEDULE_DELAY_SEC; + +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Set; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Exercise; @@ -35,20 +40,28 @@ public class NotificationScheduleService { private final SingleUserNotificationService singleUserNotificationService; + private final TaskScheduler scheduler; + public NotificationScheduleService(ScheduleService scheduleService, ExerciseRepository exerciseRepository, ProfileService profileService, - GroupNotificationService groupNotificationService, SingleUserNotificationService singleUserNotificationService) { + GroupNotificationService groupNotificationService, SingleUserNotificationService singleUserNotificationService, @Qualifier("taskScheduler") TaskScheduler scheduler) { this.scheduleService = scheduleService; this.exerciseRepository = exerciseRepository; this.profileService = profileService; this.groupNotificationService = groupNotificationService; this.singleUserNotificationService = singleUserNotificationService; + this.scheduler = scheduler; + } + + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + // schedule the task after the application has started to avoid delaying the start of the application + scheduler.schedule(this::scheduleRunningNotificationProcessesOnStartup, Instant.now().plusSeconds(NOTIFICATION_SCHEDULE_DELAY_SEC)); } /** - * Schedules ongoing notification processes on server start up + * Schedules ongoing notification processes shortly after server start up */ - @PostConstruct - public void scheduleRunningNotificationProcessesOnStartup() { + private void scheduleRunningNotificationProcessesOnStartup() { try { if (profileService.isDevActive()) { // only execute this on production server, i.e. when the prod profile is active diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java index 723e27265abe..54d9d87d59df 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.scheduled; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.PARTICIPATION_SCORES_SCHEDULE_DELAY_SEC; + import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -10,7 +12,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicBoolean; -import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.validation.constraints.NotNull; @@ -18,7 +19,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -119,16 +122,18 @@ public boolean isIdle() { /** * Schedule all outdated participant scores when the service is started. */ - @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void startup() { - isRunning.set(true); - try { - // this should never prevent the application start of Artemis - scheduleTasks(); - } - catch (Exception ex) { - log.error("Cannot schedule participant score service", ex); - } + scheduler.schedule(() -> { + isRunning.set(true); + try { + // this should never prevent the application start of Artemis + scheduleTasks(); + } + catch (Exception ex) { + log.error("Cannot schedule participant score service", ex); + } + }, Instant.now().plusSeconds(PARTICIPATION_SCORES_SCHEDULE_DELAY_SEC)); } public void activate() { @@ -151,6 +156,7 @@ public void shutdown() { * We schedule all results that were created/updated since the last run of the cron job. * Additionally, we schedule all participant scores that are outdated/invalid. */ + // TODO: could be converted to TaskScheduler, but tests depend on this implementation at the moment. See QuizScheduleService for reference @Scheduled(cron = "0 * * * * *") protected void scheduleTasks() { log.debug("Schedule tasks to process..."); diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java index 82d1810483ff..67c5c86f7cdd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ProgrammingExerciseScheduleService.java @@ -1,5 +1,8 @@ package de.tum.in.www1.artemis.service.scheduled; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.PROGRAMMING_EXERCISE_SCHEDULE_DELAY_SEC; + +import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -17,14 +20,17 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import jakarta.annotation.PostConstruct; import jakarta.validation.constraints.NotNull; import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.core.env.Environment; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.config.Constants; @@ -94,12 +100,14 @@ public class ProgrammingExerciseScheduleService implements IExerciseScheduleServ private final GitService gitService; + private final TaskScheduler scheduler; + public ProgrammingExerciseScheduleService(ScheduleService scheduleService, ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, ResultRepository resultRepository, ParticipationRepository participationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseParticipationRepository, Environment env, ProgrammingTriggerService programmingTriggerService, ProgrammingExerciseGradingService programmingExerciseGradingService, GroupNotificationService groupNotificationService, ExamDateService examDateService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, ExerciseDateService exerciseDateService, ExamRepository examRepository, - StudentExamRepository studentExamRepository, GitService gitService) { + StudentExamRepository studentExamRepository, GitService gitService, @Qualifier("taskScheduler") TaskScheduler scheduler) { this.scheduleService = scheduleService; this.programmingExerciseRepository = programmingExerciseRepository; this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository; @@ -116,9 +124,15 @@ public ProgrammingExerciseScheduleService(ScheduleService scheduleService, Progr this.programmingExerciseGradingService = programmingExerciseGradingService; this.env = env; this.gitService = gitService; + this.scheduler = scheduler; + } + + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + // schedule the task after the application has started to avoid delaying the start of the application + scheduler.schedule(this::scheduleRunningExercisesOnStartup, Instant.now().plusSeconds(PROGRAMMING_EXERCISE_SCHEDULE_DELAY_SEC)); } - @PostConstruct @Override public void scheduleRunningExercisesOnStartup() { try { @@ -141,7 +155,7 @@ public void scheduleRunningExercisesOnStartup() { programmingExercisesWithExam.forEach(this::scheduleExamExercise); log.info("Scheduled {} programming exercises.", exercisesToBeScheduled.size()); - log.info("Scheduled {} programming exercises for a score update after due date.", programmingExercisesWithTestsAfterDueDateButNoRebuild.size()); + log.debug("Scheduled {} programming exercises for a score update after due date.", programmingExercisesWithTestsAfterDueDateButNoRebuild.size()); log.info("Scheduled {} exam programming exercises.", programmingExercisesWithExam.size()); } catch (Exception e) { @@ -284,11 +298,13 @@ private void scheduleCourseExercise(ProgrammingExercise exercise) { } private void scheduleTemplateCommitCombination(ProgrammingExercise exercise) { - var scheduledRunnable = Set.of(new Tuple<>(exercise.getReleaseDate().minusSeconds(Constants.SECONDS_BEFORE_RELEASE_DATE_FOR_COMBINING_TEMPLATE_COMMITS), - combineTemplateCommitsForExercise(exercise))); - scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, scheduledRunnable); - log.debug("Scheduled combining template commits before release date for Programming Exercise \"{}\" (#{}) for {}.", exercise.getTitle(), exercise.getId(), - exercise.getReleaseDate()); + if (exercise.getReleaseDate() != null) { + var scheduledRunnable = Set.of(new Tuple<>(exercise.getReleaseDate().minusSeconds(Constants.SECONDS_BEFORE_RELEASE_DATE_FOR_COMBINING_TEMPLATE_COMMITS), + combineTemplateCommitsForExercise(exercise))); + scheduleService.scheduleTask(exercise, ExerciseLifecycle.RELEASE, scheduledRunnable); + log.debug("Scheduled combining template commits before release date for Programming Exercise \"{}\" (#{}) for {}.", exercise.getTitle(), exercise.getId(), + exercise.getReleaseDate()); + } } private void scheduleDueDateLockAndScoreUpdate(ProgrammingExercise exercise) { @@ -657,14 +673,8 @@ private void stashStudentChangesAndNotifyInstructor(ProgrammingExercise exercise if (Boolean.TRUE.equals(exercise.isAllowOnlineEditor())) { var failedStashOperations = stashChangesInAllStudentRepositories(programmingExerciseId, condition); failedStashOperations.thenAccept(failures -> { - long numberOfFailedStashOperations = failures.size(); - String stashNotificationText; - if (numberOfFailedStashOperations > 0) { - stashNotificationText = Constants.PROGRAMMING_EXERCISE_FAILED_STASH_OPERATIONS_NOTIFICATION + numberOfFailedStashOperations; - } - else { - stashNotificationText = Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_STASH_OPERATION_NOTIFICATION; - } + final var stashNotificationText = getNotificationText(failures, Constants.PROGRAMMING_EXERCISE_FAILED_STASH_OPERATIONS_NOTIFICATION, + Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_STASH_OPERATION_NOTIFICATION); groupNotificationService.notifyEditorAndInstructorGroupsAboutRepositoryLocks(exercise, stashNotificationText); }); } @@ -720,14 +730,8 @@ public Runnable runUnlockOperation(ProgrammingExercise exercise, Consumer<Progra failedUnlockOperations.thenAccept(failures -> { // We send a notification to the instructor about the success of the repository unlocking operation. - long numberOfFailedUnlockOperations = failures.size(); - String notificationText; - if (numberOfFailedUnlockOperations > 0) { - notificationText = Constants.PROGRAMMING_EXERCISE_FAILED_UNLOCK_OPERATIONS_NOTIFICATION + numberOfFailedUnlockOperations; - } - else { - notificationText = Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_UNLOCK_OPERATION_NOTIFICATION; - } + final var notificationText = getNotificationText(failures, Constants.PROGRAMMING_EXERCISE_FAILED_UNLOCK_OPERATIONS_NOTIFICATION, + Constants.PROGRAMMING_EXERCISE_SUCCESSFUL_UNLOCK_OPERATION_NOTIFICATION); groupNotificationService.notifyEditorAndInstructorGroupsAboutRepositoryLocks(exercise, notificationText); // Schedule the lock operations here, this is also done here because the working times might change often before the exam start @@ -745,6 +749,19 @@ public Runnable runUnlockOperation(ProgrammingExercise exercise, Consumer<Progra }; } + private static String getNotificationText(List<ProgrammingExerciseStudentParticipation> failures, String programmingExerciseFailedUnlockOperationsNotification, + String programmingExerciseSuccessfulUnlockOperationNotification) { + long numberOfFailedUnlockOperations = failures.size(); + String notificationText; + if (numberOfFailedUnlockOperations > 0) { + notificationText = programmingExerciseFailedUnlockOperationsNotification + numberOfFailedUnlockOperations; + } + else { + notificationText = programmingExerciseSuccessfulUnlockOperationNotification; + } + return notificationText; + } + /** * Returns a runnable that, once executed, will unlock all student repositories and will schedule all repository lock tasks. * Tasks to unlock will be grouped so that for every existing due date (which is the exam start date + the different working times), one task will be scheduled. diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/WeeklyEmailSummaryScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/WeeklyEmailSummaryScheduleService.java index 024597b8e17f..085825162411 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/WeeklyEmailSummaryScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/WeeklyEmailSummaryScheduleService.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.scheduled; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.EMAIL_SUMMARY_SCHEDULE_DELAY_SEC; + import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; @@ -7,12 +9,12 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -49,10 +51,15 @@ public WeeklyEmailSummaryScheduleService(ProfileService profileService, @Qualifi this.emailSummaryService.setScheduleInterval(weekly); } + @EventListener(ApplicationReadyEvent.class) + public void applicationReady() { + // schedule the task after the application has started to avoid delaying the start of the application + scheduler.schedule(this::scheduleEmailSummariesOnStartUp, Instant.now().plusSeconds(EMAIL_SUMMARY_SCHEDULE_DELAY_SEC)); + } + /** * Prepare summary scheduling after server start up */ - @PostConstruct public void scheduleEmailSummariesOnStartUp() { try { if (profileService.isDevActive()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/CacheHandler.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/CacheHandler.java index b7ff26c929e8..78f836fa8c01 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/CacheHandler.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/CacheHandler.java @@ -123,7 +123,7 @@ public C getTransientWriteCacheFor(K key) { public void performCacheWrite(K key, UnaryOperator<C> writeOperation) { cache.lock(key); try { - log.info("Write cache {}", key); + log.debug("Write cache {}", key); cache.set(key, writeOperation.apply(getTransientWriteCacheFor(key))); // We do this get here to deserialize and load the newly written instance into the near cache directly after the writing operation cache.get(key); @@ -147,7 +147,7 @@ public void performCacheWriteIfPresent(K key, UnaryOperator<C> writeOperation) { try { C cached = cache.get(key); if (cached != null) { - log.info("Write cache {}", key); + log.debug("Write cache {}", key); cache.set(key, writeOperation.apply(cached)); // We do this get here to deserialize and load the newly written instance into the near cache directly after the write cache.get(key); diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java index 1c18bac14a6d..e80db8acb835 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizScheduleService.java @@ -1,9 +1,11 @@ package de.tum.in.www1.artemis.service.scheduled.cache.quiz; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.in.www1.artemis.config.StartupDelayConfig.QUIZ_EXERCISE_SCHEDULE_DELAY_SEC; import static de.tum.in.www1.artemis.service.util.TimeLogUtil.formatDurationFrom; import java.time.Duration; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collection; import java.util.HashSet; @@ -23,10 +25,12 @@ import org.hibernate.exception.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; import com.hazelcast.config.Config; @@ -58,10 +62,10 @@ import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.QuizMessagingService; -import de.tum.in.www1.artemis.service.QuizStatisticService; import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.connectors.lti.LtiNewResultService; +import de.tum.in.www1.artemis.service.quiz.QuizMessagingService; +import de.tum.in.www1.artemis.service.quiz.QuizStatisticService; @Profile(PROFILE_CORE) @Service @@ -97,10 +101,14 @@ public class QuizScheduleService { private final TeamRepository teamRepository; + private final TaskScheduler scheduler; + + private static final int SCHEDULE_RATE_PERIOD_SEC = 5; + public QuizScheduleService(WebsocketMessagingService websocketMessagingService, StudentParticipationRepository studentParticipationRepository, UserRepository userRepository, - QuizSubmissionRepository quizSubmissionRepository, HazelcastInstance hazelcastInstance, QuizExerciseRepository quizExerciseRepository, - QuizMessagingService quizMessagingService, QuizStatisticService quizStatisticService, Optional<LtiNewResultService> ltiNewResultService, - TeamRepository teamRepository) { + QuizSubmissionRepository quizSubmissionRepository, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, QuizExerciseRepository quizExerciseRepository, + QuizMessagingService quizMessagingService, QuizStatisticService quizStatisticService, Optional<LtiNewResultService> ltiNewResultService, TeamRepository teamRepository, + @Qualifier("taskScheduler") TaskScheduler scheduler) { this.websocketMessagingService = websocketMessagingService; this.studentParticipationRepository = studentParticipationRepository; this.userRepository = userRepository; @@ -111,6 +119,7 @@ public QuizScheduleService(WebsocketMessagingService websocketMessagingService, this.ltiNewResultService = ltiNewResultService; this.hazelcastInstance = hazelcastInstance; this.teamRepository = teamRepository; + this.scheduler = scheduler; } @PostConstruct @@ -135,7 +144,7 @@ public static void configureHazelcast(Config config) { public void applicationReady() { // activate Quiz Schedule Service SecurityUtils.setAuthorizationObject(); - startSchedule(5 * 1000); // every 5 seconds + scheduler.schedule(this::startSchedule, Instant.now().plusSeconds(QUIZ_EXERCISE_SCHEDULE_DELAY_SEC)); } /** @@ -240,7 +249,7 @@ public QuizExercise getQuizExercise(Long quizExerciseId) { * @param quizExercise should include questions and statistics without Hibernate proxies! */ public void updateQuizExercise(@NotNull QuizExercise quizExercise) { - log.info("updateQuizExercise invoked for {}", quizExercise.getId()); + log.debug("updateQuizExercise invoked for {}", quizExercise.getId()); quizCache.updateQuizExercise(quizExercise); } @@ -257,14 +266,13 @@ public boolean finishedProcessing(Long quizExerciseId) { /** * Start scheduler of quiz schedule service * - * @param delayInMillis gap for which the QuizScheduleService should run repeatedly */ - public void startSchedule(long delayInMillis) { + private void startSchedule() { if (scheduledProcessQuizSubmissions.isNull()) { try { - var scheduledFuture = threadPoolTaskScheduler.scheduleAtFixedRate(new QuizProcessCacheTask(), 0, delayInMillis, TimeUnit.MILLISECONDS); + var scheduledFuture = threadPoolTaskScheduler.scheduleAtFixedRate(new QuizProcessCacheTask(), 0, SCHEDULE_RATE_PERIOD_SEC, TimeUnit.SECONDS); scheduledProcessQuizSubmissions.set(scheduledFuture.getHandler()); - log.info("QuizScheduleService was started to run repeatedly with {} second delay.", delayInMillis / 1000.0); + log.debug("QuizScheduleService was started to run repeatedly with {} second delay.", SCHEDULE_RATE_PERIOD_SEC); } catch (@SuppressWarnings("unused") DuplicateTaskException e) { log.warn("Quiz process cache task already registered"); @@ -273,7 +281,7 @@ public void startSchedule(long delayInMillis) { // schedule quiz start for all existing quizzes that are planned to start in the future List<QuizExercise> quizExercises = quizExerciseRepository.findAllPlannedToStartInTheFuture(); - log.info("Found {} quiz exercises with planned start in the future", quizExercises.size()); + log.debug("Found {} quiz exercises with planned start in the future", quizExercises.size()); for (QuizExercise quizExercise : quizExercises) { if (quizExercise.isCourseExercise()) { // only schedule quiz exercises in courses, not in exams @@ -283,7 +291,7 @@ public void startSchedule(long delayInMillis) { } } else { - log.info("Cannot start quiz exercise schedule service, it is already RUNNING"); + log.debug("Cannot start quiz exercise schedule service, it is already RUNNING"); } } @@ -293,17 +301,17 @@ public void startSchedule(long delayInMillis) { public void stopSchedule() { var savedHandler = scheduledProcessQuizSubmissions.get(); if (savedHandler != null) { - log.info("Try to stop quiz schedule service"); + log.debug("Try to stop quiz schedule service"); var scheduledFuture = threadPoolTaskScheduler.getScheduledFuture(savedHandler); try { // if the task has been disposed, this will throw a StaleTaskException boolean cancelSuccess = scheduledFuture.cancel(false); scheduledFuture.dispose(); scheduledProcessQuizSubmissions.set(null); - log.info("Stop Quiz Schedule Service was successful: {}", cancelSuccess); + log.debug("Stop Quiz Schedule Service was successful: {}", cancelSuccess); } catch (@SuppressWarnings("unused") StaleTaskException e) { - log.info("Stop Quiz Schedule Service already disposed/cancelled"); + log.debug("Stop Quiz Schedule Service already disposed/cancelled"); // has already been disposed (sadly there is no method to check that) } for (QuizExerciseCache quizCache : quizCache.getAllCaches()) { @@ -372,11 +380,11 @@ public void cancelScheduledQuizStart(Long quizExerciseId) { } scheduledFuture.dispose(); if (taskNotDone) { - log.info("Stop scheduled quiz start for quiz {} was successful: {}", quizExerciseId, cancelSuccess); + log.debug("Stop scheduled quiz start for quiz {} was successful: {}", quizExerciseId, cancelSuccess); } } catch (@SuppressWarnings("unused") StaleTaskException e) { - log.info("Stop scheduled quiz start for quiz {} already disposed/cancelled", quizExerciseId); + log.debug("Stop scheduled quiz start for quiz {} already disposed/cancelled", quizExerciseId); // has already been disposed (sadly there is no method to check that) } }); @@ -395,7 +403,7 @@ void executeQuizStartNowTask(Long quizExerciseId) { log.debug("Removed quiz {} start tasks", quizExerciseId); return quizExerciseCache; }); - log.info("Sending quiz {} start", quizExerciseId); + log.debug("Sending quiz {} start", quizExerciseId); QuizExercise quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId); updateQuizExercise(quizExercise); if (quizExercise.getQuizMode() != QuizMode.SYNCHRONIZED) { @@ -456,7 +464,7 @@ public void processCachedQuizSubmissions() { QuizExercise quizExercise = quizExerciseRepository.findOne(quizExerciseId); // check if quiz has been deleted if (quizExercise == null) { - log.info("Remove quiz {} from resultHashMap", quizExerciseId); + log.debug("Remove quiz {} from resultHashMap", quizExerciseId); quizCache.removeAndClear(quizExerciseId); continue; } @@ -507,7 +515,7 @@ public void processCachedQuizSubmissions() { hasNewParticipations = true; hasNewResults = true; - log.info("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle()); + log.debug("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle()); } } @@ -533,7 +541,7 @@ public void processCachedQuizSubmissions() { } }); if (!finishedParticipations.isEmpty()) { - log.info("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle()); + log.debug("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle()); } } @@ -547,7 +555,7 @@ public void processCachedQuizSubmissions() { Set<Result> newResultsForQuiz = Set.copyOf(cache.getResults().values()); // Update the statistics quizStatisticService.updateStatistics(newResultsForQuiz, quizExercise); - log.info("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle()); + log.debug("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle()); // Remove only processed results for (Result result : newResultsForQuiz) { cache.getResults().remove(result.getId()); @@ -694,7 +702,7 @@ else if (quizExercise.isQuizEnded() || quizBatch != null && quizBatch.isEnded()) // this automatically saves the results due to CascadeType.ALL quizSubmission = quizSubmissionRepository.save(quizSubmission); - log.info("Successfully saved submission in quiz {} for user {}", quizExercise.getTitle(), username); + log.debug("Successfully saved submission in quiz {} for user {}", quizExercise.getTitle(), username); // reconnect entities after save participation.setSubmissions(Set.of(quizSubmission)); diff --git a/src/main/java/de/tum/in/www1/artemis/service/tutorialgroups/TutorialGroupService.java b/src/main/java/de/tum/in/www1/artemis/service/tutorialgroups/TutorialGroupService.java index bc2f220cc151..fc349925205b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/tutorialgroups/TutorialGroupService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/tutorialgroups/TutorialGroupService.java @@ -337,8 +337,8 @@ public Set<TutorialGroupRegistrationImportDTO> importRegistrations(Course course // === Step 3: Register all found users to their respective tutorial groups === Map<TutorialGroup, Set<User>> tutorialGroupToRegisteredUsers = new HashMap<>(); for (var registrationUserPair : uniqueRegistrationsWithMatchingUsers.entrySet()) { - assert registrationUserPair.getKey().title() != null; - var tutorialGroup = tutorialGroupTitleToTutorialGroup.get(registrationUserPair.getKey().title().trim()); + String title = Objects.requireNonNull(registrationUserPair.getKey().title()); + var tutorialGroup = tutorialGroupTitleToTutorialGroup.get(title.trim()); var user = registrationUserPair.getValue(); tutorialGroupToRegisteredUsers.computeIfAbsent(tutorialGroup, key -> new HashSet<>()).add(user); } @@ -456,7 +456,9 @@ private Map<TutorialGroupRegistrationImportDTO, User> filterOutWithoutMatchingUs private static Optional<User> getMatchingUser(Set<User> users, TutorialGroupRegistrationImportDTO registration) { return users.stream().filter(user -> { - assert registration.student() != null; // should be the case as we filtered out all registrations without a student + if (registration.student() == null) { + return false; + } boolean hasRegistrationNumber = StringUtils.hasText(registration.student().registrationNumber()); boolean hasLogin = StringUtils.hasText(registration.student().login()); @@ -475,7 +477,9 @@ private Set<User> tryToFindMatchingUsers(Course course, Set<TutorialGroupRegistr var loginsToSearchFor = new HashSet<String>(); for (var registration : registrations) { - assert registration.student() != null; // should be the case as we filtered out all registrations without a student in the calling method + if (registration.student() == null) { + continue; // should not be the case as we filtered out all registrations without a student in the calling method + } boolean hasRegistrationNumber = StringUtils.hasText(registration.student().registrationNumber()); boolean hasLogin = StringUtils.hasText(registration.student().login()); @@ -499,7 +503,6 @@ private Set<User> findUsersByRegistrationNumbers(Set<String> registrationNumbers private Set<User> findUsersByLogins(Set<String> logins, String groupName) { return new HashSet<>(userRepository.findAllWithGroupsByIsDeletedIsFalseAndGroupsContainsAndLoginIn(groupName, logins)); - } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/filter/Lti13LaunchFilter.java b/src/main/java/de/tum/in/www1/artemis/web/filter/Lti13LaunchFilter.java index fe9a2b8580c9..9d699d4e94a9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/filter/Lti13LaunchFilter.java +++ b/src/main/java/de/tum/in/www1/artemis/web/filter/Lti13LaunchFilter.java @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.config.lti.CustomLti13Configurer; import de.tum.in.www1.artemis.domain.lti.Claims; -import de.tum.in.www1.artemis.domain.lti.LtiAuthenticationResponseDTO; +import de.tum.in.www1.artemis.domain.lti.LtiAuthenticationResponse; import de.tum.in.www1.artemis.exception.LtiEmailAlreadyInUseException; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.service.connectors.lti.Lti13Service; @@ -119,7 +119,7 @@ private void writeResponse(String targetLinkUri, OidcIdToken ltiIdToken, String if (SecurityUtils.isAuthenticated()) { lti13Service.buildLtiResponse(uriBuilder, response); } - LtiAuthenticationResponseDTO jsonResponse = new LtiAuthenticationResponseDTO(uriBuilder.build().toUriString(), ltiIdToken.getTokenValue(), clientRegistrationId); + LtiAuthenticationResponse jsonResponse = new LtiAuthenticationResponse(uriBuilder.build().toUriString(), ltiIdToken.getTokenValue(), clientRegistrationId); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AeolusTemplateResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AeolusTemplateResource.java index 9cc332faf691..6fd9e7312ca7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AeolusTemplateResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AeolusTemplateResource.java @@ -64,7 +64,7 @@ public AeolusTemplateResource(AeolusTemplateService aeolusTemplateService, Build * @param testCoverage Whether the test coverage template should be used * @return The requested file, or 404 if the file doesn't exist */ - @GetMapping({ "templates/{language}/{projectType}", "/templates/{language}" }) + @GetMapping({ "templates/{language}/{projectType}", "templates/{language}" }) @EnforceAtLeastEditor public ResponseEntity<String> getAeolusTemplate(@PathVariable ProgrammingLanguage language, @PathVariable Optional<ProjectType> projectType, @RequestParam(value = "staticAnalysis", defaultValue = "false") boolean staticAnalysis, @@ -91,7 +91,7 @@ public ResponseEntity<String> getAeolusTemplate(@PathVariable ProgrammingLanguag * @param testCoverage Whether the test coverage template should be used * @return The requested file, or 404 if the file doesn't exist */ - @GetMapping({ "templateScripts/{language}/{projectType}", "/templateScripts/{language}" }) + @GetMapping({ "templateScripts/{language}/{projectType}", "templateScripts/{language}" }) @EnforceAtLeastEditor public ResponseEntity<String> getAeolusTemplateScript(@PathVariable ProgrammingLanguage language, @PathVariable Optional<ProjectType> projectType, @RequestParam(value = "staticAnalysis", defaultValue = "false") boolean staticAnalysis, diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ApollonDiagramResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ApollonDiagramResource.java index e591923d3e0b..9c3f9277695c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ApollonDiagramResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ApollonDiagramResource.java @@ -118,7 +118,7 @@ public ResponseEntity<ApollonDiagram> updateApollonDiagram(@RequestBody ApollonD * @param diagramId the id of the diagram * @return the ResponseEntity with status 200 (OK) and with body the title of the diagram or 404 Not Found if no diagram with that id exists */ - @GetMapping(value = "/apollon-diagrams/{diagramId}/title") + @GetMapping("apollon-diagrams/{diagramId}/title") @EnforceAtLeastStudent public ResponseEntity<String> getDiagramTitle(@PathVariable Long diagramId) { final var title = apollonDiagramRepository.getDiagramTitle(diagramId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 1a4ffe3fda91..6b7b78747f66 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -106,10 +106,10 @@ public AthenaResource(CourseRepository courseRepository, TextExerciseRepository private interface FeedbackProvider<ExerciseType, SubmissionType, OutputType> { /** - * Method to apply the feedback provider. Examples: AthenaFeedbackSuggestionsService::getTextFeedbackSuggestions, + * Method to apply the (graded) feedback provider. Examples: AthenaFeedbackSuggestionsService::getTextFeedbackSuggestions, * AthenaFeedbackSuggestionsService::getProgrammingFeedbackSuggestions */ - List<OutputType> apply(ExerciseType exercise, SubmissionType submission) throws NetworkingException; + List<OutputType> apply(ExerciseType exercise, SubmissionType submission, Boolean isGraded) throws NetworkingException; } private <ExerciseT extends Exercise, SubmissionT extends Submission, OutputT> ResponseEntity<List<OutputT>> getFeedbackSuggestions(long exerciseId, long submissionId, @@ -128,7 +128,7 @@ private <ExerciseT extends Exercise, SubmissionT extends Submission, OutputT> Re final var submission = submissionFetcher.apply(submissionId); try { - return ResponseEntity.ok(feedbackProvider.apply(exercise, submission)); + return ResponseEntity.ok(feedbackProvider.apply(exercise, submission, true)); } catch (NetworkingException e) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java index d3647d2bea67..506555b97604 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CompetencyResource.java @@ -264,7 +264,7 @@ public ResponseEntity<Competency> createCompetency(@PathVariable long courseId, * @return the ResponseEntity with status 201 (Created) and body the created competencies * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("/courses/{courseId}/competencies/bulk") + @PostMapping("courses/{courseId}/competencies/bulk") @EnforceAtLeastInstructor public ResponseEntity<List<Competency>> createCompetencies(@PathVariable Long courseId, @RequestBody List<Competency> competencies) throws URISyntaxException { log.debug("REST request to create Competencies : {}", competencies); @@ -316,7 +316,7 @@ public ResponseEntity<Competency> importCompetency(@PathVariable long courseId, * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("/courses/{courseId}/competencies/import/bulk") + @PostMapping("courses/{courseId}/competencies/import/bulk") @EnforceAtLeastEditor public ResponseEntity<List<CompetencyWithTailRelationDTO>> importCompetencies(@PathVariable long courseId, @RequestBody List<Competency> competenciesToImport, @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { @@ -349,7 +349,7 @@ public ResponseEntity<List<CompetencyWithTailRelationDTO>> importCompetencies(@P * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies (and relations) * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("/courses/{courseId}/competencies/import-all/{sourceCourseId}") + @PostMapping("courses/{courseId}/competencies/import-all/{sourceCourseId}") @EnforceAtLeastInstructor public ResponseEntity<List<CompetencyWithTailRelationDTO>> importAllCompetenciesFromCourse(@PathVariable long courseId, @PathVariable long sourceCourseId, @RequestParam(defaultValue = "false") boolean importRelations) throws URISyntaxException { @@ -385,7 +385,7 @@ public ResponseEntity<List<CompetencyWithTailRelationDTO>> importAllCompetencies * @return the ResponseEntity with status 201 (Created) and with body containing the imported competencies * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping("/courses/{courseId}/competencies/import-standardized") + @PostMapping("courses/{courseId}/competencies/import-standardized") @EnforceAtLeastEditorInCourse public ResponseEntity<List<CompetencyImportResponseDTO>> importStandardizedCompetencies(@PathVariable long courseId, @RequestBody List<Long> competencyIdsToImport) throws URISyntaxException { @@ -612,7 +612,7 @@ public ResponseEntity<Void> removePrerequisite(@PathVariable long competencyId, * @param courseDescription the text description of the course * @return the ResponseEntity with status 200 (OK) and body the genrated competencies */ - @PostMapping("/courses/{courseId}/competencies/generate-from-description") + @PostMapping("courses/{courseId}/competencies/generate-from-description") @EnforceAtLeastEditor public ResponseEntity<List<Competency>> generateCompetenciesFromCourseDescription(@PathVariable Long courseId, @RequestBody String courseDescription) { var irisService = irisCompetencyGenerationSessionService.orElseThrow(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 472d7add3bdc..b524d6e663e3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -131,6 +131,8 @@ public class CourseResource { private static final Logger log = LoggerFactory.getLogger(CourseResource.class); + private static final int MAX_TITLE_LENGTH = 255; + private final UserRepository userRepository; private final CourseService courseService; @@ -283,6 +285,10 @@ public ResponseEntity<Course> updateCourse(@PathVariable Long courseId, @Request courseUpdate.setTutorialGroupsConfiguration(existingCourse.getTutorialGroupsConfiguration()); courseUpdate.setOnlineCourseConfiguration(existingCourse.getOnlineCourseConfiguration()); + if (courseUpdate.getTitle().length() > MAX_TITLE_LENGTH) { + throw new BadRequestAlertException("The course title is too long", Course.ENTITY_NAME, "courseTitleTooLong"); + } + courseUpdate.validateEnrollmentConfirmationMessage(); courseUpdate.validateComplaintsAndRequestMoreFeedbackConfig(); courseUpdate.validateOnlineCourseAndEnrollmentEnabled(); @@ -873,7 +879,7 @@ public ResponseEntity<List<CourseManagementOverviewStatisticsDTO>> getExerciseSt @FeatureToggle(Feature.Exports) public ResponseEntity<Void> archiveCourse(@PathVariable Long courseId) { log.info("REST request to archive Course : {}", courseId); - final Course course = courseRepository.findByIdWithExercisesAndLecturesElseThrow(courseId); + final Course course = courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); // Archiving a course is only possible after the course is over if (now().isBefore(course.getEndDate())) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ExerciseResource.java index ccadcda18942..b3011cbb4ae8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ExerciseResource.java @@ -47,10 +47,10 @@ import de.tum.in.www1.artemis.service.ExerciseDeletionService; import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.ParticipationService; -import de.tum.in.www1.artemis.service.QuizBatchService; import de.tum.in.www1.artemis.service.TutorParticipationService; import de.tum.in.www1.artemis.service.exam.ExamAccessService; import de.tum.in.www1.artemis.service.exam.ExamDateService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; import de.tum.in.www1.artemis.web.rest.dto.StatsForDashboardDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java index 1ff9d496b894..e92abba0cee5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java @@ -169,7 +169,7 @@ public ResponseEntity<SearchResultPageDTO<Lecture>> getAllLecturesOnPage(SearchT * @param courseId the courseId of the course for which all lectures should be returned * @return the ResponseEntity with status 200 (OK) and the list of lectures in body */ - @GetMapping(value = "/courses/{courseId}/lectures") + @GetMapping("courses/{courseId}/lectures") @EnforceAtLeastEditor public ResponseEntity<Set<Lecture>> getLecturesForCourse(@PathVariable Long courseId, @RequestParam(required = false, defaultValue = "false") boolean withLectureUnits) { log.debug("REST request to get all Lectures for the course with id : {}", courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java index 40873c3cd188..27099eea8e20 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LtiResource.java @@ -5,6 +5,10 @@ import java.util.Set; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.web.bind.annotation.GetMapping; @@ -13,8 +17,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.nimbusds.jwt.SignedJWT; import de.tum.in.www1.artemis.domain.Course; @@ -26,6 +32,8 @@ import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.connectors.lti.LtiDeepLinkingService; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import io.swagger.annotations.ApiParam; +import tech.jhipster.web.util.PaginationUtil; /** * REST controller to handle LTI13 launches. @@ -85,20 +93,22 @@ public ResponseEntity<String> lti13DeepLinking(@PathVariable Long courseId, @Req String targetLink = ltiDeepLinkingService.performDeepLinking(idToken, clientRegistrationId, courseId, exerciseIds); - JsonObject json = new JsonObject(); - json.addProperty("targetLinkUri", targetLink); + ObjectNode json = new ObjectMapper().createObjectNode(); + json.put("targetLinkUri", targetLink); return ResponseEntity.ok(json.toString()); } /** * GET lti platforms : Get all configured lti platforms * + * @param pageable Pageable * @return ResponseEntity containing a list of all lti platforms with status 200 (OK) */ @GetMapping("lti-platforms") @EnforceAtLeastInstructor - public ResponseEntity<List<LtiPlatformConfiguration>> getAllConfiguredLtiPlatforms() { - List<LtiPlatformConfiguration> platforms = ltiPlatformConfigurationRepository.findAll(); - return ResponseEntity.ok(platforms); + public ResponseEntity<List<LtiPlatformConfiguration>> getAllConfiguredLtiPlatforms(@ApiParam Pageable pageable) { + Page<LtiPlatformConfiguration> platformsPage = ltiPlatformConfigurationRepository.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), platformsPage); + return new ResponseEntity<>(platformsPage.getContent(), headers, HttpStatus.OK); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java index d2f62406a86f..5aef7f9eef8b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java @@ -171,7 +171,7 @@ private ResponseEntity<ModelingSubmission> handleModelingSubmission(Long exercis @ResponseStatus(HttpStatus.OK) @ApiResponses({ @ApiResponse(code = 200, message = GET_200_SUBMISSIONS_REASON, response = ModelingSubmission.class, responseContainer = "List"), @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON), }) - @GetMapping(value = "/exercises/{exerciseId}/modeling-submissions") + @GetMapping("exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastTutor public ResponseEntity<List<Submission>> getAllModelingSubmissions(@PathVariable Long exerciseId, @RequestParam(defaultValue = "false") boolean submittedOnly, @RequestParam(defaultValue = "false") boolean assessedByTutor, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { @@ -255,7 +255,7 @@ public ResponseEntity<ModelingSubmission> getModelingSubmission(@PathVariable Lo * @param correctionRound correctionRound for which submissions without a result should be returned * @return the ResponseEntity with status 200 (OK) and a modeling submission without assessment in body */ - @GetMapping(value = "/exercises/{exerciseId}/modeling-submission-without-assessment") + @GetMapping("exercises/{exerciseId}/modeling-submission-without-assessment") @EnforceAtLeastTutor public ResponseEntity<ModelingSubmission> getModelingSubmissionWithoutAssessment(@PathVariable Long exerciseId, @RequestParam(value = "lock", defaultValue = "false") boolean lockSubmission, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java index 6f79f99b250d..4a2665b7f3df 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ParticipationResource.java @@ -50,7 +50,6 @@ import de.tum.in.www1.artemis.domain.Submission; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; @@ -84,15 +83,15 @@ import de.tum.in.www1.artemis.service.GradingScaleService; import de.tum.in.www1.artemis.service.ParticipationAuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationService; -import de.tum.in.www1.artemis.service.QuizBatchService; -import de.tum.in.www1.artemis.service.QuizSubmissionService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.feature.FeatureToggleService; import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; -import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseCodeReviewFeedbackService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; +import de.tum.in.www1.artemis.service.quiz.QuizSubmissionService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -160,12 +159,12 @@ public class ParticipationResource { private final SubmittedAnswerRepository submittedAnswerRepository; - private final GroupNotificationService groupNotificationService; - private final QuizSubmissionService quizSubmissionService; private final GradingScaleService gradingScaleService; + private final ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService; + public ParticipationResource(ParticipationService participationService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, CourseRepository courseRepository, QuizExerciseRepository quizExerciseRepository, ExerciseRepository exerciseRepository, ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, @@ -174,8 +173,8 @@ public ParticipationResource(ParticipationService participationService, Programm GuidedTourConfiguration guidedTourConfiguration, TeamRepository teamRepository, FeatureToggleService featureToggleService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, SubmissionRepository submissionRepository, ResultRepository resultRepository, ExerciseDateService exerciseDateService, InstanceMessageSendService instanceMessageSendService, QuizBatchService quizBatchService, - QuizScheduleService quizScheduleService, SubmittedAnswerRepository submittedAnswerRepository, GroupNotificationService groupNotificationService, - QuizSubmissionService quizSubmissionService, GradingScaleService gradingScaleService) { + QuizScheduleService quizScheduleService, SubmittedAnswerRepository submittedAnswerRepository, QuizSubmissionService quizSubmissionService, + GradingScaleService gradingScaleService, ProgrammingExerciseCodeReviewFeedbackService programmingExerciseCodeReviewFeedbackService) { this.participationService = participationService; this.programmingExerciseParticipationService = programmingExerciseParticipationService; this.quizExerciseRepository = quizExerciseRepository; @@ -199,9 +198,9 @@ public ParticipationResource(ParticipationService participationService, Programm this.quizBatchService = quizBatchService; this.quizScheduleService = quizScheduleService; this.submittedAnswerRepository = submittedAnswerRepository; - this.groupNotificationService = groupNotificationService; this.quizSubmissionService = quizSubmissionService; this.gradingScaleService = gradingScaleService; + this.programmingExerciseCodeReviewFeedbackService = programmingExerciseCodeReviewFeedbackService; } /** @@ -350,7 +349,7 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> resumeParticipati } /** - * PUT exercises/:exerciseId/request-feedback: Requests manual feedback for the latest participation + * PUT exercises/:exerciseId/request-feedback: Requests feedback for the latest participation * * @param exerciseId of the exercise for which to resume participation * @param principal current user principal @@ -362,15 +361,24 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> resumeParticipati public ResponseEntity<ProgrammingExerciseStudentParticipation> requestFeedback(@PathVariable Long exerciseId, Principal principal) { log.debug("REST request for feedback request: {}", exerciseId); var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); + + if (programmingExercise.isExamExercise()) { + throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); + } + + if (programmingExercise.getDueDate() != null && now().isAfter(programmingExercise.getDueDate())) { + throw new BadRequestAlertException("The due date is over", "participation", "preconditions not met"); + } + var participation = programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(programmingExercise, principal.getName()); User user = userRepository.getUserWithGroupsAndAuthorities(); checkAccessPermissionOwner(participation, user); - programmingExercise.validateManualFeedbackSettings(); + programmingExercise.validateSettingsForFeedbackRequest(); - var studentParticipation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); + var studentParticipation = (ProgrammingExerciseStudentParticipation) studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); var result = studentParticipation.findLatestLegalResult(); - if (result == null || result.getScore() < 100) { + if (result == null) { throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); } @@ -380,26 +388,9 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> requestFeedback(@ throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); } - // The participations due date is a flag showing that a feedback request is sent - participation.setIndividualDueDate(currentDate); - - var savedParticipation = programmingExerciseStudentParticipationRepository.save(participation); - // Circumvent lazy loading after save - savedParticipation.setParticipant(participation.getParticipant()); - programmingExerciseParticipationService.lockStudentRepositoryAndParticipation(programmingExercise, savedParticipation); - - // Set all past results to automatic to reset earlier feedback request assessments - var participationResults = studentParticipation.getResults(); - participationResults.forEach(participationResult -> { - participationResult.setAssessmentType(AssessmentType.AUTOMATIC); - participationResult.filterSensitiveInformation(); - participationResult.setRated(false); - }); - resultRepository.saveAll(participationResults); + participation = this.programmingExerciseCodeReviewFeedbackService.handleNonGradedFeedbackRequest(exerciseId, studentParticipation, programmingExercise); - groupNotificationService.notifyTutorGroupAboutNewFeedbackRequest(programmingExercise); - - return ResponseEntity.ok().body(savedParticipation); + return ResponseEntity.ok().body(participation); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/PlantUmlResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/PlantUmlResource.java index b70c71b0b375..0a83e2bbcbed 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/PlantUmlResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/PlantUmlResource.java @@ -20,10 +20,6 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.service.PlantUmlService; -/** - * Created by Josias Montag on 14.12.16. - */ - @Profile(PROFILE_CORE) @RestController @RequestMapping("api/plantuml/") @@ -53,9 +49,7 @@ public ResponseEntity<byte[]> generatePng(@RequestParam("plantuml") String plant final var png = plantUmlService.generatePng(plantuml, useDarkTheme); final var responseHeaders = new HttpHeaders(); responseHeaders.setContentType(MediaType.IMAGE_PNG); - if (log.isInfoEnabled()) { - log.info("PlantUml.generatePng took {}", formatDurationFrom(start)); - } + log.debug("PlantUml.generatePng took {}", formatDurationFrom(start)); return new ResponseEntity<>(png, responseHeaders, HttpStatus.OK); } @@ -73,9 +67,7 @@ public ResponseEntity<String> generateSvg(@RequestParam("plantuml") String plant throws IOException { long start = System.nanoTime(); final var svg = plantUmlService.generateSvg(plantuml, useDarkTheme); - if (log.isInfoEnabled()) { - log.info("PlantUml.generateSvg took {}", formatDurationFrom(start)); - } + log.debug("PlantUml.generateSvg took {}", formatDurationFrom(start)); return new ResponseEntity<>(svg, HttpStatus.OK); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java index c0055638afe5..3c3c7dfa9eba 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java @@ -64,15 +64,15 @@ import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; -import de.tum.in.www1.artemis.service.QuizBatchService; -import de.tum.in.www1.artemis.service.QuizExerciseImportService; -import de.tum.in.www1.artemis.service.QuizExerciseService; -import de.tum.in.www1.artemis.service.QuizMessagingService; -import de.tum.in.www1.artemis.service.QuizStatisticService; import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseImportService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizMessagingService; +import de.tum.in.www1.artemis.service.quiz.QuizStatisticService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.web.rest.dto.QuizBatchJoinDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -179,7 +179,7 @@ public QuizExerciseResource(QuizExerciseService quizExerciseService, QuizMessagi * @return the ResponseEntity with status 201 (Created) and with body the new quizExercise, or with status 400 (Bad Request) if the quizExercise has already an ID * @throws URISyntaxException if the Location URI syntax is incorrect */ - @PostMapping(value = "/quiz-exercises", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "quiz-exercises", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @EnforceAtLeastEditor public ResponseEntity<QuizExercise> createQuizExercise(@RequestPart("exercise") QuizExercise quizExercise, @RequestPart(value = "files", required = false) List<MultipartFile> files) throws URISyntaxException, IOException { @@ -222,7 +222,7 @@ public ResponseEntity<QuizExercise> createQuizExercise(@RequestPart("exercise") * @return the ResponseEntity with status 200 (OK) and with body the updated quizExercise, or with status 400 (Bad Request) if the quizExercise is not valid, or with status 500 * (Internal Server Error) if the quizExercise couldn't be updated */ - @PutMapping(value = "/quiz-exercises/{exerciseId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PutMapping(value = "quiz-exercises/{exerciseId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @EnforceAtLeastEditor public ResponseEntity<QuizExercise> updateQuizExercise(@PathVariable Long exerciseId, @RequestPart("exercise") QuizExercise quizExercise, @RequestPart(value = "files", required = false) List<MultipartFile> files, @RequestParam(value = "notificationText", required = false) String notificationText) @@ -283,7 +283,7 @@ public ResponseEntity<QuizExercise> updateQuizExercise(@PathVariable Long exerci * @param courseId id of the course of which all exercises should be fetched * @return the ResponseEntity with status 200 (OK) and the list of quiz exercises in body */ - @GetMapping(value = "/courses/{courseId}/quiz-exercises") + @GetMapping("courses/{courseId}/quiz-exercises") @EnforceAtLeastTutor public ResponseEntity<List<QuizExercise>> getQuizExercisesForCourse(@PathVariable Long courseId) { log.info("REST request to get all quiz exercises for the course with id : {}", courseId); @@ -640,7 +640,7 @@ public ResponseEntity<Void> deleteQuizExercise(@PathVariable Long quizExerciseId * @return the ResponseEntity with status 200 (OK) and with body the re-evaluated quizExercise, or with status 400 (Bad Request) if the quizExercise is not valid, or with * status 500 (Internal Server Error) if the quizExercise couldn't be re-evaluated */ - @PutMapping(value = "/quiz-exercises/{quizExerciseId}/re-evaluate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PutMapping(value = "quiz-exercises/{quizExerciseId}/re-evaluate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @EnforceAtLeastInstructor public ResponseEntity<QuizExercise> reEvaluateQuizExercise(@PathVariable Long quizExerciseId, @RequestPart("exercise") QuizExercise quizExercise, @RequestPart(value = "files", required = false) List<MultipartFile> files) throws IOException { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java index ef243ecc4f28..e5185ba2ea58 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizPoolResource.java @@ -20,8 +20,8 @@ import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.QuizPoolService; import de.tum.in.www1.artemis.service.exam.ExamAccessService; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java index 2701b80d62d1..6e248ba9be3d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java @@ -37,8 +37,8 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationService; -import de.tum.in.www1.artemis.service.QuizSubmissionService; import de.tum.in.www1.artemis.service.exam.ExamSubmissionService; +import de.tum.in.www1.artemis.service.quiz.QuizSubmissionService; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; import de.tum.in.www1.artemis.web.websocket.ResultWebsocketService; diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index 12e80f4e894f..73133896d839 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -685,7 +685,7 @@ public ResponseEntity<Void> deleteTestRun(@PathVariable Long courseId, @PathVari * @param examId the exam to which the student exam belongs to * @return ResponseEntity containing the list of generated participations */ - @PostMapping(value = "/courses/{courseId}/exams/{examId}/student-exams/start-exercises") + @PostMapping(value = "courses/{courseId}/exams/{examId}/student-exams/start-exercises") @EnforceAtLeastInstructor public ResponseEntity<Void> startExercises(@PathVariable Long courseId, @PathVariable Long examId) { long start = System.nanoTime(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TeamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TeamResource.java index 6d816ccdce3b..e9c4433b35c9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TeamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TeamResource.java @@ -444,7 +444,7 @@ public ResponseEntity<List<Team>> importTeamsFromSourceExercise(@PathVariable lo * @param teamShortName short name of the team (all teams with the short name in the course are seen as the same team) * @return Course with exercises and participations (and latest submissions) for the team */ - @GetMapping(value = "/courses/{courseId}/teams/{teamShortName}/with-exercises-and-participations") + @GetMapping("courses/{courseId}/teams/{teamShortName}/with-exercises-and-participations") @EnforceAtLeastStudent public ResponseEntity<Course> getCourseWithExercisesAndParticipationsForTeam(@PathVariable Long courseId, @PathVariable String teamShortName) { log.debug("REST request to get Course {} with exercises and participations for Team with short name {}", courseId, teamShortName); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java index 22afc6bab93d..6c0317c88e13 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java @@ -187,7 +187,7 @@ public ResponseEntity<TextSubmission> getTextSubmissionWithResults(@PathVariable * @param assessedByTutor mark if only assessed Submissions should be returned * @return the ResponseEntity with status 200 (OK) and the list of textSubmissions in body */ - @GetMapping(value = "/exercises/{exerciseId}/text-submissions") + @GetMapping("exercises/{exerciseId}/text-submissions") @EnforceAtLeastTutor public ResponseEntity<List<Submission>> getAllTextSubmissions(@PathVariable Long exerciseId, @RequestParam(defaultValue = "false") boolean submittedOnly, @RequestParam(defaultValue = "false") boolean assessedByTutor, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { @@ -205,7 +205,7 @@ public ResponseEntity<List<Submission>> getAllTextSubmissions(@PathVariable Long * @param lockSubmission optional value to define if the submission should be locked and has the value of false if not set manually * @return the ResponseEntity with status 200 (OK) and the list of textSubmissions in body */ - @GetMapping(value = "/exercises/{exerciseId}/text-submission-without-assessment") + @GetMapping("exercises/{exerciseId}/text-submission-without-assessment") @EnforceAtLeastTutor public ResponseEntity<TextSubmission> getTextSubmissionWithoutAssessment(@PathVariable Long exerciseId, @RequestParam(value = "head", defaultValue = "false") boolean skipAssessmentOrderOptimization, diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminBuildJobQueueResource.java index adc8ff8347f5..b18bfbc7ded2 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminBuildJobQueueResource.java @@ -77,11 +77,25 @@ public ResponseEntity<List<LocalCIBuildJobQueueItem>> getRunningBuildJobs() { */ @GetMapping("build-agents") @EnforceAdmin - public ResponseEntity<List<LocalCIBuildAgentInformation>> getBuildAgentInformation() { + public ResponseEntity<List<LocalCIBuildAgentInformation>> getBuildAgentSummary() { log.debug("REST request to get information on available build agents"); - List<LocalCIBuildAgentInformation> buildAgentInfo = localCIBuildJobQueueService.getBuildAgentInformation(); - // TODO: convert into a proper DTO and strip unnecessary information, e.g. build config, because it's not shown in the client and contains too much information - return ResponseEntity.ok(buildAgentInfo); + List<LocalCIBuildAgentInformation> buildAgentSummary = localCIBuildJobQueueService.getBuildAgentInformationWithoutRecentBuildJobs(); + return ResponseEntity.ok(buildAgentSummary); + } + + /** + * Returns detailed information on a specific build agent + * + * @param agentName the name of the agent + * @return the build agent information + */ + @GetMapping("build-agent") + @EnforceAdmin + public ResponseEntity<LocalCIBuildAgentInformation> getBuildAgentDetails(@RequestParam String agentName) { + log.debug("REST request to get information on build agent {}", agentName); + LocalCIBuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() + .orElse(null); + return ResponseEntity.ok(buildAgentDetails); } /** @@ -136,7 +150,7 @@ public ResponseEntity<Void> cancelAllRunningBuildJobs() { * @param agentName the name of the agent * @return the ResponseEntity with the result of the cancellation */ - @DeleteMapping("/cancel-all-running-jobs-for-agent") + @DeleteMapping("cancel-all-running-jobs-for-agent") @EnforceAdmin public ResponseEntity<Void> cancelAllRunningBuildJobsForAgent(@RequestParam String agentName) { log.debug("REST request to cancel all running build jobs for agent {}", agentName); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java index ec82babb5301..8fcd97fdec90 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminCourseResource.java @@ -54,6 +54,8 @@ public class AdminCourseResource { private static final Logger log = LoggerFactory.getLogger(AdminCourseResource.class); + private static final int MAX_TITLE_LENGTH = 255; + @Value("${jhipster.clientApp.name}") private String applicationName; @@ -118,6 +120,10 @@ public ResponseEntity<Course> createCourse(@RequestPart Course course, @RequestP throw new BadRequestAlertException("A new course cannot already have an ID", Course.ENTITY_NAME, "idExists"); } + if (course.getTitle().length() > MAX_TITLE_LENGTH) { + throw new BadRequestAlertException("The course title is too long", Course.ENTITY_NAME, "courseTitleTooLong"); + } + course.validateShortName(); List<Course> coursesWithSameShortName = courseRepository.findAllByShortName(course.getShortName()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminLtiConfigurationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminLtiConfigurationResource.java index 706e9715f775..0d7d7a54b1f1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminLtiConfigurationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AdminLtiConfigurationResource.java @@ -144,7 +144,7 @@ public ResponseEntity<Void> addLtiPlatformConfiguration(@RequestBody LtiPlatform * @param registrationToken Optional token for the registration process. * @return a {@link ResponseEntity} with status 200 (OK) if the dynamic registration process was successful. */ - @PostMapping("/lti13/dynamic-registration") + @PostMapping("lti13/dynamic-registration") @EnforceAdmin public ResponseEntity<Void> lti13DynamicRegistration(@RequestParam(name = "openid_configuration") String openIdConfiguration, @RequestParam(name = "registration_token", required = false) String registrationToken) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AuditResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AuditResource.java index 37e1f6f9f8e5..a98cc05df7b3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AuditResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/AuditResource.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.web.rest.admin; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static tech.jhipster.web.util.PaginationUtil.generatePaginationHttpHeaders; import java.time.Instant; import java.time.LocalDate; @@ -24,7 +25,6 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAdmin; import de.tum.in.www1.artemis.service.AuditEventService; import io.swagger.annotations.ApiParam; -import tech.jhipster.web.util.PaginationUtil; import tech.jhipster.web.util.ResponseUtil; /** @@ -51,7 +51,7 @@ public AuditResource(AuditEventService auditEventService) { @EnforceAdmin public ResponseEntity<List<AuditEvent>> getAll(@ApiParam Pageable pageable) { Page<AuditEvent> page = auditEventService.findAll(pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); } @@ -72,7 +72,7 @@ public ResponseEntity<List<AuditEvent>> getByDates(@RequestParam(value = "fromDa Instant to = toDate.atStartOfDay(ZoneId.systemDefault()).plusDays(1).toInstant(); Page<AuditEvent> page = auditEventService.findByDates(from, to, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisChatSessionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisChatSessionResource.java index 6bf6c805c482..77ebf0035c6d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisChatSessionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisChatSessionResource.java @@ -20,9 +20,10 @@ import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisHealthIndicator; import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSettingsDTO; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; @@ -38,8 +39,8 @@ public class IrisChatSessionResource extends IrisExerciseChatBasedSessionResourc protected IrisChatSessionResource(AuthorizationCheckService authCheckService, IrisChatSessionRepository irisChatSessionRepository, UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, IrisSessionService irisSessionService, IrisSettingsService irisSettingsService, - IrisRateLimitService irisRateLimitService) { - super(authCheckService, userRepository, irisSessionService, irisSettingsService, irisRateLimitService, programmingExerciseRepository::findByIdElseThrow); + PyrisHealthIndicator pyrisHealthIndicator, IrisRateLimitService irisRateLimitService) { + super(authCheckService, userRepository, irisSessionService, irisSettingsService, pyrisHealthIndicator, irisRateLimitService, programmingExerciseRepository); this.irisChatSessionRepository = irisChatSessionRepository; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisExerciseChatBasedSessionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisExerciseChatBasedSessionResource.java index af6944370db7..fec600737e65 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisExerciseChatBasedSessionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisExerciseChatBasedSessionResource.java @@ -2,10 +2,12 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; +import org.springframework.boot.actuate.health.Status; import org.springframework.http.ResponseEntity; import de.tum.in.www1.artemis.domain.Exercise; @@ -13,13 +15,16 @@ import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSubSettingsInterface; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisHealthIndicator; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisHealthStatusDTO; import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSubSettingsInterface; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; @@ -36,22 +41,26 @@ public abstract class IrisExerciseChatBasedSessionResource<E extends Exercise, S protected final IrisSettingsService irisSettingsService; + protected final PyrisHealthIndicator pyrisHealthIndicator; + protected final IrisRateLimitService irisRateLimitService; - protected final Function<Long, E> exerciseByIdFunction; + protected final ProgrammingExerciseRepository programmingExerciseRepository; protected IrisExerciseChatBasedSessionResource(AuthorizationCheckService authCheckService, UserRepository userRepository, IrisSessionService irisSessionService, - IrisSettingsService irisSettingsService, IrisRateLimitService irisRateLimitService, Function<Long, E> exerciseByIdFunction) { + IrisSettingsService irisSettingsService, PyrisHealthIndicator pyrisHealthIndicator, IrisRateLimitService irisRateLimitService, + ProgrammingExerciseRepository programmingExerciseRepository) { this.authCheckService = authCheckService; this.userRepository = userRepository; this.irisSessionService = irisSessionService; this.irisSettingsService = irisSettingsService; + this.pyrisHealthIndicator = pyrisHealthIndicator; this.irisRateLimitService = irisRateLimitService; - this.exerciseByIdFunction = exerciseByIdFunction; + this.programmingExerciseRepository = programmingExerciseRepository; } protected ResponseEntity<S> getCurrentSession(Long exerciseId, IrisSubSettingsType subSettingsType, Role role, BiFunction<Exercise, User, S> sessionsFunction) { - var exercise = exerciseByIdFunction.apply(exerciseId); + var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); irisSettingsService.isEnabledForElseThrow(subSettingsType, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(role, exercise, user); @@ -62,7 +71,7 @@ protected ResponseEntity<S> getCurrentSession(Long exerciseId, IrisSubSettingsTy } protected ResponseEntity<List<S>> getAllSessions(Long exerciseId, IrisSubSettingsType subSettingsType, Role role, BiFunction<Exercise, User, List<S>> sessionsFunction) { - var exercise = exerciseByIdFunction.apply(exerciseId); + var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); irisSettingsService.isEnabledForElseThrow(subSettingsType, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(role, exercise, user); @@ -74,7 +83,7 @@ protected ResponseEntity<List<S>> getAllSessions(Long exerciseId, IrisSubSetting protected ResponseEntity<S> createSessionForExercise(Long exerciseId, IrisSubSettingsType subSettingsType, Role role, BiFunction<Exercise, User, S> sessionsFunction) throws URISyntaxException { - var exercise = exerciseByIdFunction.apply(exerciseId); + var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); if (exercise.isExamExercise()) { throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); } @@ -92,6 +101,14 @@ protected IrisHealthDTO isIrisActiveInternal(E exercise, S session, Function<Iri var user = userRepository.getUser(); irisSessionService.checkHasAccessToIrisSession(session, user); irisSessionService.checkIsIrisActivated(session); + var settings = irisSettingsService.getCombinedIrisSettingsFor(exercise, false); + var health = pyrisHealthIndicator.health(); + PyrisHealthStatusDTO[] modelStatuses = (PyrisHealthStatusDTO[]) health.getDetails().get("modelStatuses"); + var specificModelStatus = false; + if (modelStatuses != null) { + specificModelStatus = Arrays.stream(modelStatuses).filter(x -> x.model().equals(subSettingsFunction.apply(settings).preferredModel())) + .anyMatch(x -> x.status() == PyrisHealthStatusDTO.ModelStatus.UP); + } IrisRateLimitService.IrisRateLimitInformation rateLimitInfo = null; @@ -100,7 +117,7 @@ protected IrisHealthDTO isIrisActiveInternal(E exercise, S session, Function<Iri rateLimitInfo = irisRateLimitService.getRateLimitInformation(user); } - return new IrisHealthDTO(true, rateLimitInfo); + return new IrisHealthDTO(health.getStatus() == Status.UP, rateLimitInfo); } public record IrisHealthDTO(boolean active, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisModelsResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisModelsResource.java index e1c2326273f3..781975c1aa9b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisModelsResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisModelsResource.java @@ -9,9 +9,9 @@ import org.springframework.web.bind.annotation.RestController; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorException; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisModelDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorException; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorService; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisModelDTO; import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** @@ -22,10 +22,10 @@ @RequestMapping("api/") public class IrisModelsResource { - private final IrisConnectorService irisConnectorService; + private final PyrisConnectorService pyrisConnectorService; - public IrisModelsResource(IrisConnectorService irisConnectorService) { - this.irisConnectorService = irisConnectorService; + public IrisModelsResource(PyrisConnectorService pyrisConnectorService) { + this.pyrisConnectorService = pyrisConnectorService; } /** @@ -35,12 +35,12 @@ public IrisModelsResource(IrisConnectorService irisConnectorService) { */ @GetMapping("iris/models") @EnforceAtLeastEditor - public ResponseEntity<List<IrisModelDTO>> getAllModels() { + public ResponseEntity<List<PyrisModelDTO>> getAllModels() { try { - var models = irisConnectorService.getOfferedModels(); + var models = pyrisConnectorService.getOfferedModels(); return ResponseEntity.ok(models); } - catch (IrisConnectorException e) { + catch (PyrisConnectorException e) { throw new InternalServerErrorException("Could not fetch available Iris models"); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java index 366cf7f1da9f..d718718e7fb6 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSettingsDTO; import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/dto/IrisTutorChatStatusUpdateDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/dto/IrisTutorChatStatusUpdateDTO.java new file mode 100644 index 000000000000..d89aab49e5de --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/dto/IrisTutorChatStatusUpdateDTO.java @@ -0,0 +1,10 @@ +package de.tum.in.www1.artemis.web.rest.iris.dto; + +import java.util.List; + +import de.tum.in.www1.artemis.domain.iris.message.IrisMessage; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; + +public record IrisTutorChatStatusUpdateDTO(IrisMessage result, List<PyrisStageDTO> stages) { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/localci/BuildJobQueueResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/localci/BuildJobQueueResource.java index 60c499270359..26cf705211b9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/localci/BuildJobQueueResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/localci/BuildJobQueueResource.java @@ -159,7 +159,7 @@ public ResponseEntity<Void> cancelAllRunningBuildJobs(@PathVariable long courseI * @param search the search criteria * @return the page of finished build jobs */ - @GetMapping("/courses/{courseId}/finished-jobs") + @GetMapping("courses/{courseId}/finished-jobs") @EnforceAtLeastInstructorInCourse public ResponseEntity<List<FinishedBuildJobDTO>> getFinishedBuildJobsForCourse(@PathVariable long courseId, PageableSearchDTO<String> search) { log.debug("REST request to get the finished build jobs for course {}", courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ChannelResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ChannelResource.java index d22a8a4437e4..b5bff2de727d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ChannelResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ChannelResource.java @@ -55,7 +55,7 @@ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/courses") +@RequestMapping("api/courses/") public class ChannelResource extends ConversationManagementResource { private static final Logger log = LoggerFactory.getLogger(ChannelResource.class); @@ -281,9 +281,9 @@ public ResponseEntity<Void> deleteChannel(@PathVariable Long courseId, @PathVari var requestingUser = userRepository.getUserWithGroupsAndAuthorities(); channelAuthorizationService.isAllowedToDeleteChannel(channel, requestingUser); - tutorialGroupChannelManagementService.getTutorialGroupBelongingToChannel(channel).ifPresentOrElse(tutorialGroup -> { + tutorialGroupChannelManagementService.getTutorialGroupBelongingToChannel(channel).ifPresent(tutorialGroup -> { throw new BadRequestAlertException("The channel belongs to tutorial group " + tutorialGroup.getTitle(), CHANNEL_ENTITY_NAME, "channel.tutorialGroup.mismatch"); - }, Optional::empty); + }); var usersToNotify = conversationParticipantRepository.findConversationParticipantsByConversationId(channel.getId()).stream().map(ConversationParticipant::getUser) .collect(Collectors.toSet()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java index 2954f4627e5d..7072079ba2bc 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/ConversationResource.java @@ -47,7 +47,7 @@ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/courses") +@RequestMapping("api/courses/") public class ConversationResource extends ConversationManagementResource { private static final Logger log = LoggerFactory.getLogger(ConversationResource.class); @@ -135,7 +135,7 @@ public ResponseEntity<Void> updateIsHidden(@PathVariable Long courseId, @PathVar * @param isMuted the new muted status * @return ResponseEntity with status 200 (Ok) */ - @PostMapping("/{courseId}/conversations/{conversationId}/muted") + @PostMapping("{courseId}/conversations/{conversationId}/muted") @EnforceAtLeastStudent public ResponseEntity<Void> updateIsMuted(@PathVariable Long courseId, @PathVariable Long conversationId, @RequestParam boolean isMuted) { checkMessagingOrCommunicationEnabledElseThrow(courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/GroupChatResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/GroupChatResource.java index 260267d7cc3c..a26d3ceb4546 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/GroupChatResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/GroupChatResource.java @@ -38,7 +38,7 @@ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/courses") +@RequestMapping("api/courses/") public class GroupChatResource extends ConversationManagementResource { private static final Logger log = LoggerFactory.getLogger(GroupChatResource.class); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/OneToOneChatResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/OneToOneChatResource.java index 0e4e8ba6eee0..cd34a2276e9d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/OneToOneChatResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/metis/conversation/OneToOneChatResource.java @@ -32,7 +32,7 @@ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/courses") +@RequestMapping("api/courses/") public class OneToOneChatResource extends ConversationManagementResource { private static final Logger log = LoggerFactory.getLogger(OneToOneChatResource.class); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicOAuth2JWKSResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicOAuth2JWKSResource.java index 2a9ca6594835..50e8ac086307 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicOAuth2JWKSResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicOAuth2JWKSResource.java @@ -1,12 +1,16 @@ package de.tum.in.www1.artemis.web.rest.open; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.google.gson.GsonBuilder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import de.tum.in.www1.artemis.security.OAuth2JWKSService; import de.tum.in.www1.artemis.security.annotations.EnforceNothing; @@ -20,17 +24,30 @@ @RestController public class PublicOAuth2JWKSResource { + private static final Logger log = LoggerFactory.getLogger(PublicOAuth2JWKSResource.class); + private final OAuth2JWKSService jwksService; public PublicOAuth2JWKSResource(OAuth2JWKSService jwksService) { this.jwksService = jwksService; } - @GetMapping("/.well-known/jwks.json") + /** + * GET JWKS: Retrieves the JSON Web Key Set (JWKS). + * + * @return ResponseEntity containing the JWKS as a JSON string with status 200 (OK). If an error occurs, returns null. + */ + @GetMapping(".well-known/jwks.json") @EnforceNothing @ManualConfig public ResponseEntity<String> getJwkSet() { - String keysAsJson = new GsonBuilder().setPrettyPrinting().create().toJson(jwksService.getJwkSet().toPublicJWKSet().toJSONObject()); + String keysAsJson = null; + try { + keysAsJson = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).writeValueAsString(jwksService.getJwkSet().toPublicJWKSet().toJSONObject()); + } + catch (JsonProcessingException exception) { + log.debug("Error occurred parsing jwkSet: {}", exception.getMessage()); + } return new ResponseEntity<>(keysAsJson, HttpStatus.OK); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicPyrisStatusUpdateResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicPyrisStatusUpdateResource.java new file mode 100644 index 000000000000..bb064451e45b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/open/PublicPyrisStatusUpdateResource.java @@ -0,0 +1,68 @@ +package de.tum.in.www1.artemis.web.rest.open; + +import java.util.Objects; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.in.www1.artemis.security.annotations.EnforceNothing; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisJobService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisStatusUpdateService; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatStatusUpdateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.job.TutorChatJob; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; +import de.tum.in.www1.artemis.web.rest.errors.ConflictException; + +/** + * REST controller for providing Pyris access to Artemis internal data and status updates. + * All endpoints in this controller use custom token based authentication. + * See {@link PyrisJobService#getAndAuthenticateJobFromHeaderElseThrow(HttpServletRequest)} for more information. + */ +@RestController +@Profile("iris") +@RequestMapping("api/public/pyris/pipelines/") +public class PublicPyrisStatusUpdateResource { + + private final PyrisJobService pyrisJobService; + + private final PyrisStatusUpdateService pyrisStatusUpdateService; + + public PublicPyrisStatusUpdateResource(PyrisJobService pyrisJobService, PyrisStatusUpdateService pyrisStatusUpdateService) { + this.pyrisJobService = pyrisJobService; + this.pyrisStatusUpdateService = pyrisStatusUpdateService; + } + + /** + * {@code POST /api/public/pyris/pipelines/tutor-chat/runs/{runId}/status} : Set the status of a tutor chat job. + * Uses custom token based authentication. + * + * @param runId the ID of the job + * @param statusUpdateDTO the status update + * @param request the HTTP request + * @throws ConflictException if the run ID in the URL does not match the run ID in the request body + * @throws AccessForbiddenException if the token is invalid + * @return a {@link ResponseEntity} with status {@code 200 (OK)} + */ + @PostMapping("tutor-chat/runs/{runId}/status") + @EnforceNothing + public ResponseEntity<Void> setStatusOfJob(@PathVariable String runId, @RequestBody PyrisTutorChatStatusUpdateDTO statusUpdateDTO, HttpServletRequest request) { + var job = pyrisJobService.getAndAuthenticateJobFromHeaderElseThrow(request); + if (!Objects.equals(job.jobId(), runId)) { + throw new ConflictException("Run ID in URL does not match run ID in request body", "Job", "runIdMismatch"); + } + if (!(job instanceof TutorChatJob tutorChatJob)) { + throw new ConflictException("Run ID is not a tutor chat job", "Job", "invalidRunId"); + } + + pyrisStatusUpdateService.handleStatusUpdate(tutorChatJob, statusUpdateDTO); + + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java index cf69133f2385..6e0e1bd3a1b1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java @@ -191,7 +191,7 @@ public ResponseEntity<ProgrammingExercise> importProgrammingExercise(@PathVariab log.debug("REST request to import programming exercise {} into course {}", sourceExerciseId, newExercise.getCourseViaExerciseGroupOrCourseMember().getId()); newExercise.validateGeneralSettings(); newExercise.validateProgrammingSettings(); - newExercise.validateManualFeedbackSettings(); + newExercise.validateSettingsForFeedbackRequest(); validateStaticCodeAnalysisSettings(newExercise); final var user = userRepository.getUserWithGroupsAndAuthorities(); @@ -529,4 +529,32 @@ public ResponseEntity<Resource> exportStudentRequestedRepository(@PathVariable l return returnZipFileForRepositoryExport(zipFile, RepositoryType.SOLUTION.getName(), programmingExercise, start); } + + /** + * GET /programming-exercises/:exerciseId/export-student-repository/:participationId : Exports the repository belonging to a participation as a zip file. + * + * @param exerciseId The id of the programming exercise + * @param participationId The id of the student participation for which to export the repository. + * @return A ResponseEntity containing the zipped repository. + * @throws IOException If the repository could not be zipped. + */ + @GetMapping("programming-exercises/{exerciseId}/export-student-repository/{participationId}") + @EnforceAtLeastStudent + @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + public ResponseEntity<Resource> exportStudentRepository(@PathVariable long exerciseId, @PathVariable long participationId) throws IOException { + var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); + var studentParticipation = programmingExercise.getStudentParticipations().stream().filter(p -> p.getId().equals(participationId)) + .map(p -> (ProgrammingExerciseStudentParticipation) p).findFirst() + .orElseThrow(() -> new EntityNotFoundException("No student participation with id " + participationId + " was found for programming exercise " + exerciseId)); + if (!authCheckService.isOwnerOfParticipation(studentParticipation)) { + authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, programmingExercise, null); + } + List<String> exportErrors = new ArrayList<>(); + long start = System.nanoTime(); + Optional<File> zipFile = programmingExerciseExportService.exportStudentRepository(exerciseId, studentParticipation, exportErrors); + if (zipFile.isEmpty()) { + throw new InternalServerErrorException("Could not export the student repository of participation " + participationId + ". Logged errors: " + exportErrors); + } + return returnZipFileForRepositoryExport(zipFile, RepositoryType.USER.getName(), programmingExercise, start); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseParticipationResource.java index 8ad5fad5b923..9a1495d05cb5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseParticipationResource.java @@ -111,7 +111,7 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> getParticipationW * @param participationId for which to retrieve the student participation with all results and feedbacks. * @return the ResponseEntity with status 200 (OK) and the participation with all results and feedbacks in the body. */ - @GetMapping("/programming-exercise-participations/{participationId}/student-participation-with-all-results") + @GetMapping("programming-exercise-participations/{participationId}/student-participation-with-all-results") @EnforceAtLeastStudent public ResponseEntity<ProgrammingExerciseStudentParticipation> getParticipationWithAllResultsForStudentParticipation(@PathVariable Long participationId) { ProgrammingExerciseStudentParticipation participation = programmingExerciseStudentParticipationRepository.findByIdWithAllResultsAndRelatedSubmissions(participationId) @@ -132,7 +132,7 @@ public ResponseEntity<ProgrammingExerciseStudentParticipation> getParticipationW * @return the ResponseEntity with status 200 (OK) and the latest result with feedbacks in its body, 404 if the participation can't be found or 403 if the user is not allowed * to access the participation. */ - @GetMapping(value = "/programming-exercise-participations/{participationId}/latest-result-with-feedbacks") + @GetMapping("programming-exercise-participations/{participationId}/latest-result-with-feedbacks") @EnforceAtLeastStudent public ResponseEntity<Result> getLatestResultWithFeedbacksForProgrammingExerciseParticipation(@PathVariable Long participationId, @RequestParam(defaultValue = "false") boolean withSubmission) { @@ -150,7 +150,7 @@ public ResponseEntity<Result> getLatestResultWithFeedbacksForProgrammingExercise * @param participationId of the participation to check. * @return the ResponseEntity with status 200 (OK) with true if there is a result, otherwise false. */ - @GetMapping(value = "/programming-exercise-participations/{participationId}/has-result") + @GetMapping("programming-exercise-participations/{participationId}/has-result") @EnforceAtLeastStudent public ResponseEntity<Boolean> checkIfParticipationHashResult(@PathVariable Long participationId) { boolean hasResult = resultRepository.existsByParticipationId(participationId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java index 197240601673..6a3ac819ecdb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java @@ -669,7 +669,7 @@ public ResponseEntity<SearchResultPageDTO<ProgrammingExercise>> getAllExercisesO * @param programmingLanguage Filters for only exercises with this language * @return The desired page, sorted and matching the given query */ - @GetMapping("/programming-exercises/with-sca") + @GetMapping("programming-exercises/with-sca") @EnforceAtLeastEditor public ResponseEntity<SearchResultPageDTO<ProgrammingExercise>> getAllExercisesWithSCAOnPage(SearchTermPageableSearchDTO<String> search, @RequestParam(defaultValue = "true") boolean isCourseFilter, @RequestParam(defaultValue = "true") boolean isExamFilter, diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingSubmissionResource.java index f8f706388000..cf2d6a96553c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingSubmissionResource.java @@ -384,7 +384,7 @@ public ResponseEntity<ProgrammingSubmission> getProgrammingSubmissionWithoutAsse // TODO Check if submission has newly created manual result for this and endpoint and endpoint above ProgrammingSubmission submission; - if (programmingExercise.getAllowManualFeedbackRequests() && programmingExercise.getDueDate() != null && programmingExercise.getDueDate().isAfter(ZonedDateTime.now())) { + if (programmingExercise.getAllowFeedbackRequests() && programmingExercise.getDueDate() != null && programmingExercise.getDueDate().isAfter(ZonedDateTime.now())) { // Assess manual feedback request before the due date submission = programmingSubmissionService.getNextAssessableSubmission(programmingExercise, programmingExercise.isExamExercise(), correctionRound).orElse(null); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/push_notification/PushNotificationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/push_notification/PushNotificationResource.java index 33ddf6dc6b8e..d3e6a655d884 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/push_notification/PushNotificationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/push_notification/PushNotificationResource.java @@ -39,7 +39,7 @@ */ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/push_notification") +@RequestMapping("api/push_notification/") public class PushNotificationResource { private static final Logger log = LoggerFactory.getLogger(PushNotificationResource.class); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java index 1683d1078d14..0aaa218aeaf3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/RepositoryProgrammingExerciseParticipationResource.java @@ -185,7 +185,7 @@ else if (participation instanceof ProgrammingExerciseStudentParticipation studen } @Override - @GetMapping(value = "/repository/{participationId}/files", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/files", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<Map<String, FileType>> getFiles(@PathVariable Long participationId) { return super.getFiles(participationId); @@ -197,7 +197,7 @@ public ResponseEntity<Map<String, FileType>> getFiles(@PathVariable Long partici * @param participationId the participationId of the repository we want to get the files from * @return a map with the file path as key and the file type as value */ - @GetMapping(value = "/repository/{participationId}/files-plagiarism-view", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/files-plagiarism-view", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<Map<String, FileType>> getFilesForPlagiarismView(@PathVariable Long participationId) { log.debug("REST request to files for plagiarism view for domainId : {}", participationId); @@ -218,7 +218,7 @@ public ResponseEntity<Map<String, FileType>> getFilesForPlagiarismView(@PathVari * @param repositoryType the type of the repository (template, solution, tests) * @return a map with the file path as key and the file content as value */ - @GetMapping(value = "/repository/{participationId}/files-content/{commitId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/files-content/{commitId}", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<Map<String, String>> getFilesAtCommit(@PathVariable long participationId, @PathVariable String commitId, @RequestAttribute(required = false) RepositoryType repositoryType) { @@ -251,7 +251,7 @@ public ResponseEntity<Map<String, String>> getFilesAtCommit(@PathVariable long p * @param participationId participation of the student * @return the ResponseEntity with status 200 (OK) and a map of files with the information if they were changed/are new. */ - @GetMapping(value = "/repository/{participationId}/files-change", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/files-change", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<Map<String, Boolean>> getFilesWithInformationAboutChange(@PathVariable Long participationId) { return super.executeAndCheckForExceptions(() -> { @@ -266,7 +266,7 @@ public ResponseEntity<Map<String, Boolean>> getFilesWithInformationAboutChange(@ } @Override - @GetMapping(value = "/repository/{participationId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @GetMapping(value = "repository/{participationId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @EnforceAtLeastStudent public ResponseEntity<byte[]> getFile(@PathVariable Long participationId, @RequestParam("file") String filename) { return super.getFile(participationId, filename); @@ -279,7 +279,7 @@ public ResponseEntity<byte[]> getFile(@PathVariable Long participationId, @Reque * @param filename the name of the file to retrieve * @return the file with the given filename */ - @GetMapping(value = "/repository/{participationId}/file-plagiarism-view", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @GetMapping(value = "repository/{participationId}/file-plagiarism-view", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @EnforceAtLeastStudent public ResponseEntity<byte[]> getFileForPlagiarismView(@PathVariable Long participationId, @RequestParam("file") String filename) { log.debug("REST request to file {} for plagiarism view for domainId : {}", filename, participationId); @@ -298,7 +298,7 @@ public ResponseEntity<byte[]> getFileForPlagiarismView(@PathVariable Long partic * @param participationId participation of the student/template/solution * @return the ResponseEntity with status 200 (OK) and a map of files with their content */ - @GetMapping(value = "/repository/{participationId}/files-content", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/files-content", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<Map<String, String>> getFilesWithContent(@PathVariable Long participationId) { return super.executeAndCheckForExceptions(() -> { @@ -314,7 +314,7 @@ public ResponseEntity<Map<String, String>> getFilesWithContent(@PathVariable Lon * @param participationId participation of the student/template/solution * @return the ResponseEntity with status 200 (OK) and a set of file names */ - @GetMapping(value = "/repository/{participationId}/file-names", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/file-names", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<Set<String>> getFileNames(@PathVariable Long participationId) { return super.executeAndCheckForExceptions(() -> { @@ -327,7 +327,7 @@ public ResponseEntity<Set<String>> getFileNames(@PathVariable Long participation } @Override - @PostMapping(value = "/repository/{participationId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "repository/{participationId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @FeatureToggle(Feature.ProgrammingExercises) @EnforceAtLeastStudent public ResponseEntity<Void> createFile(@PathVariable Long participationId, @RequestParam("file") String filePath, HttpServletRequest request) { @@ -335,7 +335,7 @@ public ResponseEntity<Void> createFile(@PathVariable Long participationId, @Requ } @Override - @PostMapping(value = "/repository/{participationId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "repository/{participationId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) @FeatureToggle(Feature.ProgrammingExercises) @EnforceAtLeastStudent public ResponseEntity<Void> createFolder(@PathVariable Long participationId, @RequestParam("folder") String folderPath, HttpServletRequest request) { @@ -343,7 +343,7 @@ public ResponseEntity<Void> createFolder(@PathVariable Long participationId, @Re } @Override - @PostMapping(value = "/repository/{participationId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "repository/{participationId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) @FeatureToggle(Feature.ProgrammingExercises) @EnforceAtLeastStudent public ResponseEntity<Void> renameFile(@PathVariable Long participationId, @RequestBody FileMove fileMove) { @@ -351,14 +351,14 @@ public ResponseEntity<Void> renameFile(@PathVariable Long participationId, @Requ } @Override - @DeleteMapping(value = "/repository/{participationId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @DeleteMapping(value = "repository/{participationId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<Void> deleteFile(@PathVariable Long participationId, @RequestParam("file") String filename) { return super.deleteFile(participationId, filename); } @Override - @GetMapping(value = "/repository/{participationId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<Void> pullChanges(@PathVariable Long participationId) { return super.pullChanges(participationId); @@ -372,7 +372,7 @@ public ResponseEntity<Void> pullChanges(@PathVariable Long participationId) { * @param commit whether to commit after updating the files * @return {Map<String, String>} file submissions or the appropriate http error */ - @PutMapping(value = "/repository/{participationId}/files") + @PutMapping("repository/{participationId}/files") @EnforceAtLeastStudent public ResponseEntity<Map<String, String>> updateParticipationFiles(@PathVariable("participationId") Long participationId, @RequestBody List<FileSubmission> submissions, @RequestParam(defaultValue = "false") boolean commit) { @@ -427,7 +427,7 @@ public ResponseEntity<Map<String, String>> updateParticipationFiles(@PathVariabl * participation OR the buildAndTestAfterDueDate is set and the repository is now locked. */ @Override - @PostMapping(value = "/repository/{participationId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "repository/{participationId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) @FeatureToggle(Feature.ProgrammingExercises) @EnforceAtLeastStudent public ResponseEntity<Void> commitChanges(@PathVariable Long participationId) { @@ -435,7 +435,7 @@ public ResponseEntity<Void> commitChanges(@PathVariable Long participationId) { } @Override - @PostMapping(value = "/repository/{participationId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "repository/{participationId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) @FeatureToggle(Feature.ProgrammingExercises) @EnforceAtLeastStudent public ResponseEntity<Void> resetToLastCommit(@PathVariable Long participationId) { @@ -443,7 +443,7 @@ public ResponseEntity<Void> resetToLastCommit(@PathVariable Long participationId } @Override - @GetMapping(value = "/repository/{participationId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<RepositoryStatusDTO> getStatus(@PathVariable Long participationId) throws GitAPIException { return super.getStatus(participationId); @@ -458,7 +458,7 @@ public ResponseEntity<RepositoryStatusDTO> getStatus(@PathVariable Long particip * @return the ResponseEntity with status 200 (OK) and with body the result, or with status 404 (Not Found) */ // TODO: rename to participation/{participationId}/buildlogs - @GetMapping(value = "/repository/{participationId}/buildlogs", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "repository/{participationId}/buildlogs", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastStudent public ResponseEntity<List<BuildLogEntry>> getBuildLogs(@PathVariable Long participationId, @RequestParam(name = "resultId") Optional<Long> resultId) { log.debug("REST request to get build log : {}", participationId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java index 8c96c47cfa7f..6340a1679951 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java @@ -97,21 +97,21 @@ String getOrRetrieveBranchOfDomainObject(Long exerciseId) { } @Override - @GetMapping(value = "/test-repository/{exerciseId}/files", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "test-repository/{exerciseId}/files", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<Map<String, FileType>> getFiles(@PathVariable Long exerciseId) { return super.getFiles(exerciseId); } @Override - @GetMapping(value = "/test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @GetMapping(value = "test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @EnforceAtLeastTutor public ResponseEntity<byte[]> getFile(@PathVariable Long exerciseId, @RequestParam("file") String filename) { return super.getFile(exerciseId, filename); } @Override - @PostMapping(value = "/test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> createFile(@PathVariable Long exerciseId, @RequestParam("file") String filePath, HttpServletRequest request) { @@ -119,7 +119,7 @@ public ResponseEntity<Void> createFile(@PathVariable Long exerciseId, @RequestPa } @Override - @PostMapping(value = "/test-repository/{exerciseId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "test-repository/{exerciseId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> createFolder(@PathVariable Long exerciseId, @RequestParam("folder") String folderPath, HttpServletRequest request) { @@ -127,7 +127,7 @@ public ResponseEntity<Void> createFolder(@PathVariable Long exerciseId, @Request } @Override - @PostMapping(value = "/test-repository/{exerciseId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "test-repository/{exerciseId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> renameFile(@PathVariable Long exerciseId, @RequestBody FileMove fileMove) { @@ -135,7 +135,7 @@ public ResponseEntity<Void> renameFile(@PathVariable Long exerciseId, @RequestBo } @Override - @DeleteMapping(value = "/test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @DeleteMapping(value = "test-repository/{exerciseId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> deleteFile(@PathVariable Long exerciseId, @RequestParam("file") String filename) { @@ -143,14 +143,14 @@ public ResponseEntity<Void> deleteFile(@PathVariable Long exerciseId, @RequestPa } @Override - @GetMapping(value = "/test-repository/{exerciseId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "test-repository/{exerciseId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<Void> pullChanges(@PathVariable Long exerciseId) { return super.pullChanges(exerciseId); } @Override - @PostMapping(value = "/test-repository/{exerciseId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "test-repository/{exerciseId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> commitChanges(@PathVariable Long exerciseId) { @@ -158,7 +158,7 @@ public ResponseEntity<Void> commitChanges(@PathVariable Long exerciseId) { } @Override - @PostMapping(value = "/test-repository/{exerciseId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "test-repository/{exerciseId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity<Void> resetToLastCommit(@PathVariable Long exerciseId) { @@ -166,7 +166,7 @@ public ResponseEntity<Void> resetToLastCommit(@PathVariable Long exerciseId) { } @Override - @GetMapping(value = "/test-repository/{exerciseId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "test-repository/{exerciseId}", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity<RepositoryStatusDTO> getStatus(@PathVariable Long exerciseId) throws GitAPIException { return super.getStatus(exerciseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java index 493838c35777..34c3adbddda0 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/QuizSubmissionWebsocketService.java @@ -15,8 +15,8 @@ import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exception.QuizSubmissionException; import de.tum.in.www1.artemis.security.SecurityUtils; -import de.tum.in.www1.artemis.service.QuizSubmissionService; import de.tum.in.www1.artemis.service.WebsocketMessagingService; +import de.tum.in.www1.artemis.service.quiz.QuizSubmissionService; @Controller @Profile(PROFILE_CORE) diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIWebsocketMessagingService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIWebsocketMessagingService.java index 2d294cf61ff2..368d4ae65abf 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIWebsocketMessagingService.java +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/localci/LocalCIWebsocketMessagingService.java @@ -90,13 +90,19 @@ public void sendRunningBuildJobs(List<LocalCIBuildJobQueueItem> buildJobQueue) { * * @param buildAgentInfo the build agent information */ - public void sendBuildAgentInformation(List<LocalCIBuildAgentInformation> buildAgentInfo) { + public void sendBuildAgentSummary(List<LocalCIBuildAgentInformation> buildAgentInfo) { String channel = "/topic/admin/build-agents"; log.debug("Sending message on topic {}: {}", channel, buildAgentInfo); // TODO: convert into a proper DTO and strip unnecessary information, e.g. build config, because it's not shown in the client and contains too much information websocketMessagingService.sendMessage(channel, buildAgentInfo); } + public void sendBuildAgentDetails(LocalCIBuildAgentInformation buildAgentDetails) { + String channel = "/topic/admin/build-agent/" + buildAgentDetails.name(); + log.debug("Sending message on topic {}: {}", channel, buildAgentDetails); + websocketMessagingService.sendMessage(channel, buildAgentDetails); + } + /** * Checks if the given destination is a build queue admin destination. * This is the case if the destination is either /topic/admin/queued-jobs or /topic/admin/running-jobs. diff --git a/src/main/java/de/tum/in/www1/artemis/web/websocket/team/ParticipationTeamWebsocketService.java b/src/main/java/de/tum/in/www1/artemis/web/websocket/team/ParticipationTeamWebsocketService.java index 294b52b5d7ad..119109a30bb1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/websocket/team/ParticipationTeamWebsocketService.java +++ b/src/main/java/de/tum/in/www1/artemis/web/websocket/team/ParticipationTeamWebsocketService.java @@ -14,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.context.event.EventListener; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -81,7 +82,7 @@ public class ParticipationTeamWebsocketService { public ParticipationTeamWebsocketService(WebsocketMessagingService websocketMessagingService, SimpUserRegistry simpUserRegistry, UserRepository userRepository, StudentParticipationRepository studentParticipationRepository, ExerciseRepository exerciseRepository, TextSubmissionService textSubmissionService, - ModelingSubmissionService modelingSubmissionService, HazelcastInstance hazelcastInstance) { + ModelingSubmissionService modelingSubmissionService, @Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance) { this.websocketMessagingService = websocketMessagingService; this.simpUserRegistry = simpUserRegistry; this.userRepository = userRepository; diff --git a/src/main/resources/config/liquibase/changelog/20240515204415_changelog.xml b/src/main/resources/config/liquibase/changelog/20240515204415_changelog.xml new file mode 100644 index 000000000000..75daf359837e --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240515204415_changelog.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + <changeSet id="20240515204415" author="krusche"> + <createIndex indexName="idx_build_job_docker_image_build_start_date" tableName="build_job"> + <column name="docker_image"/> + <column name="build_start_date"/> + </createIndex> + </changeSet> +</databaseChangeLog> diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 4711d210aa25..1c43ec819509 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -8,6 +8,7 @@ <include file="classpath:config/liquibase/changelog/20230628215302_changelog.xml" relativeToChangelogFile="false"/> <include file="classpath:config/liquibase/changelog/20240412170323_changelog.xml" relativeToChangelogFile="false"/> <include file="classpath:config/liquibase/changelog/20240418204415_changelog.xml" relativeToChangelogFile="false"/> + <include file="classpath:config/liquibase/changelog/20240515204415_changelog.xml" relativeToChangelogFile="false"/> <!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! --> <!-- we should also stay in a chronological order! --> <!-- you can use the command 'date '+%Y%m%d%H%M%S'' to get the current date and time in the correct format --> diff --git a/src/main/resources/templates/aeolus/ocaml/default.yaml b/src/main/resources/templates/aeolus/ocaml/default.yaml index 7d7dc4cbdbf8..df5aef046d3a 100644 --- a/src/main/resources/templates/aeolus/ocaml/default.yaml +++ b/src/main/resources/templates/aeolus/ocaml/default.yaml @@ -12,6 +12,6 @@ actions: runAlways: true results: - name: junit - path: test-reports/results.xml + path: 'test-reports/*.xml' type: junit before: true diff --git a/src/main/webapp/app/admin/admin.module.ts b/src/main/webapp/app/admin/admin.module.ts index 22149c9e8a3d..569fb75f221e 100644 --- a/src/main/webapp/app/admin/admin.module.ts +++ b/src/main/webapp/app/admin/admin.module.ts @@ -33,7 +33,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { ReactiveFormsModule } from '@angular/forms'; import { LtiConfigurationComponent } from 'app/admin/lti-configuration/lti-configuration.component'; import { EditLtiConfigurationComponent } from 'app/admin/lti-configuration/edit-lti-configuration.component'; -import { BuildAgentsComponent } from 'app/localci/build-agents/build-agents.component'; +import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; import { StandardizedCompetencyEditComponent } from 'app/admin/standardized-competencies/standardized-competency-edit.component'; import { StandardizedCompetencyManagementComponent } from 'app/admin/standardized-competencies/standardized-competency-management.component'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; @@ -41,6 +41,7 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { ArtemisCompetenciesModule } from 'app/course/competencies/competency.module'; import { UserImportModule } from 'app/shared/user-import/user-import.module'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; +import { BuildAgentDetailsComponent } from 'app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component'; import { KnowledgeAreaEditComponent } from 'app/admin/standardized-competencies/knowledge-area-edit.component'; import { AdminImportStandardizedCompetenciesComponent } from 'app/admin/standardized-competencies/import/admin-import-standardized-competencies.component'; import { ArtemisStandardizedCompetencyModule } from 'app/shared/standardized-competencies/standardized-competency.module'; @@ -90,7 +91,8 @@ const ENTITY_STATES = [...adminState]; OrganizationManagementUpdateComponent, LtiConfigurationComponent, EditLtiConfigurationComponent, - BuildAgentsComponent, + BuildAgentSummaryComponent, + BuildAgentDetailsComponent, StandardizedCompetencyEditComponent, KnowledgeAreaEditComponent, StandardizedCompetencyManagementComponent, diff --git a/src/main/webapp/app/admin/admin.route.ts b/src/main/webapp/app/admin/admin.route.ts index 8adaee8b1682..0c2099494d4a 100644 --- a/src/main/webapp/app/admin/admin.route.ts +++ b/src/main/webapp/app/admin/admin.route.ts @@ -16,8 +16,9 @@ import { MetricsComponent } from 'app/admin/metrics/metrics.component'; import { BuildQueueComponent } from 'app/localci/build-queue/build-queue.component'; import { LocalCIGuard } from 'app/localci/localci-guard.service'; import { ltiConfigurationRoute } from 'app/admin/lti-configuration/lti-configuration.route'; -import { BuildAgentsComponent } from 'app/localci/build-agents/build-agents.component'; +import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; import { StandardizedCompetencyManagementComponent } from 'app/admin/standardized-competencies/standardized-competency-management.component'; +import { BuildAgentDetailsComponent } from 'app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component'; import { AdminImportStandardizedCompetenciesComponent } from 'app/admin/standardized-competencies/import/admin-import-standardized-competencies.component'; export const adminState: Routes = [ @@ -95,7 +96,15 @@ export const adminState: Routes = [ }, { path: 'build-agents', - component: BuildAgentsComponent, + component: BuildAgentSummaryComponent, + data: { + pageTitle: 'artemisApp.buildAgents.title', + }, + canActivate: [LocalCIGuard], + }, + { + path: 'build-agents/details', + component: BuildAgentDetailsComponent, data: { pageTitle: 'artemisApp.buildAgents.title', }, diff --git a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html index 049da31270b1..6e026c952a34 100644 --- a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html +++ b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.html @@ -105,6 +105,23 @@ <h4 class="modal-title" style="margin-right: 2px">{{ 'artemisApp.lti.configuredP </tbody> </table> </div> + <div> + <div class="row justify-content-center"> + <jhi-item-count [params]="{ page: page, totalItems: totalItems, itemsPerPage: itemsPerPage }" /> + </div> + <div class="row justify-content-center"> + <ngb-pagination + [collectionSize]="totalItems" + [(page)]="page" + [pageSize]="itemsPerPage" + [maxSize]="5" + [rotate]="true" + [boundaryLinks]="true" + (pageChange)="transition()" + [disabled]="false" + /> + </div> + </div> </div> </div> </ng-template> diff --git a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.ts b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.ts index f0e0f1f86869..2d978673adbc 100644 --- a/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.ts +++ b/src/main/webapp/app/admin/lti-configuration/lti-configuration.component.ts @@ -1,14 +1,16 @@ import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Course } from 'app/entities/course.model'; import { faExclamationTriangle, faPencilAlt, faPlus, faSort, faTrash, faWrench } from '@fortawesome/free-solid-svg-icons'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; import { LtiConfigurationService } from 'app/admin/lti-configuration/lti-configuration.service'; import { SortService } from 'app/shared/service/sort.service'; import { Subject } from 'rxjs'; -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { LTI_URLS } from 'app/admin/lti-configuration/lti-configuration.urls'; +import { ITEMS_PER_PAGE } from 'app/shared/constants/pagination.constants'; +import { combineLatest } from 'rxjs'; @Component({ selector: 'jhi-lti-configuration', @@ -17,12 +19,16 @@ import { LTI_URLS } from 'app/admin/lti-configuration/lti-configuration.urls'; export class LtiConfigurationComponent implements OnInit { course: Course; platforms: LtiPlatformConfiguration[]; - + ascending!: boolean; activeTab = 1; - predicate = 'id'; reverse = false; + // page information + page = 1; + itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + // Icons faSort = faSort; faExclamationTriangle = faExclamationTriangle; @@ -39,16 +45,39 @@ export class LtiConfigurationComponent implements OnInit { private ltiConfigurationService: LtiConfigurationService, private sortService: SortService, private alertService: AlertService, + private activatedRoute: ActivatedRoute, ) {} /** * Gets the configuration for the course encoded in the route and fetches the exercises */ - ngOnInit() { - this.ltiConfigurationService.findAll().subscribe((configuredLtiPlatforms) => { - if (configuredLtiPlatforms) { - this.platforms = configuredLtiPlatforms; - } + ngOnInit(): void { + combineLatest({ data: this.activatedRoute.data, params: this.activatedRoute.queryParamMap }).subscribe(({ data, params }) => { + const page = params.get('page'); + this.page = page !== null ? +page : 1; + const sort = (params.get('sort') ?? data['defaultSort']).split(','); + this.predicate = sort[0]; + this.ascending = sort[1] === 'asc'; + this.loadData(); + }); + } + + loadData(): void { + this.ltiConfigurationService + .query({ + page: this.page - 1, + size: this.itemsPerPage, + sort: this.sort(), + }) + .subscribe((res: HttpResponse<LtiPlatformConfiguration[]>) => this.onSuccess(res.body, res.headers)); + } + + transition(): void { + this.router.navigate(['/admin/lti-configuration'], { + queryParams: { + page: this.page, + sort: this.predicate + ',' + (this.ascending ? 'asc' : 'desc'), + }, }); } @@ -120,4 +149,17 @@ export class LtiConfigurationComponent implements OnInit { }, }); } + + private sort(): string[] { + const result = [this.predicate + ',' + (this.ascending ? 'asc' : 'desc')]; + if (this.predicate !== 'id') { + result.push('id'); + } + return result; + } + + private onSuccess(platforms: LtiPlatformConfiguration[] | null, headers: HttpHeaders): void { + this.totalItems = Number(headers.get('X-Total-Count')); + this.platforms = platforms || []; + } } diff --git a/src/main/webapp/app/admin/lti-configuration/lti-configuration.route.ts b/src/main/webapp/app/admin/lti-configuration/lti-configuration.route.ts index 0fd2354ca669..aa663034e8bf 100644 --- a/src/main/webapp/app/admin/lti-configuration/lti-configuration.route.ts +++ b/src/main/webapp/app/admin/lti-configuration/lti-configuration.route.ts @@ -10,6 +10,7 @@ export const ltiConfigurationRoute: Routes = [ component: LtiConfigurationComponent, data: { pageTitle: 'global.menu.admin.lti', + defaultSort: 'id,desc', }, }, { diff --git a/src/main/webapp/app/admin/lti-configuration/lti-configuration.service.ts b/src/main/webapp/app/admin/lti-configuration/lti-configuration.service.ts index 2760f4e4336d..c6484edd768f 100644 --- a/src/main/webapp/app/admin/lti-configuration/lti-configuration.service.ts +++ b/src/main/webapp/app/admin/lti-configuration/lti-configuration.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; +import { createRequestOption } from 'app/shared/util/request.util'; @Injectable({ providedIn: 'root' }) export class LtiConfigurationService { @@ -10,8 +11,12 @@ export class LtiConfigurationService { /** * Sends a GET request to retrieve all lti platform configurations */ - findAll(): Observable<LtiPlatformConfiguration[]> { - return this.http.get<LtiPlatformConfiguration[]>('api/lti-platforms'); + query(req?: any): Observable<HttpResponse<LtiPlatformConfiguration[]>> { + const params: HttpParams = createRequestOption(req); + return this.http.get<LtiPlatformConfiguration[]>('api/lti-platforms', { + params, + observe: 'response', + }); } /** diff --git a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.html b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.html index bc40307eec65..1a3a842ec0f2 100644 --- a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.html +++ b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.html @@ -22,19 +22,26 @@ <h4 class="mt-3" jhiTranslate="artemisApp.standardizedCompetency.manage.import.p jhiTranslate="artemisApp.standardizedCompetency.manage.import.count" [translateValues]="{ competencies: importCount.competencies, knowledgeAreas: importCount.knowledgeAreas }" ></div> - <jhi-knowledge-area-tree [dataSource]="dataSource" [treeControl]="treeControl"> - <ng-template let-competency="competency" #competencyTemplate> - <div class="d-flex align-items-center"> - <fa-icon - class="me-2" - [icon]="getIcon(competency.taxonomy)" - [ngbTooltip]="'artemisApp.competency.taxonomies.' + (competency.taxonomy ?? 'none') | artemisTranslate" - [fixedWidth]="true" - /> - <h6 class="mb-0">{{ competency.title }}</h6> + <div class="d-flex"> + <jhi-knowledge-area-tree class="d-flex flex-grow-1 h-100 w-50" [dataSource]="dataSource" [treeControl]="treeControl"> + <ng-template let-competency="competency" let-knowledgeArea="knowledgeArea" #competencyTemplate> + <div class="d-flex align-items-center clickable" (click)="openCompetencyDetails(competency, knowledgeArea.title)"> + <fa-icon + class="me-2" + [icon]="getIcon(competency.taxonomy)" + [ngbTooltip]="'artemisApp.competency.taxonomies.' + (competency.taxonomy ?? 'none') | artemisTranslate" + [fixedWidth]="true" + /> + <h6 class="mb-0">{{ competency.title }}</h6> + </div> + </ng-template> + </jhi-knowledge-area-tree> + @if (selectedCompetency) { + <div style="background-color: var(--overview-light-background-color)" class="card d-flex flex-grow-1 w-100 h-100 p-3 ms-1"> + <jhi-standardized-competency-detail [competency]="selectedCompetency" [knowledgeAreaTitle]="knowledgeAreaTitle" (onClose)="closeCompetencyDetails()" /> </div> - </ng-template> - </jhi-knowledge-area-tree> + } + </div> } @else { <span jhiTranslate="artemisApp.standardizedCompetency.manage.import.preview.empty"></span> } diff --git a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts index 2e8e9b5c4100..48f4da19bf5d 100644 --- a/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts +++ b/src/main/webapp/app/admin/standardized-competencies/import/admin-import-standardized-competencies.component.ts @@ -1,6 +1,12 @@ import { Component } from '@angular/core'; import { faBan, faChevronRight, faFileImport, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; -import { KnowledgeAreaDTO, KnowledgeAreaForTree, KnowledgeAreasForImportDTO, convertToKnowledgeAreaForTree } from 'app/entities/competency/standardized-competency.model'; +import { + KnowledgeAreaDTO, + KnowledgeAreaForTree, + KnowledgeAreasForImportDTO, + StandardizedCompetencyForTree, + convertToKnowledgeAreaForTree, +} from 'app/entities/competency/standardized-competency.model'; import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants'; import { AlertService } from 'app/core/util/alert.service'; import { AdminStandardizedCompetencyService } from 'app/admin/standardized-competencies/admin-standardized-competency.service'; @@ -25,6 +31,9 @@ interface ImportCount { export class AdminImportStandardizedCompetenciesComponent { protected isLoading = false; protected isCollapsed = false; + protected selectedCompetency?: StandardizedCompetencyForTree; + //the title of the knowledge area belonging to the selected competency + protected knowledgeAreaTitle = ''; protected importData?: KnowledgeAreasForImportDTO; protected importCount?: ImportCount; protected dataSource = new MatTreeNestedDataSource<KnowledgeAreaForTree>(); @@ -101,6 +110,16 @@ export class AdminImportStandardizedCompetenciesComponent { } } + protected openCompetencyDetails(competency: StandardizedCompetencyForTree, knowledgeAreaTitle: string) { + this.knowledgeAreaTitle = knowledgeAreaTitle; + this.selectedCompetency = competency; + } + + protected closeCompetencyDetails() { + this.knowledgeAreaTitle = ''; + this.selectedCompetency = undefined; + } + importCompetencies() { this.isLoading = true; this.adminStandardizedCompetencyService.importCompetencies(this.importData!).subscribe({ diff --git a/src/main/webapp/app/app-routing.module.ts b/src/main/webapp/app/app-routing.module.ts index 0fcd6d526bab..9e488b66cec5 100644 --- a/src/main/webapp/app/app-routing.module.ts +++ b/src/main/webapp/app/app-routing.module.ts @@ -36,10 +36,6 @@ const LAYOUT_ROUTES: Routes = [navbarRoute, ...errorRoute]; path: 'courses/:courseId/competencies/:competencyId', loadChildren: () => import('./overview/course-competencies/course-competencies-details.module').then((m) => m.ArtemisCourseCompetenciesDetailsModule), }, - { - path: 'courses/:courseId/tutorial-groups/:tutorialGroupId', - loadChildren: () => import('./overview/tutorial-group-details/course-tutorial-group-details.module').then((m) => m.CourseTutorialGroupDetailsModule), - }, // ===== TEAM ==== { path: 'course-management/:courseId/exercises/:exerciseId/teams', diff --git a/src/main/webapp/app/assessment/assessment-warning/assessment-warning.component.ts b/src/main/webapp/app/assessment/assessment-warning/assessment-warning.component.ts index 710a5bc61db7..aa21e99239c3 100644 --- a/src/main/webapp/app/assessment/assessment-warning/assessment-warning.component.ts +++ b/src/main/webapp/app/assessment/assessment-warning/assessment-warning.component.ts @@ -44,7 +44,7 @@ export class AssessmentWarningComponent implements OnChanges { if (this.exercise.dueDate) { const now = dayjs(); this.isBeforeExerciseDueDate = now.isBefore(this.exercise.dueDate); - this.showWarning = now.isBefore(this.getLatestDueDate()) && !this.exercise.allowManualFeedbackRequests; + this.showWarning = now.isBefore(this.getLatestDueDate()) && !this.exercise.allowFeedbackRequests; } } diff --git a/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.html b/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.html index c30f880de55d..85a65e3bf46f 100644 --- a/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.html +++ b/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.html @@ -59,6 +59,23 @@ <h4 class="modal-title mt-4" id="lti13" jhiTranslate="artemisApp.lti.version13"> {{ getLtiPlatform(platform) }} </button> } + <div> + <div class="row justify-content-center"> + <jhi-item-count [params]="{ page: page, totalItems: totalItems, itemsPerPage: itemsPerPage }" /> + </div> + <div class="row justify-content-center"> + <ngb-pagination + [collectionSize]="totalItems" + [(page)]="page" + [pageSize]="itemsPerPage" + [maxSize]="5" + [rotate]="true" + [boundaryLinks]="true" + (pageChange)="transition()" + [disabled]="false" + /> + </div> + </div> </div> </div> </div> diff --git a/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.ts b/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.ts index 3558960edb19..ed940d582462 100644 --- a/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.ts +++ b/src/main/webapp/app/course/manage/course-lti-configuration/edit-course-lti-configuration.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Course } from 'app/entities/course.model'; import { finalize } from 'rxjs'; @@ -10,18 +10,28 @@ import { LOGIN_PATTERN } from 'app/shared/constants/input.constants'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; import { LtiConfigurationService } from 'app/admin/lti-configuration/lti-configuration.service'; +import { ITEMS_PER_PAGE } from 'app/shared/constants/pagination.constants'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { combineLatest } from 'rxjs'; @Component({ selector: 'jhi-edit-course-lti-configuration', templateUrl: './edit-course-lti-configuration.component.html', }) export class EditCourseLtiConfigurationComponent implements OnInit { + @ViewChild('scrollableContent') scrollableContent: ElementRef; + course: Course; onlineCourseConfiguration: OnlineCourseConfiguration; onlineCourseConfigurationForm: FormGroup; - ltiConfiguredPlatforms: LtiPlatformConfiguration[]; + ltiConfiguredPlatforms: LtiPlatformConfiguration[] = []; + + page = 1; + itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; isSaving = false; + loading = false; // Icons faBan = faBan; @@ -52,7 +62,34 @@ export class EditCourseLtiConfigurationComponent implements OnInit { ltiPlatformConfiguration: new FormControl(''), }); - this.getPreconfiguredPlatforms(); + this.loadInitialPlatforms(); + } + + loadInitialPlatforms() { + combineLatest({ data: this.route.data, params: this.route.queryParamMap }).subscribe(({ params }) => { + const page = params.get('page'); + this.page = page !== null ? +page : 1; + this.loadData(); + }); + } + + loadData(): void { + this.ltiConfigurationService + .query({ + page: this.page - 1, + size: this.itemsPerPage, + sort: ['id', 'asc'], + }) + .subscribe((res: HttpResponse<LtiPlatformConfiguration[]>) => this.onSuccess(res.body, res.headers)); + } + + transition(): void { + this.router.navigate(['/admin/lti-configuration'], { + queryParams: { + page: this.page, + sort: ['id', 'asc'], + }, + }); } /** @@ -81,6 +118,10 @@ export class EditCourseLtiConfigurationComponent implements OnInit { this.navigateToLtiConfigurationPage(); } + private onSuccess(platforms: LtiPlatformConfiguration[] | null, headers: HttpHeaders): void { + this.totalItems = Number(headers.get('X-Total-Count')); + this.ltiConfiguredPlatforms = platforms || []; + } /** * Gets the user prefix */ @@ -95,17 +136,6 @@ export class EditCourseLtiConfigurationComponent implements OnInit { this.router.navigate(['course-management', this.course.id!.toString(), 'lti-configuration']); } - /** - * Gets the LTI 1.3 pre-configured platforms - */ - getPreconfiguredPlatforms() { - this.ltiConfigurationService.findAll().subscribe((configuredLtiPlatforms) => { - if (configuredLtiPlatforms) { - this.ltiConfiguredPlatforms = configuredLtiPlatforms; - } - }); - } - setPlatform(platform: LtiPlatformConfiguration) { this.onlineCourseConfiguration.ltiPlatformConfiguration = platform; this.onlineCourseConfigurationForm.get('ltiPlatformConfiguration')?.setValue(platform); diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index e993e20a9ac6..a7b73fe7a72a 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -339,7 +339,7 @@ <h5> ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" /> </div> - @if (messagingEnabled) { + @if (messagingEnabled || communicationEnabled) { <div class="form-group"> <label class="form-control-label" jhiTranslate="artemisApp.codeOfConduct.title"></label> <jhi-help-icon text="artemisApp.codeOfConduct.tooltip" /> diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index c6aafea3e7ec..c39a1b5cdbbc 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -85,6 +85,8 @@ export class CourseUpdateComponent implements OnInit { readonly COMPLAINT_RESPONSE_TEXT_LIMIT = 65535; readonly COMPLAINT_TEXT_LIMIT = 65535; + readonly COURSE_TITLE_LIMIT = 255; + constructor( private eventManager: EventManager, private courseManagementService: CourseManagementService, @@ -162,7 +164,10 @@ export class CourseUpdateComponent implements OnInit { this.courseForm = new FormGroup( { id: new FormControl(this.course.id), - title: new FormControl(this.course.title, [Validators.required]), + title: new FormControl(this.course.title, { + validators: [Validators.required, Validators.maxLength(this.COURSE_TITLE_LIMIT)], + updateOn: 'blur', + }), shortName: new FormControl( { value: this.course.shortName, disabled: !!this.course.id }, { diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.scss b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.scss index 82a16ec6cbd7..e4a25e0c952d 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.scss +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.scss @@ -7,3 +7,7 @@ .section-detail-list:not(:last-child) { border-bottom: 1px solid var(--bs-border-color); } + +.diff-view-modal .modal-dialog { + max-width: 80vw; +} 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 497aae17a47e..8bdf8e132958 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,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { faArrowUpRightFromSquare, faCodeBranch, faExclamationTriangle, faEye } from '@fortawesome/free-solid-svg-icons'; import { isEmpty } from 'lodash-es'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; @@ -43,6 +43,7 @@ export enum DetailType { selector: 'jhi-detail-overview-list', templateUrl: './detail-overview-list.component.html', styleUrls: ['./detail-overview-list.component.scss'], + encapsulation: ViewEncapsulation.None, }) export class DetailOverviewListComponent implements OnInit, OnDestroy { protected readonly isEmpty = isEmpty; @@ -96,7 +97,7 @@ export class DetailOverviewListComponent implements OnInit, OnDestroy { return; } - const modalRef = this.modalService.open(GitDiffReportModalComponent, { size: 'xl' }); + const modalRef = this.modalService.open(GitDiffReportModalComponent, { windowClass: 'diff-view-modal' }); modalRef.componentInstance.report = gitDiff; } diff --git a/src/main/webapp/app/entities/assessment-type.model.ts b/src/main/webapp/app/entities/assessment-type.model.ts index d7f0eb1763f4..0644fe653cde 100644 --- a/src/main/webapp/app/entities/assessment-type.model.ts +++ b/src/main/webapp/app/entities/assessment-type.model.ts @@ -2,4 +2,5 @@ export enum AssessmentType { AUTOMATIC = 'AUTOMATIC', SEMI_AUTOMATIC = 'SEMI_AUTOMATIC', MANUAL = 'MANUAL', + AUTOMATIC_ATHENA = 'AUTOMATIC_ATHENA', } diff --git a/src/main/webapp/app/entities/exercise.model.ts b/src/main/webapp/app/entities/exercise.model.ts index fc5f8968374a..7f4c2b98ff98 100644 --- a/src/main/webapp/app/entities/exercise.model.ts +++ b/src/main/webapp/app/entities/exercise.model.ts @@ -87,7 +87,7 @@ export abstract class Exercise implements BaseEntity { public bonusPoints?: number; public assessmentType?: AssessmentType; public allowComplaintsForAutomaticAssessments?: boolean; - public allowManualFeedbackRequests?: boolean; + public allowFeedbackRequests?: boolean; public difficulty?: DifficultyLevel; public mode?: ExerciseMode = ExerciseMode.INDIVIDUAL; // default value public includedInOverallScore?: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY; // default value @@ -158,7 +158,7 @@ export abstract class Exercise implements BaseEntity { this.exampleSolutionPublicationDateError = false; this.presentationScoreEnabled = false; // default value; this.allowComplaintsForAutomaticAssessments = false; // default value; - this.allowManualFeedbackRequests = false; // default value; + this.allowFeedbackRequests = false; // default value; } /** @@ -278,5 +278,5 @@ export function resetDates(exercise: Exercise) { // without dates set, they can only be false exercise.allowComplaintsForAutomaticAssessments = false; - exercise.allowManualFeedbackRequests = false; + exercise.allowFeedbackRequests = false; } diff --git a/src/main/webapp/app/entities/feedback.model.ts b/src/main/webapp/app/entities/feedback.model.ts index 3c1d97cd8000..e3e13998c76b 100644 --- a/src/main/webapp/app/entities/feedback.model.ts +++ b/src/main/webapp/app/entities/feedback.model.ts @@ -33,6 +33,7 @@ export const SUBMISSION_POLICY_FEEDBACK_IDENTIFIER = 'SubPolFeedbackIdentifier:' export const FEEDBACK_SUGGESTION_IDENTIFIER = 'FeedbackSuggestion:'; export const FEEDBACK_SUGGESTION_ACCEPTED_IDENTIFIER = 'FeedbackSuggestion:accepted:'; export const FEEDBACK_SUGGESTION_ADAPTED_IDENTIFIER = 'FeedbackSuggestion:adapted:'; +export const NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER = 'NonGradedFeedbackSuggestion:'; export interface DropInfo { instruction: GradingInstruction; @@ -122,6 +123,13 @@ export class Feedback implements BaseEntity { return that.text.startsWith(FEEDBACK_SUGGESTION_IDENTIFIER); } + public static isNonGradedFeedbackSuggestion(that: Feedback): boolean { + if (!that.text) { + return false; + } + return that.text.startsWith(NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER); + } + /** * Determine the type of the feedback suggestion. See FeedbackSuggestionType for more details on the meanings. * @param that feedback to determine the type of diff --git a/src/main/webapp/app/entities/result.model.ts b/src/main/webapp/app/entities/result.model.ts index 1ed8cad65cbc..d6c2f96adaaa 100644 --- a/src/main/webapp/app/entities/result.model.ts +++ b/src/main/webapp/app/entities/result.model.ts @@ -48,6 +48,15 @@ export class Result implements BaseEntity { return that.assessmentType === AssessmentType.MANUAL || that.assessmentType === AssessmentType.SEMI_AUTOMATIC; } + /** + * Checks whether the result is generated by Athena AI. + * + * @return true if the result is an automatic Athena AI result + */ + public static isAthenaAIResult(that: Result): boolean { + return that.assessmentType === AssessmentType.AUTOMATIC_ATHENA; + } + /** * Checks whether the given result has an assessment note that is not empty. * @param that the result of which the presence of an assessment note is being checked diff --git a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html index d81acc677321..df0f821146f3 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html @@ -139,7 +139,7 @@ <h5 class="group-title font-weight-bold mb-0">{{ exerciseGroup.title }}</h5> }}" deleteConfirmationText="artemisApp.examManagement.exerciseGroup.delete.typeNameToConfirm" [additionalChecks]=" - localVCEnabled + localCIEnabled ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', diff --git a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.ts b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.ts index 5005054cced5..906c8ec190ea 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.ts +++ b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.ts @@ -33,7 +33,7 @@ import { import { ExamImportComponent } from 'app/exam/manage/exams/exam-import/exam-import.component'; import { ExerciseImportWrapperComponent } from 'app/exercises/shared/import/exercise-import-wrapper/exercise-import-wrapper.component'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; @Component({ selector: 'jhi-exercise-groups', @@ -54,6 +54,7 @@ export class ExerciseGroupsComponent implements OnInit { exerciseGroupToExerciseTypesDict = new Map<number, ExerciseType[]>(); localVCEnabled = false; + localCIEnabled = false; // Icons faPlus = faPlus; @@ -100,6 +101,7 @@ export class ExerciseGroupsComponent implements OnInit { }); this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); }); } diff --git a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.html b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.html index cd397988e0ae..fb70ab92e0e5 100644 --- a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.html +++ b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.html @@ -92,19 +92,26 @@ <div class="row align-items-top m-1"> <div class="col-auto ps-0"> <h5 class="d-inline"> - <span - class="badge" - [class.bg-success]="feedback.credits! > 0 && feedback.isSubsequent === undefined" - [class.bg-danger]="feedback.credits! < 0 && feedback.isSubsequent === undefined" - [class.bg-warning]="feedback.credits === 0 && feedback.isSubsequent === undefined" - [class.bg-secondary]="readOnly && feedback.isSubsequent" - >{{ roundScoreSpecifiedByCourseSettings(feedback.credits, course) + 'P' }}</span - > + @if (!Feedback.isNonGradedFeedbackSuggestion(feedback)) { + <span + class="badge" + [class.bg-success]="feedback.credits! > 0 && feedback.isSubsequent === undefined" + [class.bg-danger]="feedback.credits! < 0 && feedback.isSubsequent === undefined" + [class.bg-warning]="feedback.credits === 0 && feedback.isSubsequent === undefined" + [class.bg-secondary]="readOnly && feedback.isSubsequent" + >{{ roundScoreSpecifiedByCourseSettings(feedback.credits, course) + 'P' }}</span + > + } </h5> </div> <div class="col ps-0 pt-1"> - <h6 class="d-inline" jhiTranslate="artemisApp.assessment.detail.tutorComment"></h6> - <p [innerHTML]="buildFeedbackTextForCodeEditor(feedback)" class="mt-2"></p> + @if (Feedback.isNonGradedFeedbackSuggestion(feedback)) { + <h6 class="d-inline" jhiTranslate="artemisApp.assessment.detail.feedback"></h6> + <p [innerHTML]="buildFeedbackTextForCodeEditor(feedback)" class="mt-2"></p> + } @else { + <h6 class="d-inline" jhiTranslate="artemisApp.assessment.detail.tutorComment"></h6> + <p [innerHTML]="buildFeedbackTextForCodeEditor(feedback)" class="mt-2"></p> + } </div> @if (!readOnly) { <div class="col d-flex justify-content-end align-items-start pe-0"> diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html new file mode 100644 index 000000000000..a9328ed7f816 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html @@ -0,0 +1,14 @@ +<div class="my-2 ps-1 file-path-with-badge"> + {{ title }} + @if (fileStatus !== FileStatus.UNCHANGED) { + <span + [jhiTranslate]="'artemisApp.programmingExercise.diffReport.fileChange.' + fileStatus" + [ngClass]="{ + badge: true, + 'bg-success': fileStatus === FileStatus.CREATED, + 'bg-warning': fileStatus === FileStatus.RENAMED, + 'bg-danger': fileStatus === FileStatus.DELETED + }" + ></span> + } +</div> diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.scss b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.scss new file mode 100644 index 000000000000..afbb161f119d --- /dev/null +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.scss @@ -0,0 +1,9 @@ +.file-path-with-badge { + display: flex; + align-items: center; + column-gap: 5px; + + .badge { + margin-top: 2px; + } +} diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.ts b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.ts new file mode 100644 index 000000000000..3e7706581b88 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit } from '@angular/core'; + +enum FileStatus { + CREATED = 'created', + RENAMED = 'renamed', + DELETED = 'deleted', + UNCHANGED = 'unchanged', +} +@Component({ + selector: 'jhi-git-diff-file-panel-title', + templateUrl: './git-diff-file-panel-title.component.html', + styleUrls: ['./git-diff-file-panel-title.component.scss'], +}) +export class GitDiffFilePanelTitleComponent implements OnInit { + @Input() + previousFilePath?: string; + + @Input() + filePath?: string; + + title?: string; + fileStatus: FileStatus = FileStatus.UNCHANGED; + + // Expose to template + protected readonly FileStatus = FileStatus; + + ngOnInit(): void { + if (this.filePath && this.previousFilePath) { + if (this.filePath !== this.previousFilePath) { + this.title = `${this.previousFilePath} → ${this.filePath}`; + this.fileStatus = FileStatus.RENAMED; + } else { + this.title = this.filePath; + this.fileStatus = FileStatus.UNCHANGED; + } + } else if (this.filePath) { + this.title = this.filePath; + this.fileStatus = FileStatus.CREATED; + } else { + this.title = this.previousFilePath; + this.fileStatus = FileStatus.DELETED; + } + } +} diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.html b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.html index a43464c0ad84..7b8fafd1178e 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.html +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.html @@ -1,13 +1,7 @@ <div ngbAccordion [destroyOnHide]="false" class="git-diff-file-panel"> <div ngbAccordionItem #fileDiffPanel="ngbAccordionItem" [collapsed]="false"> <div ngbAccordionHeader class="d-flex align-items-center justify-content-between"> - <div class="my-2 ps-1"> - @if (filePath && previousFilePath && filePath !== previousFilePath) { - <div>{{ previousFilePath }} → {{ filePath }}</div> - } @else { - {{ filePath || previousFilePath }} - } - </div> + <jhi-git-diff-file-panel-title [filePath]="filePath" [previousFilePath]="previousFilePath" /> <div> <jhi-git-diff-line-stat [addedLineCount]="addedLineCount" @@ -27,10 +21,12 @@ <div ngbAccordionCollapse> <div ngbAccordionBody> <jhi-git-diff-file + [allowSplitView]="allowSplitView" + [diffForTemplateAndSolution]="diffForTemplateAndSolution" [diffEntries]="diffEntries" - [templateFileContent]="templateFileContent" - [solutionFileContent]="solutionFileContent" - [numberOfContextLines]="numberOfContextLines" + [originalFileContent]="templateFileContent" + [modifiedFileContent]="solutionFileContent" + (onDiffReady)="onDiffReady.emit($event)" /> </div> </div> diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.ts b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.ts index ead264f8b7cb..cd36cfc5b5f5 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.ts +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; @@ -15,10 +15,12 @@ export class GitDiffFilePanelComponent implements OnInit { @Input() solutionFileContent: string | undefined; - @Input() numberOfContextLines = 3; - @Input() diffForTemplateAndSolution = true; + @Input() allowSplitView = true; + + @Output() onDiffReady = new EventEmitter<boolean>(); + previousFilePath: string | undefined; filePath: string | undefined; diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.html b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.html index 0cff7091d21c..17ed16a47935 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.html +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.html @@ -1,10 +1,3 @@ <div class="git-diff-file ph-1"> - <div class="row"> - <div class="col col-6"> - <jhi-ace-editor #editorPrevious [readOnly]="true" [hidden]="false" [autoUpdateContent]="true" class="jhi-git-diff-report-editor-previous" /> - </div> - <div class="col col-6"> - <jhi-ace-editor #editorNow [readOnly]="true" [hidden]="false" [autoUpdateContent]="true" class="jhi-git-diff-report-editor-now" /> - </div> - </div> + <jhi-monaco-diff-editor [allowSplitView]="allowSplitView" (onReadyForDisplayChange)="onDiffReady.emit($event)" /> </div> diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.scss b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.scss deleted file mode 100644 index 9b6f0aeb155f..000000000000 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -.git-diff-file { - .removed-line { - position: absolute; - background: var(--git-diff-viewer-removed-line-background); - } - - .removed-line-gutter { - background: var(--git-diff-viewer-removed-line-gutter-background); - } - - .added-line { - position: absolute; - background: var(--git-diff-viewer-added-line-background); - } - - .added-line-gutter { - background: var(--git-diff-viewer-added-line-gutter-background); - } - - .placeholder-line { - position: absolute; - background: var(--git-diff-viewer-placeholder-line-background); - } - - .placeholder-line-gutter { - background: var(--git-diff-viewer-placeholder-line-gutter-background); - } - - .col { - padding-right: calc(var(--bs-gutter-x) * 0.15); - padding-left: calc(var(--bs-gutter-x) * 0.15); - } -} diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.ts b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.ts index fbf87d28f12b..004b64fd86d5 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.ts +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file.component.ts @@ -1,350 +1,54 @@ -import { Component, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { AceEditorComponent } from 'app/shared/markdown-editor/ace-editor/ace-editor.component'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; -import ace, { acequire } from 'brace'; +import { MonacoDiffEditorComponent } from 'app/shared/monaco-editor/monaco-diff-editor.component'; @Component({ selector: 'jhi-git-diff-file', templateUrl: './git-diff-file.component.html', - styleUrls: ['./git-diff-file.component.scss'], encapsulation: ViewEncapsulation.None, }) export class GitDiffFileComponent implements OnInit { - @ViewChild('editorPrevious', { static: true }) - editorPrevious: AceEditorComponent; + @ViewChild(MonacoDiffEditorComponent, { static: true }) + monacoDiffEditor: MonacoDiffEditorComponent; - @ViewChild('editorNow', { static: true }) - editorNow: AceEditorComponent; + @Input() + diffForTemplateAndSolution: boolean = false; @Input() diffEntries: ProgrammingExerciseGitDiffEntry[]; @Input() - templateFileContent: string | undefined; + originalFileContent?: string; @Input() - solutionFileContent: string | undefined; + modifiedFileContent?: string; @Input() - numberOfContextLines = 3; - - previousFilePath: string | undefined; - filePath: string | undefined; + allowSplitView = true; - templateLines: string[] = []; - solutionLines: string[] = []; + @Output() + onDiffReady = new EventEmitter<boolean>(); - actualStartLine: number; - previousEndLine: number; - endLine: number; - actualEndLine: number; - - readonly aceModeList = acequire('ace/ext/modelist'); + originalFilePath?: string; + modifiedFilePath?: string; ngOnInit(): void { - ace.Range = ace.acequire('ace/range').Range; - - // Create a clone of the diff entries to prevent modifications to the original diff entries - this.diffEntries = this.diffEntries.map((entry) => ({ ...entry })); - this.determineFilePaths(); - this.createLineArrays(); - this.determineActualStartLine(); - this.determineEndLines(); - - this.processEntries(); - - this.actualEndLine = Math.min(Math.max(this.templateLines.length, this.solutionLines.length), Math.max(this.previousEndLine, this.endLine)); - - this.setupEditor(this.editorPrevious); - this.setupEditor(this.editorNow); - this.renderTemplateFile(); - this.renderSolutionFile(); + this.monacoDiffEditor.setFileContents(this.originalFileContent, this.originalFilePath, this.modifiedFileContent, this.modifiedFilePath); } /** * Determines the previous and current file path of the current file */ private determineFilePaths() { - this.filePath = this.diffEntries + this.modifiedFilePath = this.diffEntries .map((entry) => entry.filePath) .filter((filePath) => filePath) .first(); - this.previousFilePath = this.diffEntries + this.originalFilePath = this.diffEntries .map((entry) => entry.previousFilePath) .filter((filePath) => filePath) .first(); } - - /** - * Splits the content of the template and solution files into an array of lines - */ - private createLineArrays() { - this.templateLines = this.templateFileContent?.split('\n') ?? []; - this.solutionLines = this.solutionFileContent?.split('\n') ?? []; - // Pop the last lines if they are empty, as these are irrelevant and not included in the diff entries - if (this.templateLines.last() === '') { - this.templateLines.pop(); - } - if (this.solutionLines.last() === '') { - this.solutionLines.pop(); - } - } - - /** - * Determines the first line that should be displayed. - * Compares the previous and current start line and takes the minimum of both and offsets it by the number of context lines. - */ - private determineActualStartLine() { - this.actualStartLine = Math.max( - 0, - Math.min( - ...[...this.diffEntries.map((entry) => entry.startLine ?? Number.MAX_VALUE), ...this.diffEntries.map((entry) => entry.previousStartLine ?? Number.MAX_VALUE)], - ) - - this.numberOfContextLines - - 1, - ); - } - - /** - * Determines the last line that should be displayed. - * Compares the previous and current last line (extracted from the diff entries) - * and takes the maximum of both and offsets it by the number of context lines. - */ - private determineEndLines() { - this.previousEndLine = Math.max(...this.diffEntries.map((entry) => (entry.previousStartLine ?? 0) + (entry.previousLineCount ?? 0))) + this.numberOfContextLines - 1; - this.endLine = Math.max(...this.diffEntries.map((entry) => (entry.startLine ?? 0) + (entry.lineCount ?? 0))) + this.numberOfContextLines - 1; - } - - /** - * Processes all git-diff entries by delegating to the appropriate processing method for each entry type. - */ - private processEntries() { - this.diffEntries.forEach((entry) => { - if (entry.previousStartLine && entry.previousLineCount && !entry.startLine && !entry.lineCount) { - this.processEntryWithDeletion(entry); - } else if (!entry.previousStartLine && !entry.previousLineCount && entry.startLine && entry.lineCount) { - this.processEntryWithAddition(entry); - } else if (entry.previousStartLine && entry.previousLineCount && entry.startLine && entry.lineCount) { - this.processEntryWithChange(entry); - } - }); - } - - /** - * Processes a git-diff entry with a deletion. Counterpart of processEntryWithAddition. - * Adds empty lines to the solution file to match the number of lines that are deleted in the template file. - * Also, accordingly offsets the start line of the entries that come after the added empty lines. - */ - private processEntryWithDeletion(entry: ProgrammingExerciseGitDiffEntry) { - this.solutionLines = [ - ...this.solutionLines.slice(0, entry.previousStartLine! - 1), - ...Array(entry.previousLineCount).fill(undefined), - ...this.solutionLines.slice(entry.previousStartLine! - 1), - ]; - this.endLine += entry.previousLineCount!; - this.diffEntries - .filter((entry2) => entry2.startLine && entry2.lineCount && entry2.startLine >= entry.previousStartLine!) - .forEach((entry2) => { - entry2.startLine! += entry.previousLineCount!; - }); - } - - /** - * Processes a git-diff entries with an addition. Counterpart of processEntryWithDeletion. - * Adds empty lines to the template file to match the number of lines that are added in the solution file. - * Also, accordingly offsets the start line of the entries that come after the added empty lines. - */ - private processEntryWithAddition(entry: ProgrammingExerciseGitDiffEntry) { - this.templateLines = [...this.templateLines.slice(0, entry.startLine! - 1), ...Array(entry.lineCount).fill(undefined), ...this.templateLines.slice(entry.startLine! - 1)]; - this.previousEndLine += entry.lineCount!; - this.diffEntries - .filter((entry2) => entry2.previousStartLine && entry2.previousLineCount && entry2.previousStartLine >= entry.startLine!) - .forEach((entry2) => { - entry2.previousStartLine! += entry.lineCount!; - }); - } - - /** - * Processes a git-diff entry with a change (deletion and addition). - * Adds empty lines to the template/solution file to match the number of lines that are added/removed in the solution/template file. - * Also, accordingly offsets the start line of the entries that come after the added empty lines. - */ - private processEntryWithChange(entry: ProgrammingExerciseGitDiffEntry) { - if (entry.previousLineCount! < entry.lineCount!) { - // There are more added lines than deleted lines -> add empty lines to the template file - this.templateLines = [ - ...this.templateLines.slice(0, entry.startLine! + entry.previousLineCount! - 1), - ...Array(entry.lineCount! - entry.previousLineCount!).fill(undefined), - ...this.templateLines.slice(entry.startLine! + entry.previousLineCount! - 1), - ]; - this.diffEntries - .filter((entry2) => entry2.previousStartLine && entry2.previousLineCount && entry2.previousStartLine > entry.startLine!) - .forEach((entry2) => { - entry2.previousStartLine! += entry.lineCount! - entry.previousLineCount!; - }); - } else { - // There are more deleted lines than added lines -> add empty lines to the solution file - this.solutionLines = [ - ...this.solutionLines.slice(0, entry.previousStartLine! + entry.lineCount! - 1), - ...Array(entry.previousLineCount! - entry.lineCount!).fill(undefined), - ...this.solutionLines.slice(entry.previousStartLine! + entry.lineCount! - 1), - ]; - this.diffEntries - .filter((entry2) => entry2.startLine && entry2.lineCount && entry2.startLine > entry.previousStartLine!) - .forEach((entry2) => { - entry2.startLine! += entry.previousLineCount! - entry.lineCount!; - }); - } - } - - /** - * Sets up an ace editor for the template or solution file. - * @param editor The editor to set up. - */ - private setupEditor(editor: AceEditorComponent): void { - editor.getEditor().setOptions({ - animatedScroll: true, - maxLines: Infinity, - showPrintMargin: false, - readOnly: true, - highlightActiveLine: false, - highlightGutterLine: false, - }); - editor.getEditor().renderer.setOptions({ - showFoldWidgets: false, - }); - editor.getEditor().renderer.$cursorLayer.element.style.display = 'none'; - const editorMode = this.aceModeList.getModeForPath(this.filePath ?? this.previousFilePath ?? '').name; - editor.setMode(editorMode); - } - - /** - * Renders the content of the template file in the template editor. - * Sets the content of the editor and colors the lines that were removed red. - * All empty lines added in the processEntries methods are colored gray. - */ - private renderTemplateFile() { - const session = this.editorPrevious.getEditor().getSession(); - session.setValue( - this.templateLines - .slice(this.actualStartLine, this.actualEndLine) - .map((line) => line ?? '') - .join('\n'), - ); - - Object.entries(session.getMarkers() ?? {}).forEach(([, v]) => session.removeMarker((v as any).id)); - - // Adds the red coloring to the code and the gutter - this.diffEntries - .filter((entry) => entry.previousStartLine !== undefined && entry.previousLineCount !== undefined) - .map((entry) => { - const startRow = entry.previousStartLine! - this.actualStartLine - 1; - const endRow = entry.previousStartLine! + entry.previousLineCount! - this.actualStartLine - 2; - const range = new ace.Range(startRow, 0, endRow, 1); - session.addMarker(range, 'removed-line', 'fullLine'); - for (let i = startRow; i <= endRow; i++) { - session.addGutterDecoration(i, 'removed-line-gutter'); - } - }); - - // Adds the gray coloring to the code and the gutter - this.templateLines.forEach((line, index) => { - if (line === undefined) { - const actualLine = index - this.actualStartLine; - const range = new ace.Range(actualLine, 0, actualLine, 1); - session.addMarker(range, 'placeholder-line', 'fullLine'); - session.addGutterDecoration(actualLine, 'placeholder-line-gutter'); - } - }); - - // Copy the lines here, as otherwise they may be undefined in the gutter - const templateLinesCopy = this.templateLines; - const copyActualStartLine = this.actualStartLine; - const copyActualEndLine = this.actualEndLine; - let rowNumber: number; - - // Takes care of the correct numbering of the lines, as empty lines added by the processEntries methods are not counted - session.gutterRenderer = { - getWidth(session2: any, lastLineNumber: number, config: any) { - return Math.max( - ...Array.from({ length: copyActualEndLine - copyActualStartLine }, (_, index) => index + 1).map((lineNumber) => { - return this.getText(session, lineNumber - 1).toString().length * config.characterWidth; - }), - ); - }, - getText(_: any, row: number): string | number { - if (row === 0) { - rowNumber = copyActualStartLine + 1; - } - return templateLinesCopy[row + copyActualStartLine] !== undefined ? rowNumber++ : ''; - }, - } as any; - this.editorPrevious.getEditor().resize(); - } - - /** - * Renders the content of the solution file in the solution editor. - * Sets the content of the editor and colors the lines that were added green. - * All empty lines added in the processEntries methods are colored gray. - */ - private renderSolutionFile() { - const session = this.editorNow.getEditor().getSession(); - session.setValue( - this.solutionLines - .slice(this.actualStartLine, this.actualEndLine) - .map((line) => line ?? '') - .join('\n'), - ); - - Object.entries(session.getMarkers() ?? {}).forEach(([, v]) => session.removeMarker((v as any).id)); - - // Adds the red coloring to the code and the gutter - this.diffEntries - .filter((entry) => entry.startLine && entry.lineCount) - .map((entry) => { - const startRow = entry.startLine! - this.actualStartLine - 1; - const endRow = entry.startLine! + entry.lineCount! - this.actualStartLine - 2; - const range = new ace.Range(startRow, 0, endRow, 1); - session.addMarker(range, 'added-line', 'fullLine'); - for (let i = startRow; i <= endRow; i++) { - session.addGutterDecoration(i, 'added-line-gutter'); - } - }); - - // Adds the gray coloring to the code and the gutter - this.solutionLines.forEach((line, index) => { - if (line === undefined) { - const actualLine = index - this.actualStartLine; - const range = new ace.Range(actualLine, 0, actualLine, 1); - session.addMarker(range, 'placeholder-line', 'fullLine'); - session.addGutterDecoration(actualLine, 'placeholder-line-gutter'); - } - }); - - // Copy the lines here, as otherwise they may be undefined in the gutter - const solutionLinesCopy = this.solutionLines; - const copyActualStartLine = this.actualStartLine; - const copyActualEndLine = this.actualEndLine; - let rowNumber: number; - - // Takes care of the correct numbering of the lines, as empty lines added by the processEntries methods are not counted - session.gutterRenderer = { - getWidth(session2: any, lastLineNumber: number, config: any) { - return Math.max( - ...Array.from({ length: copyActualEndLine - copyActualStartLine }, (_, index) => index + 1).map((lineNumber) => { - return this.getText(session, lineNumber - 1).toString().length * config.characterWidth; - }), - ); - }, - getText(_: any, row: number): string | number { - if (row === 0) { - rowNumber = copyActualStartLine + 1; - } - return solutionLinesCopy[row + copyActualStartLine] !== undefined ? rowNumber++ : ''; - }, - }; - this.editorPrevious.getEditor().resize(); - } } diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.html b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.html index 317b16429079..193986a6dcd2 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.html +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.html @@ -20,7 +20,7 @@ <h4 class="ps-2"> } </h4> </div> - <div class="col-4"> + <div class="col-3"> <h4 class="ps-2"> @if (isRepositoryView && rightCommit) { @if (diffForTemplateAndEmptyRepository) { @@ -39,29 +39,52 @@ <h4 class="ps-2"> } </h4> </div> - <div class="col-2 text-end fw-bold"> - <jhi-git-diff-line-stat - [addedLineCount]="addedLineCount" - [removedLineCount]="removedLineCount" - ngbTooltip="{{ - (diffForTemplateAndSolution - ? 'artemisApp.programmingExercise.diffReport.lineStatTooltipFullReport' - : 'artemisApp.programmingExercise.diffReport.lineStatTooltipFullReportExamTimeline' - ) | artemisTranslate - }}" - /> - </div> -</div> -@for (filePath of filePaths; track filePath) { - <div class="mt-2"> - @if (entriesByPath.get(filePath)?.length) { - <jhi-git-diff-file-panel - [diffForTemplateAndSolution]="diffForTemplateAndSolution" - [diffEntries]="entriesByPath.get(filePath) ?? []" - [templateFileContent]="templateFileContentByPath.get(filePath)" - [solutionFileContent]="solutionFileContentByPath.get(filePath)" - [numberOfContextLines]="numberOfContextLines" + <div class="col-3"> + <div class="d-flex flex-row gap-2 justify-content-end"> + <jhi-button + [btnType]="ButtonType.PRIMARY" + [btnSize]="ButtonSize.SMALL" + [icon]="faTableColumns" + tooltip="artemisApp.programmingExercise.diffReport.splitView.tooltip" + [title]="'artemisApp.programmingExercise.diffReport.splitView.' + (allowSplitView ? 'disable' : 'enable')" + (onClick)="allowSplitView = !allowSplitView" + /> + <jhi-git-diff-line-stat + class="mt-1 fw-bold" + [addedLineCount]="addedLineCount" + [removedLineCount]="removedLineCount" + ngbTooltip="{{ + (diffForTemplateAndSolution + ? 'artemisApp.programmingExercise.diffReport.lineStatTooltipFullReport' + : 'artemisApp.programmingExercise.diffReport.lineStatTooltipFullReportExamTimeline' + ) | artemisTranslate + }}" /> - } + </div> </div> -} +</div> +<div [hidden]="!allDiffsReady"> + @for (filePath of filePaths; track filePath) { + <div class="mt-2"> + @if (entriesByPath.get(filePath)?.length) { + <jhi-git-diff-file-panel + [allowSplitView]="allowSplitView" + [diffForTemplateAndSolution]="diffForTemplateAndSolution" + [diffEntries]="entriesByPath.get(filePath) ?? []" + [templateFileContent]="templateFileContentByPath.get(filePath)" + [solutionFileContent]="solutionFileContentByPath.get(filePath)" + (onDiffReady)="onDiffReady(filePath, $event)" + /> + } + </div> + } +</div> +<div class="d-flex w-100 justify-content-center align-items-center fs-4"> + @if (nothingToDisplay) { + <div class="text-secondary mt-3" jhiTranslate="artemisApp.repository.commitHistory.commitDetails.empty"></div> + } @else if (!allDiffsReady) { + <div> + <fa-icon [icon]="faSpinner" [spin]="true" /> + </div> + } +</div> diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.ts b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.ts index ebcd1ea734d6..1c52a0d349ff 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.ts +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.component.ts @@ -1,6 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; +import { faSpinner, faTableColumns } from '@fortawesome/free-solid-svg-icons'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; @Component({ selector: 'jhi-git-diff-report', @@ -25,12 +27,21 @@ export class GitDiffReportComponent implements OnInit { rightCommit: string | undefined; - // TODO: Make this configurable by the user - numberOfContextLines = 3; entries: ProgrammingExerciseGitDiffEntry[]; entriesByPath: Map<string, ProgrammingExerciseGitDiffEntry[]>; addedLineCount: number; removedLineCount: number; + diffsReadyByPath: { [path: string]: boolean } = {}; + allDiffsReady = false; + nothingToDisplay = false; + allowSplitView = true; + + faSpinner = faSpinner; + faTableColumns = faTableColumns; + + // Expose to template + protected readonly ButtonSize = ButtonSize; + protected readonly ButtonType = ButtonType; constructor() {} @@ -73,7 +84,6 @@ export class GitDiffReportComponent implements OnInit { // Create a set of all file paths this.filePaths = [...new Set([...this.templateFileContentByPath.keys(), ...this.solutionFileContentByPath.keys()])].sort(); - // Group the diff entries by file path this.entriesByPath = new Map<string, ProgrammingExerciseGitDiffEntry[]>(); [...this.templateFileContentByPath.keys()].forEach((filePath) => { @@ -90,5 +100,22 @@ export class GitDiffReportComponent implements OnInit { }); this.leftCommit = this.report.leftCommitHash?.substring(0, 10); this.rightCommit = this.report.rightCommitHash?.substring(0, 10); + this.filePaths.forEach((path) => { + if (this.entriesByPath.get(path)?.length) { + this.diffsReadyByPath[path] = false; + } + }); + this.nothingToDisplay = Object.keys(this.diffsReadyByPath).length === 0; + } + + /** + * Records that the diff editor for a file has changed its "ready" state. + * If all paths have reported that they are ready, {@link allDiffsReady} will be set to true. + * @param path The path of the file whose diff this event refers to. + * @param ready Whether the diff is ready to be displayed or not. + */ + onDiffReady(path: string, ready: boolean) { + this.diffsReadyByPath[path] = ready; + this.allDiffsReady = Object.values(this.diffsReadyByPath).reduce((a, b) => a && b); } } diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.module.ts b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.module.ts index 9417cb8fbaaa..b134a0c17ad2 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.module.ts +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-report.module.ts @@ -7,10 +7,13 @@ import { GitDiffFileComponent } from 'app/exercises/programming/hestia/git-diff- import { GitDiffReportModalComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-report-modal.component'; import { GitDiffFilePanelComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { GitDiffFilePanelTitleComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; @NgModule({ - imports: [ArtemisSharedModule, AceEditorModule, NgbAccordionModule], - declarations: [GitDiffFilePanelComponent, GitDiffReportComponent, GitDiffFileComponent, GitDiffReportModalComponent, GitDiffLineStatComponent], + imports: [ArtemisSharedModule, AceEditorModule, NgbAccordionModule, MonacoEditorModule, ArtemisSharedComponentModule], + declarations: [GitDiffFilePanelComponent, GitDiffFilePanelTitleComponent, GitDiffReportComponent, GitDiffFileComponent, GitDiffReportModalComponent, GitDiffLineStatComponent], exports: [GitDiffReportComponent, GitDiffReportModalComponent, GitDiffLineStatComponent], }) export class GitDiffReportModule {} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index 58520f55f064..9adbffa0019d 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -211,7 +211,7 @@ <h2><span jhiTranslate="artemisApp.programmingExercise.detail.title"></span> {{ (delete)="deleteProgrammingExercise($event)" [dialogError]="dialogError$" [additionalChecks]=" - localVCEnabled + localCIEnabled ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', 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 6a25143811ec..5caa177d4960 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 @@ -48,7 +48,7 @@ import { ProgrammingLanguageFeatureService } from 'app/exercises/programming/sha import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ConsistencyCheckService } from 'app/shared/consistency-check/consistency-check.service'; import { hasEditableBuildPlan } from 'app/shared/layouts/profiles/profile-info.model'; -import { PROFILE_IRIS, PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_IRIS, PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; import { ArtemisMarkdownService } from 'app/shared/markdown.service'; import { DetailOverviewSection, DetailType } from 'app/detail-overview-list/detail-overview-list.component'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; @@ -90,6 +90,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { // Used to hide links to repositories and build plans when the "localvc" profile is active. // Also used to hide the buttons to lock and unlock all repositories as that does not do anything in the local VCS. localVCEnabled = false; + localCIEnabled = false; irisEnabled = false; irisChatEnabled = false; @@ -195,6 +196,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.supportsAuxiliaryRepositories = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? false; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); this.irisEnabled = profileInfo.activeProfiles.includes(PROFILE_IRIS); if (this.irisEnabled) { this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { @@ -373,7 +375,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { showOpenLink: !this.localVCEnabled, }, }, - !this.localVCEnabled && { + !this.localCIEnabled && { type: DetailType.Link, title: 'artemisApp.programmingExercise.templateBuildPlanId', data: { @@ -381,7 +383,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { text: exercise.templateParticipation?.buildPlanId, }, }, - !this.localVCEnabled && { + !this.localCIEnabled && { type: DetailType.Link, title: 'artemisApp.programmingExercise.solutionBuildPlanId', data: { @@ -534,7 +536,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { title: 'artemisApp.programmingExercise.timeline.complaintOnAutomaticAssessment', data: { boolean: exercise.allowComplaintsForAutomaticAssessments }, }, - { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.timeline.manualFeedbackRequests', data: { boolean: exercise.allowManualFeedbackRequests } }, + { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.timeline.manualFeedbackRequests', data: { boolean: exercise.allowFeedbackRequests } }, { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.showTestNamesToStudents', data: { boolean: exercise.showTestNamesToStudents } }, { type: DetailType.Boolean, diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 67b6a2cc1b59..2b133a7a219b 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -270,7 +270,7 @@ (delete)="deleteProgrammingExercise(programmingExercise.id!, $event)" [dialogError]="dialogError$" [additionalChecks]=" - localVCEnabled + localCIEnabled ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', @@ -320,7 +320,7 @@ (delete)="deleteMultipleProgrammingExercises(selectedExercises, $event)" [requireConfirmationOnlyForAdditionalChecks]="true" [additionalChecks]=" - localVCEnabled + localCIEnabled ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', 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 3d99ccbd7980..b28fbed3d6b9 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 @@ -39,7 +39,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; @Component({ selector: 'jhi-programming-exercise', @@ -54,6 +54,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O templateParticipationType = ProgrammingExerciseParticipationType.TEMPLATE; // Used to make the repository links download the repositories instead of linking to GitLab. localVCEnabled = false; + localCIEnabled = false; // extension points, see shared/extension-point @ContentChild('overrideRepositoryAndBuildPlan') overrideRepositoryAndBuildPlan: TemplateRef<any>; @@ -109,6 +110,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O this.profileService.getProfileInfo().subscribe((profileInfo) => { this.buildPlanLinkTemplate = profileInfo.buildPlanURLTemplate; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); }); // reconnect exercise with course this.programmingExercises.forEach((exercise) => { diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts index 1cf987c5f950..63649449d858 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts @@ -449,6 +449,18 @@ export class ProgrammingExerciseService { } } + /** + * Exports the repository belonging to a specific student participation of a programming exercise. + * @param exerciseId The ID of the programming exercise. + * @param participationId The ID of the (student) participation + */ + exportStudentRepository(exerciseId: number, participationId: number): Observable<HttpResponse<Blob>> { + return this.http.get(`${this.resourceUrl}/${exerciseId}/export-student-repository/${participationId}`, { + observe: 'response', + responseType: 'blob', + }); + } + /** * Exports all instructor repositories (solution, template, test), the problem statement and the exercise details. * @param exerciseId diff --git a/src/main/webapp/app/exercises/programming/participate/programming-repository.module.ts b/src/main/webapp/app/exercises/programming/participate/programming-repository.module.ts index c8bbb0d290b4..c1b68191f38a 100644 --- a/src/main/webapp/app/exercises/programming/participate/programming-repository.module.ts +++ b/src/main/webapp/app/exercises/programming/participate/programming-repository.module.ts @@ -19,6 +19,7 @@ import { ArtemisProgrammingExerciseModule } from 'app/exercises/programming/shar import { CommitHistoryComponent } from 'app/localvc/commit-history/commit-history.component'; import { CommitDetailsViewComponent } from 'app/localvc/commit-details-view/commit-details-view.component'; import { GitDiffReportModule } from 'app/exercises/programming/hestia/git-diff-report/git-diff-report.module'; +import { ArtemisProgrammingExerciseActionsModule } from 'app/exercises/programming/shared/actions/programming-exercise-actions.module'; @NgModule({ imports: [ @@ -39,6 +40,7 @@ import { GitDiffReportModule } from 'app/exercises/programming/hestia/git-diff-r SubmissionResultStatusModule, ArtemisProgrammingExerciseModule, GitDiffReportModule, + ArtemisProgrammingExerciseActionsModule, ], declarations: [RepositoryViewComponent, CommitHistoryComponent, CommitDetailsViewComponent], exports: [RepositoryViewComponent], diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-actions.module.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-actions.module.ts index 8af9ff827b27..2dbfbbdb5de0 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-actions.module.ts +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-actions.module.ts @@ -11,6 +11,7 @@ import { ProgrammingExerciseReEvaluateButtonComponent } from 'app/exercises/prog import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FeatureToggleModule } from 'app/shared/feature-toggle/feature-toggle.module'; import { ProgrammingExerciseInstructorRepoDownloadComponent } from 'app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component'; +import { ProgrammingExerciseStudentRepoDownloadComponent } from 'app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component'; @NgModule({ imports: [ArtemisSharedModule, ArtemisSharedComponentModule, FeatureToggleModule], @@ -22,6 +23,7 @@ import { ProgrammingExerciseInstructorRepoDownloadComponent } from 'app/exercise ProgrammingExerciseTriggerAllButtonComponent, ProgrammingExerciseReEvaluateButtonComponent, ProgrammingExerciseInstructorRepoDownloadComponent, + ProgrammingExerciseStudentRepoDownloadComponent, ], exports: [ ProgrammingExerciseInstructorTriggerBuildButtonComponent, @@ -30,6 +32,7 @@ import { ProgrammingExerciseInstructorRepoDownloadComponent } from 'app/exercise ProgrammingExerciseTriggerAllButtonComponent, ProgrammingExerciseReEvaluateButtonComponent, ProgrammingExerciseInstructorRepoDownloadComponent, + ProgrammingExerciseStudentRepoDownloadComponent, ], }) export class ArtemisProgrammingExerciseActionsModule {} diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.html b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.html index 2a5f95485669..a634c9aef170 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.html +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.html @@ -1,7 +1,7 @@ <jhi-button [disabled]="!exerciseId" [btnType]="ButtonType.INFO" - [btnSize]="ButtonSize.SMALL" + [btnSize]="buttonSize" [shouldSubmit]="false" [featureToggle]="[FeatureToggle.ProgrammingExercises, FeatureToggle.Exports]" [icon]="faDownload" diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.ts index ad3c86d2b67c..988a80c356f1 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component.ts @@ -24,6 +24,9 @@ export class ProgrammingExerciseInstructorRepoDownloadComponent { @Input() auxiliaryRepositoryId: number; + @Input() + buttonSize: ButtonSize = ButtonSize.SMALL; + @Input() title = 'artemisApp.programmingExercise.export.downloadRepo'; @@ -39,7 +42,7 @@ export class ProgrammingExerciseInstructorRepoDownloadComponent { if (this.exerciseId && this.repositoryType) { this.programmingExerciseService.exportInstructorRepository(this.exerciseId, this.repositoryType, this.auxiliaryRepositoryId).subscribe((response) => { downloadZipFileFromResponse(response); - this.alertService.success('artemisApp.programmingExercise.export.successMessageRepos'); + this.alertService.success('artemisApp.programmingExercise.export.successMessageRepo'); }); } } diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.html b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.html new file mode 100644 index 000000000000..974495f69ab1 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.html @@ -0,0 +1,10 @@ +<jhi-button + [disabled]="!exerciseId || !participationId" + [btnType]="ButtonType.INFO" + [btnSize]="buttonSize" + [shouldSubmit]="false" + [featureToggle]="[FeatureToggle.ProgrammingExercises, FeatureToggle.Exports]" + [icon]="faDownload" + [title]="title" + (onClick)="exportRepository()" +/> diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.ts new file mode 100644 index 000000000000..420104a21c0d --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component.ts @@ -0,0 +1,50 @@ +import { Component, Input } from '@angular/core'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; +import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; +import { AlertService } from 'app/core/util/alert.service'; +import { faDownload } from '@fortawesome/free-solid-svg-icons'; +import { take } from 'rxjs'; + +@Component({ + selector: 'jhi-programming-exercise-student-repo-download', + templateUrl: './programming-exercise-student-repo-download.component.html', +}) +export class ProgrammingExerciseStudentRepoDownloadComponent { + ButtonType = ButtonType; + ButtonSize = ButtonSize; + readonly FeatureToggle = FeatureToggle; + + @Input() + exerciseId: number; + + @Input() + participationId: number; + + @Input() + buttonSize: ButtonSize = ButtonSize.SMALL; + + @Input() + title = 'artemisApp.programmingExercise.export.downloadRepo'; + + // Icons + faDownload = faDownload; + + constructor( + protected programmingExerciseService: ProgrammingExerciseService, + protected alertService: AlertService, + ) {} + + exportRepository() { + if (this.exerciseId && this.participationId) { + this.programmingExerciseService + .exportStudentRepository(this.exerciseId, this.participationId) + .pipe(take(1)) + .subscribe((response) => { + downloadZipFileFromResponse(response); + this.alertService.success('artemisApp.programmingExercise.export.successMessageRepo'); + }); + } + } +} diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts index f0af406df9ac..264fb2bf79f3 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/build-output/code-editor-build-output.component.ts @@ -18,6 +18,7 @@ import { StaticCodeAnalysisIssue } from 'app/entities/static-code-analysis-issue import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { faChevronDown, faCircleNotch, faTerminal } from '@fortawesome/free-solid-svg-icons'; import { hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; +import { AssessmentType } from 'app/entities/assessment-type.model'; @Component({ selector: 'jhi-code-editor-build-output', @@ -201,7 +202,7 @@ export class CodeEditorBuildOutputComponent implements AfterViewInit, OnInit, On * @param result */ fetchBuildResults(result?: Result): Observable<BuildLogEntry[] | undefined> { - if (result && (!result.submission || (result.submission as ProgrammingSubmission).buildFailed)) { + if (result && result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA && (!result.submission || (result.submission as ProgrammingSubmission).buildFailed)) { return this.getBuildLogs(); } else { return of([]); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html index 3487f56e17e1..d7b96f5511c8 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.html @@ -27,6 +27,7 @@ <h4 class="editor-title"><ng-content select="[editorTitle]" /></h4> <jhi-code-editor-file-browser editorSidebarLeft [disableActions]="!editable" + [displayOnly]="forRepositoryView" [unsavedFiles]="unsavedFiles | keys" [errorFiles]="errorFiles" [editorState]="editorState" diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts index 844e96e3722a..731c4e7a2cc8 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/container/code-editor-container.component.ts @@ -55,6 +55,8 @@ export class CodeEditorContainerComponent implements OnChanges, ComponentCanDeac @Input() editable = true; @Input() + forRepositoryView = false; + @Input() buildable = true; @Input() showEditorInstructions = true; diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.html index 13f2c5ee1426..ab68ed3f8628 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.html @@ -19,37 +19,39 @@ <h3 class="card-title justify-content-center"> <fa-icon class="ms-auto clickable" [icon]="faChevronLeft" /> </div> <!-- Root actions --> - <div class="card-second-header"> - <button - id="create_file_root" - [disabled]="disableActions || isLoadingFiles || commitState === CommitState.CONFLICT" - class="btn" - (click)="setCreatingFileInRoot(FileType.FILE)" - title="{{ 'artemisApp.editor.fileBrowser.createFileRoot' | artemisTranslate }}" - > - <fa-icon [icon]="faPlus" /> - <fa-icon [icon]="faFile" class="ms-1" /> - </button> - <button - id="create_folder_root" - [disabled]="disableActions || isLoadingFiles || commitState === CommitState.CONFLICT" - class="btn ms-2" - (click)="setCreatingFileInRoot(FileType.FOLDER)" - title="{{ 'artemisApp.editor.fileBrowser.createFolderRoot' | artemisTranslate }}" - > - <fa-icon [icon]="faPlus" /> - <fa-icon [icon]="faFolder" class="ms-1" /> - </button> - <button - id="compress_tree" - [disabled]="isLoadingFiles" - class="btn ms-auto" - (click)="toggleTreeCompress()" - title="{{ 'artemisApp.editor.fileBrowser.compressTree' | artemisTranslate }}" - > - <fa-icon [icon]="compressFolders ? faAngleDoubleUp : faAngleDoubleDown" /> - </button> - </div> + @if (!displayOnly) { + <div class="card-second-header"> + <button + id="create_file_root" + [disabled]="disableActions || isLoadingFiles || commitState === CommitState.CONFLICT" + class="btn" + (click)="setCreatingFileInRoot(FileType.FILE)" + title="{{ 'artemisApp.editor.fileBrowser.createFileRoot' | artemisTranslate }}" + > + <fa-icon [icon]="faPlus" /> + <fa-icon [icon]="faFile" class="ms-1" /> + </button> + <button + id="create_folder_root" + [disabled]="disableActions || isLoadingFiles || commitState === CommitState.CONFLICT" + class="btn ms-2" + (click)="setCreatingFileInRoot(FileType.FOLDER)" + title="{{ 'artemisApp.editor.fileBrowser.createFolderRoot' | artemisTranslate }}" + > + <fa-icon [icon]="faPlus" /> + <fa-icon [icon]="faFolder" class="ms-1" /> + </button> + <button + id="compress_tree" + [disabled]="isLoadingFiles" + class="btn ms-auto" + (click)="toggleTreeCompress()" + title="{{ 'artemisApp.editor.fileBrowser.compressTree' | artemisTranslate }}" + > + <fa-icon [icon]="compressFolders ? faAngleDoubleUp : faAngleDoubleDown" /> + </button> + </div> + } <!-- File Tree --> <div class="card-body"> <!-- Root level create file/folder tree element --> @@ -72,7 +74,7 @@ <h3 class="card-title justify-content-center"> </div> } </div> - @if (!collapsed) { + @if (!collapsed && !displayOnly) { <jhi-code-editor-status #status [editorState]="editorState" [commitState]="commitState" /> } </div> diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.ts index e1f3bb342802..52fa1c7dbd28 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/code-editor-file-browser.component.ts @@ -59,7 +59,10 @@ export class CodeEditorFileBrowserComponent implements OnInit, OnChanges, AfterV get selectedFile(): string | undefined { return this.selectedFileValue; } - @Input() disableActions = false; + @Input() + disableActions = false; + @Input() + displayOnly = false; @Input() unsavedFiles: string[]; @Input() diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html index d6051ff7bee1..3c1650f7fd9d 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html @@ -86,7 +86,7 @@ }} </div> </div> - @if (exercise.assessmentType === assessmentType.SEMI_AUTOMATIC && !isExamMode && !exercise.allowManualFeedbackRequests) { + @if (exercise.assessmentType === assessmentType.SEMI_AUTOMATIC && !isExamMode && !exercise.allowFeedbackRequests) { <jhi-programming-exercise-test-schedule-date-picker class="test-schedule-date" [(ngModel)]="exercise.assessmentDueDate" @@ -143,19 +143,29 @@ <h6 jhiTranslate="artemisApp.assessment.assessment"></h6> <input type="checkbox" class="form-check-input" - name="allowManualFeedbackRequests" - [checked]="exercise.allowManualFeedbackRequests" + name="allowFeedbackRequests" + [checked]="exercise.allowFeedbackRequests" [disabled]="exercise.assessmentType !== assessmentType.SEMI_AUTOMATIC" - id="allowManualFeedbackRequests" - (change)="toggleManualFeedbackRequests()" + id="allowFeedbackRequests" + (change)="toggleFeedbackRequests()" /> - <label - [ngStyle]="exercise.assessmentType !== assessmentType.SEMI_AUTOMATIC ? { color: 'grey' } : {}" - class="form-control-label" - for="allowManualFeedbackRequests" - jhiTranslate="artemisApp.programmingExercise.timeline.manualFeedbackRequests" - ></label> - <jhi-help-icon placement="right auto" [text]="'artemisApp.programmingExercise.timeline.manualFeedbackRequestsTooltip'" /> + @if (isAthenaEnabled$ | async) { + <label + [ngStyle]="exercise.assessmentType !== assessmentType.SEMI_AUTOMATIC ? { color: 'grey' } : {}" + class="form-control-label" + for="allowFeedbackRequests" + jhiTranslate="artemisApp.programmingExercise.timeline.allowFeedbackRequests" + ></label> + <jhi-help-icon placement="right auto" [text]="'artemisApp.programmingExercise.timeline.allowFeedbackRequestsTooltip'" /> + } @else { + <label + [ngStyle]="exercise.assessmentType !== assessmentType.SEMI_AUTOMATIC ? { color: 'grey' } : {}" + class="form-control-label" + for="allowFeedbackRequests" + jhiTranslate="artemisApp.programmingExercise.timeline.manualFeedbackRequests" + ></label> + <jhi-help-icon placement="right auto" [text]="'artemisApp.programmingExercise.timeline.manualFeedbackRequestsTooltip'" /> + } </div> } <div class="form-check"> diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts index 89d77c2756b8..2a9bbe164002 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts @@ -100,9 +100,9 @@ export class ProgrammingExerciseLifecycleComponent implements AfterViewInit, OnD } } - toggleManualFeedbackRequests() { - this.exercise.allowManualFeedbackRequests = !this.exercise.allowManualFeedbackRequests; - if (this.exercise.allowManualFeedbackRequests) { + toggleFeedbackRequests() { + this.exercise.allowFeedbackRequests = !this.exercise.allowFeedbackRequests; + if (this.exercise.allowFeedbackRequests) { this.exercise.assessmentDueDate = undefined; this.exercise.buildAndTestStudentSubmissionsAfterDueDate = undefined; } @@ -122,7 +122,7 @@ export class ProgrammingExerciseLifecycleComponent implements AfterViewInit, OnD } else { this.exercise.assessmentType = AssessmentType.SEMI_AUTOMATIC; this.exercise.allowComplaintsForAutomaticAssessments = false; - this.exercise.allowManualFeedbackRequests = false; + this.exercise.allowFeedbackRequests = false; } } diff --git a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts index 10b2c039098e..3234ee6a1cf1 100644 --- a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts +++ b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts @@ -59,6 +59,9 @@ export const isResultPreliminary = (latestResult: Result, programmingExercise?: if (!programmingExercise) { return false; } + if (latestResult.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { + return false; + } if (latestResult.participation?.type === ParticipationType.PROGRAMMING && isPracticeMode(latestResult.participation)) { return false; } diff --git a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html index 537cfc808e6d..9e90d1ce8ebb 100644 --- a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html @@ -249,7 +249,7 @@ <h3 class="mb-0"><span class="badge bg-warning align-text-top" style="width: 60p <div class="question-options row d-flex justify-content-start"> <div class="input-group col-lg-7 col-md-8 col-sm-8 col-xs-10 drag-item-file"> <div class="input-group-prepend"> - <button class="btn btn-outline-secondary" (click)="backgroundFileInput.click()"> + <button class="btn btn-outline-secondary" id="background-file-input-button" (click)="backgroundFileInput.click()"> <fa-icon [icon]="faPlus" /> @if (!reEvaluationInProgress) { <span jhiTranslate="artemisApp.dragAndDropQuestion.selectBackgroundPicture" class="no-flex-shrink"></span> diff --git a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts index 827cd5bd9bad..62f371f95057 100644 --- a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts +++ b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts @@ -332,7 +332,7 @@ export class ExerciseAssessmentDashboardComponent implements OnInit { // The assessment for team exercises is not started from the tutor exercise dashboard but from the team pages const isAfterDueDate = !this.exercise.dueDate || this.exercise.dueDate.isBefore(dayjs()); - if ((this.exercise.allowManualFeedbackRequests || isAfterDueDate) && !this.exercise.teamMode && !this.isTestRun) { + if ((this.exercise.allowFeedbackRequests || isAfterDueDate) && !this.exercise.teamMode && !this.isTestRun) { this.getSubmissionWithoutAssessmentForAllCorrectionRounds(); } diff --git a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html index c973e46b9f0e..864843d9ed93 100644 --- a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html +++ b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html @@ -149,7 +149,7 @@ [dialogError]="dialogError$" deleteConfirmationText="artemisApp.exercise.delete.typeNameToConfirm" [additionalChecks]=" - localVCEnabled + localCIEnabled ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', diff --git a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.ts b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.ts index 6c7a050ab7f1..79f1e755b358 100644 --- a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.ts +++ b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.ts @@ -14,7 +14,7 @@ import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { EventManager } from 'app/core/util/event-manager.service'; import { faBook, faExclamationTriangle, faEye, faFileExport, faFileSignature, faPencilAlt, faSignal, faTable, faTrash, faUsers, faWrench } from '@fortawesome/free-solid-svg-icons'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; @Component({ @@ -47,6 +47,7 @@ export class ExamExerciseRowButtonsComponent implements OnInit { farListAlt = faListAlt; localVCEnabled = false; + localCIEnabled = false; constructor( private textExerciseService: TextExerciseService, @@ -61,6 +62,7 @@ export class ExamExerciseRowButtonsComponent implements OnInit { ngOnInit(): void { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); }); } diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html index 7f451ca145c6..86b82b2226a4 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -39,22 +39,28 @@ <h6 [innerHTML]="'artemisApp.result.afterDueDateFeedbackHidden' | artemisTransla <div> <div class="d-flex justify-content-between align-items-start"> <h4> - @if (!exercise?.maxPoints) { + @if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { <span> - {{ 'artemisApp.result.score' | artemisTranslate: { score: roundValueSpecifiedByCourseSettings(result.score, course) } }} + {{ 'artemisApp.result.resultString.automaticAIFeedbackSuccessfulTooltip' | artemisTranslate }} </span> } @else { - @if (exercise && exercise.maxPoints) { + @if (!exercise?.maxPoints) { <span> - {{ - 'artemisApp.result.scoreWithPoints' - | artemisTranslate - : { - score: roundValueSpecifiedByCourseSettings(result.score ?? 0, course), - points: roundValueSpecifiedByCourseSettings(((result.score ?? 0) * exercise.maxPoints) / 100, course) - } - }} + {{ 'artemisApp.result.score' | artemisTranslate: { score: roundValueSpecifiedByCourseSettings(result.score, course) } }} </span> + } @else { + @if (exercise && exercise.maxPoints) { + <span> + {{ + 'artemisApp.result.scoreWithPoints' + | artemisTranslate + : { + score: roundValueSpecifiedByCourseSettings(result.score ?? 0, course), + points: roundValueSpecifiedByCourseSettings(((result.score ?? 0) * exercise.maxPoints) / 100, course) + } + }} + </span> + } } } </h4> @@ -84,7 +90,7 @@ <h4> </div> } - @if (showScoreChart && result.participation?.exercise) { + @if (result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA && showScoreChart && result.participation?.exercise) { <div class="result-score-chart"> <div id="feedback-chart" #containerRef class="chart-space"> <ngx-charts-bar-horizontal-stacked diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts b/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts index a51c65e2b66c..8fe19b908551 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts @@ -213,7 +213,6 @@ export class FeedbackComponent implements OnInit, OnChanges { const filteredFeedback = this.feedbackService.filterFeedback(feedbacks, this.feedbackFilter); checkSubsequentFeedbackInAssessment(filteredFeedback); - const feedbackItems = this.feedbackItemService.create(filteredFeedback, this.showTestDetails); this.feedbackItemNodes = this.feedbackItemService.group(feedbackItems, this.exercise!); if (this.isExamReviewPage) { @@ -223,6 +222,7 @@ export class FeedbackComponent implements OnInit, OnChanges { // If we don't receive a submission or the submission is marked with buildFailed, fetch the build logs. if ( + this.result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA && this.exerciseType === ExerciseType.PROGRAMMING && this.result.participation && (!this.result.submission || (this.result.submission as ProgrammingSubmission).buildFailed) diff --git a/src/main/webapp/app/exercises/shared/feedback/item/programming-feedback-item.service.ts b/src/main/webapp/app/exercises/shared/feedback/item/programming-feedback-item.service.ts index ec3fa68e540f..03b491da0195 100644 --- a/src/main/webapp/app/exercises/shared/feedback/item/programming-feedback-item.service.ts +++ b/src/main/webapp/app/exercises/shared/feedback/item/programming-feedback-item.service.ts @@ -6,6 +6,7 @@ import { FEEDBACK_SUGGESTION_IDENTIFIER, Feedback, FeedbackType, + NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER, STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER, SUBMISSION_POLICY_FEEDBACK_IDENTIFIER, } from 'app/entities/feedback.model'; @@ -49,8 +50,10 @@ export class ProgrammingFeedbackItemService implements FeedbackItemService { return this.createScaFeedbackItem(feedback, showTestDetails); } else if (Feedback.isFeedbackSuggestion(feedback)) { return this.createFeedbackSuggestionItem(feedback, showTestDetails); - } else if (feedback.type === FeedbackType.AUTOMATIC) { + } else if (feedback.type === FeedbackType.AUTOMATIC && !Feedback.isNonGradedFeedbackSuggestion(feedback)) { return this.createAutomaticFeedbackItem(feedback, showTestDetails); + } else if (feedback.type === FeedbackType.AUTOMATIC && Feedback.isNonGradedFeedbackSuggestion(feedback)) { + return this.createNonGradedFeedbackItem(feedback); } else if ((feedback.type === FeedbackType.MANUAL || feedback.type === FeedbackType.MANUAL_UNREFERENCED) && feedback.gradingInstruction) { return this.createGradingInstructionFeedbackItem(feedback, showTestDetails); } else { @@ -90,7 +93,10 @@ export class ProgrammingFeedbackItemService implements FeedbackItemService { return { type: 'Static Code Analysis', name: this.translateService.instant('artemisApp.result.detail.codeIssue.name'), - title: this.translateService.instant('artemisApp.result.detail.codeIssue.title', { scaCategory, location: this.getIssueLocation(scaIssue) }), + title: this.translateService.instant('artemisApp.result.detail.codeIssue.title', { + scaCategory, + location: this.getIssueLocation(scaIssue), + }), text, positive: false, credits: scaIssue.penalty ? -scaIssue.penalty : feedback.credits, @@ -140,7 +146,6 @@ export class ProgrammingFeedbackItemService implements FeedbackItemService { : this.translateService.instant('artemisApp.result.detail.test.failed', { name: feedback.testCase.testName }); } } - return { type: 'Test', name: this.translateService.instant('artemisApp.result.detail.test.name'), @@ -152,6 +157,18 @@ export class ProgrammingFeedbackItemService implements FeedbackItemService { }; } + private createNonGradedFeedbackItem(feedback: Feedback): FeedbackItem { + return { + type: 'Reviewer', + name: this.translateService.instant('artemisApp.result.detail.feedback'), + title: feedback.text?.slice(NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER.length), + text: feedback.detailText, + positive: feedback.positive, + credits: feedback.credits, + feedbackReference: feedback, + }; + } + /** * Creates a feedback item for a manual feedback where the tutor used a grading instruction. * @param feedback The manual feedback where a grading instruction was used. @@ -196,12 +213,18 @@ export class ProgrammingFeedbackItemService implements FeedbackItemService { const lineText = !issue.endLine || issue.startLine === issue.endLine ? this.translateService.instant('artemisApp.result.detail.codeIssue.line', { line: issue.startLine }) - : this.translateService.instant('artemisApp.result.detail.codeIssue.lines', { from: issue.startLine, to: issue.endLine }); + : this.translateService.instant('artemisApp.result.detail.codeIssue.lines', { + from: issue.startLine, + to: issue.endLine, + }); if (issue.startColumn) { const columnText = !issue.endColumn || issue.startColumn === issue.endColumn ? this.translateService.instant('artemisApp.result.detail.codeIssue.column', { column: issue.startColumn }) - : this.translateService.instant('artemisApp.result.detail.codeIssue.columns', { from: issue.startColumn, to: issue.endColumn }); + : this.translateService.instant('artemisApp.result.detail.codeIssue.columns', { + from: issue.startColumn, + to: issue.endColumn, + }); return `${issue.filePath} ${lineText} ${columnText}`; } return `${issue.filePath} ${lineText}`; diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index 405464238dc0..3b56013e747a 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -10,6 +10,33 @@ <span id="test-building" jhiTranslate="artemisApp.editor.building"></span> </span> } + @case (ResultTemplateStatus.FEEDBACK_GENERATION_FAILED) { + @if (result) { + <span [ngClass]="textColorClass" class="guided-tour result" [class.clickable-result]="false" id="result-score"> + <span [ngbTooltip]="resultTooltip | artemisTranslate"> + {{ resultString }} + </span> + </span> + } + } + @case (ResultTemplateStatus.IS_GENERATING_FEEDBACK) { + @if (result) { + <span [ngClass]="textColorClass" class="guided-tour result" [class.clickable-result]="false" id="result-score"> + <span [ngbTooltip]="resultTooltip | artemisTranslate"> + {{ resultString }} + </span> + </span> + } + } + @case (ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT) { + @if (result) { + <span [ngClass]="textColorClass" class="guided-tour result" [class.clickable-result]="false" id="result-score"> + <span [ngbTooltip]="resultTooltip | artemisTranslate"> + {{ resultString }} + </span> + </span> + } + } @case (ResultTemplateStatus.HAS_RESULT) { @if (result) { @if (showIcon) { diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index bd98d27e87e9..3295c09a4699 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit, Optional, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { MissingResultInformation, ResultTemplateStatus, evaluateTemplateStatus, getResultIconClass, getTextColorClass } from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -34,7 +34,7 @@ import { prepareFeedbackComponentParameters } from 'app/exercises/shared/feedbac * When using the result component make sure that the reference to the participation input is changed if the result changes * e.g. by using Object.assign to trigger ngOnChanges which makes sure that the result is updated */ -export class ResultComponent implements OnInit, OnChanges { +export class ResultComponent implements OnInit, OnChanges, OnDestroy { // make constants available to html readonly ResultTemplateStatus = ResultTemplateStatus; readonly MissingResultInfo = MissingResultInformation; @@ -70,6 +70,8 @@ export class ResultComponent implements OnInit, OnChanges { readonly faExclamationCircle = faExclamationCircle; readonly faExclamationTriangle = faExclamationTriangle; + private resultUpdateSubscription?: ReturnType<typeof setTimeout>; + constructor( private participationService: ParticipationService, private translateService: TranslateService, @@ -104,8 +106,16 @@ export class ResultComponent implements OnInit, OnChanges { }); } // Make sure result and participation are connected - this.result = this.participation.results[0]; - this.result.participation = this.participation; + if (!this.showUngradedResults) { + const firstRatedResult = this.participation.results.find((result) => result.rated); + if (firstRatedResult) { + this.result = firstRatedResult; + this.result.participation = this.participation; + } + } else { + this.result = this.participation.results[0]; + this.result.participation = this.participation; + } } } else if (!this.participation && this.result && this.result.participation) { // make sure this.participation is initialized in case it was not passed @@ -136,6 +146,12 @@ export class ResultComponent implements OnInit, OnChanges { } } + ngOnDestroy(): void { + if (this.resultUpdateSubscription) { + clearTimeout(this.resultUpdateSubscription); + } + } + /** * Executed when changes happen sets the corresponding template status to display a message. * @param changes The hashtable of the occurred changes as SimpleChanges object. @@ -169,7 +185,10 @@ export class ResultComponent implements OnInit, OnChanges { this.textColorClass = getTextColorClass(this.result, this.templateStatus); this.resultIconClass = getResultIconClass(this.result, this.templateStatus); this.resultString = this.resultService.getResultString(this.result, this.exercise, this.short); - } else if (this.result && this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) { + } else if ( + this.result && + ((this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) || Result.isAthenaAIResult(this.result)) + ) { this.textColorClass = getTextColorClass(this.result, this.templateStatus); this.resultIconClass = getResultIconClass(this.result, this.templateStatus); this.resultString = this.resultService.getResultString(this.result, this.exercise, this.short); @@ -181,6 +200,16 @@ export class ResultComponent implements OnInit, OnChanges { this.result = undefined; this.resultString = ''; } + + if (this.templateStatus === ResultTemplateStatus.IS_GENERATING_FEEDBACK && this.result?.completionDate) { + const dueTime = -dayjs().diff(this.result.completionDate, 'milliseconds'); + this.resultUpdateSubscription = setTimeout(() => { + this.evaluate(); + if (this.resultUpdateSubscription) { + clearTimeout(this.resultUpdateSubscription); + } + }, dueTime); + } } /** @@ -189,6 +218,19 @@ export class ResultComponent implements OnInit, OnChanges { buildResultTooltip(): string | undefined { // Only show the 'preliminary' tooltip for programming student participation results and if the buildAndTestAfterDueDate has not passed. const programmingExercise = this.exercise as ProgrammingExercise; + + // Automatically generated feedback section + if (this.result) { + if (this.templateStatus === ResultTemplateStatus.FEEDBACK_GENERATION_FAILED) { + return 'artemisApp.result.resultString.automaticAIFeedbackFailedTooltip'; + } else if (this.templateStatus === ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT) { + return 'artemisApp.result.resultString.automaticAIFeedbackTimedOutTooltip'; + } else if (this.templateStatus === ResultTemplateStatus.IS_GENERATING_FEEDBACK) { + return 'artemisApp.result.resultString.automaticAIFeedbackInProgressTooltip'; + } else if (this.templateStatus === ResultTemplateStatus.HAS_RESULT && Result.isAthenaAIResult(this.result)) { + return 'artemisApp.result.resultString.automaticAIFeedbackSuccessfulTooltip'; + } + } if ( this.participation && isProgrammingExerciseStudentParticipation(this.participation) && diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index f6e07de4a961..f43a4e90dbb8 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -19,7 +19,13 @@ import { ProgrammingSubmission } from 'app/entities/programming-submission.model import { captureException } from '@sentry/angular-ivy'; import { Participation, ParticipationType } from 'app/entities/participation/participation.model'; import { SubmissionService } from 'app/exercises/shared/submission/submission.service'; -import { isStudentParticipation } from 'app/exercises/shared/result/result.utils'; +import { + isAIResultAndFailed, + isAIResultAndIsBeingProcessed, + isAIResultAndProcessed, + isAIResultAndTimedOut, + isStudentParticipation, +} from 'app/exercises/shared/result/result.utils'; export type EntityResponseType = HttpResponse<Result>; export type EntityArrayResponseType = HttpResponse<Result[]>; @@ -124,6 +130,14 @@ export class ResultService implements IResultService { let buildAndTestMessage: string; if (result.submission && (result.submission as ProgrammingSubmission).buildFailed) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildFailed'); + } else if (isAIResultAndFailed(result)) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackFailed'); + } else if (isAIResultAndIsBeingProcessed(result)) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); + } else if (isAIResultAndTimedOut(result)) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackTimedOut'); + } else if (isAIResultAndProcessed(result)) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); } else if (!result.testCaseCount) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildSuccessfulNoTests'); } else { @@ -151,6 +165,9 @@ export class ResultService implements IResultService { * @param short flag that indicates if the resultString should use the short format */ private getBaseResultStringProgrammingExercise(result: Result, relativeScore: number, points: number, buildAndTestMessage: string, short: boolean | undefined): string { + if (Result.isAthenaAIResult(result)) { + return buildAndTestMessage; + } if (short) { if (!result.testCaseCount) { return this.translateService.instant('artemisApp.result.resultString.programmingShort', { diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index e51a3dc6b7cc..9a997cd2be69 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -28,6 +28,21 @@ export enum ResultTemplateStatus { * This is currently only relevant for programming exercises. */ IS_BUILDING = 'IS_BUILDING', + /** + * An automatic feedback suggestion is currently being generated and should be available soon. + * This is currently only relevant for programming exercises. + */ + IS_GENERATING_FEEDBACK = 'IS_GENERATING_FEEDBACK', + /** + * An automatic feedback suggestion has failed. + * This is currently only relevant for programming exercises. + */ + FEEDBACK_GENERATION_FAILED = 'FEEDBACK_GENERATION_FAILED', + /** + * The generation of an automatic feedback suggestion was in progress, but did not return a result. + * This is currently only relevant for programming exercises. + */ + FEEDBACK_GENERATION_TIMED_OUT = 'FEEDBACK_GENERATION_TIMED_OUT', /** * A regular, finished result is available. * Can be rated (counts toward the score) or not rated (after the due date for practice). @@ -103,6 +118,22 @@ export const getUnreferencedFeedback = (feedbacks: Feedback[] | undefined): Feed return feedbacks ? feedbacks.filter((feedbackElement) => !feedbackElement.reference && feedbackElement.type === FeedbackType.MANUAL_UNREFERENCED) : undefined; }; +export function isAIResultAndFailed(result: Result | undefined) { + return result && Result.isAthenaAIResult(result) && result.successful === false; +} + +export function isAIResultAndTimedOut(result: Result | undefined) { + return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isAfter(result.completionDate); +} + +export function isAIResultAndProcessed(result: Result | undefined) { + return result && Result.isAthenaAIResult(result) && result.successful === true; +} + +export function isAIResultAndIsBeingProcessed(result: Result | undefined) { + return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isSameOrBefore(result.completionDate); +} + export const evaluateTemplateStatus = ( exercise: Exercise | undefined, participation: Participation | undefined, @@ -168,6 +199,14 @@ export const evaluateTemplateStatus = ( if (isProgrammingOrQuiz(participation)) { if (isBuilding) { return ResultTemplateStatus.IS_BUILDING; + } else if (isAIResultAndIsBeingProcessed(result)) { + return ResultTemplateStatus.IS_GENERATING_FEEDBACK; + } else if (isAIResultAndProcessed(result)) { + return ResultTemplateStatus.HAS_RESULT; + } else if (isAIResultAndFailed(result)) { + return ResultTemplateStatus.FEEDBACK_GENERATION_FAILED; + } else if (isAIResultAndTimedOut(result)) { + return ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT; } else if (initializedResultWithScore(result)) { return ResultTemplateStatus.HAS_RESULT; } else { @@ -208,11 +247,11 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re return 'result-late'; } - if (isBuildFailedAndResultIsAutomatic(result)) { + if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { return 'text-danger'; } - if (resultIsPreliminary(result)) { + if (resultIsPreliminary(result) || isAIResultAndIsBeingProcessed(result) || isAIResultAndTimedOut(result)) { return 'text-secondary'; } @@ -244,11 +283,11 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R return faQuestionCircle; } - if (isBuildFailedAndResultIsAutomatic(result)) { + if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { return faTimesCircle; } - if (resultIsPreliminary(result)) { + if (resultIsPreliminary(result) || isAIResultAndTimedOut(result) || isAIResultAndIsBeingProcessed(result)) { return faQuestionCircle; } diff --git a/src/main/webapp/app/iris/iris.module.ts b/src/main/webapp/app/iris/iris.module.ts index f7599a8023c0..250223060c95 100644 --- a/src/main/webapp/app/iris/iris.module.ts +++ b/src/main/webapp/app/iris/iris.module.ts @@ -14,14 +14,11 @@ import { IrisCommonSubSettingsUpdateComponent } from './settings/iris-settings-u import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; import { IrisLogoComponent } from './iris-logo/iris-logo.component'; -import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; -import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; import { IrisGlobalAutoupdateSettingsUpdateComponent } from './settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { IrisTutorChatbotButtonComponent } from 'app/iris/exercise-chatbot/tutor-chatbot-button.component'; import { IrisStateStore } from 'app/iris/state-store.service'; import { IrisChatbotWidgetComponent } from 'app/iris/exercise-chatbot/widget/chatbot-widget.component'; import { IrisEnabledComponent } from 'app/iris/settings/shared/iris-enabled.component'; -import { IrisCompetencyGenerationSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component'; import { IrisLogoButtonComponent } from 'app/iris/iris-logo-button/iris-logo-button.component'; import { FeatureToggleModule } from 'app/shared/feature-toggle/feature-toggle.module'; @@ -36,9 +33,6 @@ import { FeatureToggleModule } from 'app/shared/feature-toggle/feature-toggle.mo IrisExerciseSettingsUpdateComponent, IrisCommonSubSettingsUpdateComponent, IrisLogoComponent, - IrisChatSubSettingsUpdateComponent, - IrisHestiaSubSettingsUpdateComponent, - IrisCompetencyGenerationSubSettingsUpdateComponent, IrisGlobalAutoupdateSettingsUpdateComponent, IrisEnabledComponent, IrisLogoButtonComponent, diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html deleted file mode 100644 index 8ddb1563fa3d..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html +++ /dev/null @@ -1,34 +0,0 @@ -@if (rateLimitSettable) { - <div> - <label class="form-check-label" for="rateLimit" jhiTranslate="artemisApp.iris.settings.subSettings.rateLimit"></label> - <jhi-help-icon [text]="'artemisApp.iris.settings.subSettings.rateLimitTooltip'" /> - <input id="rateLimit" name="rateLimit" class="form-control" type="number" [customMin]="-1" [customMax]="1000000" [(ngModel)]="subSettings!.rateLimit" /> - </div> -} -@if (rateLimitSettable) { - <div> - <label class="form-check-label" for="rateLimitTimeframeHours" jhiTranslate="artemisApp.iris.settings.subSettings.rateLimitTimeframeHours" - >Rate Limit Timeframe (Hours)</label - > - <jhi-help-icon [text]="'artemisApp.iris.settings.subSettings.rateLimitTimeframeHoursTooltip'" /> - <input - id="rateLimitTimeframeHours" - name="rateLimitTimeframeHours" - class="form-control" - type="number" - [customMin]="0" - [customMax]="1000000" - [(ngModel)]="subSettings!.rateLimitTimeframeHours" - /> - </div> -} -<div class="mb-3 mt-3"> - <h4 jhiTranslate="artemisApp.iris.settings.subSettings.template.title"></h4> - @if (parentSubSettings) { - <div class="form-check form-switch"> - <input class="form-check-input" type="checkbox" id="inheritTemplate" [checked]="!subSettings?.template" (change)="onInheritTemplateChanged()" /> - <label class="form-check-label" for="inheritTemplate" jhiTranslate="artemisApp.iris.settings.subSettings.template.inherit"></label> - </div> - } - <textarea id="template-editor" class="form-control" rows="25" [(ngModel)]="templateContent" (change)="onTemplateChanged()" [disabled]="!subSettings?.template"></textarea> -</div> diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts deleted file mode 100644 index df6192fa55e3..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisChatSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; - -@Component({ - selector: 'jhi-iris-chat-sub-settings-update', - templateUrl: './iris-chat-sub-settings-update.component.html', -}) -export class IrisChatSubSettingsUpdateComponent implements OnInit, OnChanges { - @Input() - subSettings?: IrisChatSubSettings; - - @Input() - parentSubSettings?: IrisChatSubSettings; - - @Input() - rateLimitSettable = false; - - @Output() - onChanges = new EventEmitter<IrisSubSettings>(); - - previousTemplate?: IrisTemplate; - - isAdmin: boolean; - - templateContent: string; - - ngOnInit(): void { - this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.subSettings || changes.parentSubSettings) { - this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - } - - onInheritTemplateChanged() { - if (this.subSettings?.template) { - this.previousTemplate = this.subSettings?.template; - this.subSettings.template = undefined; - this.templateContent = this.parentSubSettings?.template?.content ?? ''; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = ''; - this.subSettings!.template = this.previousTemplate ?? irisTemplate; - } - } - - onTemplateChanged() { - if (this.subSettings?.template) { - this.subSettings.template.content = this.templateContent; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = this.templateContent; - this.subSettings!.template = irisTemplate; - } - } -} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html index a8b337b49257..e231aae3390b 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html @@ -24,7 +24,7 @@ <h4 class="form-label mt-3" jhiTranslate="artemisApp.iris.settings.subSettings.m class="form-check-input" type="checkbox" id="inheritAllowedModelsSwitch{{ subSettings?.type ?? '' }}" - [disabled]="!isAdmin" + [disabled]="!isAdmin && false // TODO: Reimplement in the future" [(ngModel)]="inheritAllowedModels" (change)="onInheritAllowedModelsChange()" /> @@ -42,7 +42,7 @@ <h4 class="form-label mt-3" jhiTranslate="artemisApp.iris.settings.subSettings.m class="form-check-input" type="checkbox" id="{{ model.id }}{{ subSettings?.type ?? '' }}" - [disabled]="!isAdmin || inheritAllowedModels" + [disabled]="(!isAdmin || inheritAllowedModels) && false // TODO: Reimplement in the future" [ngModel]="allowedIrisModels.includes(model)" (ngModelChange)="onAllowedIrisModelsSelectionChange(model)" /> @@ -52,26 +52,3 @@ <h4 class="form-label mt-3" jhiTranslate="artemisApp.iris.settings.subSettings.m </div> } </div> -<h5 class="mt-3"><span class="fw-bold" jhiTranslate="artemisApp.iris.settings.subSettings.models.preferredModel.title"></span>:</h5> -<div class="d-flex align-items-center"> - <div ngbDropdown class="d-inline-block"> - <button class="btn btn-outline-primary w-100" id="dropdownBasic1" ngbDropdownToggle> - {{ getPreferredModelName() ?? ('artemisApp.iris.settings.subSettings.models.preferredModel.inherit' | artemisTranslate) }} - </button> - <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> - @if (parentSubSettings) { - <button (click)="setModel(undefined)" [class.selected]="subSettings?.preferredModel === undefined" ngbDropdownItem> - {{ 'artemisApp.iris.settings.subSettings.models.preferredModel.inherit' | artemisTranslate }} - </button> - } - @for (model of allowedIrisModels; track model) { - <button (click)="setModel(model)" [class.selected]="model.id === subSettings?.preferredModel" [ngbTooltip]="model.description" ngbDropdownItem> - {{ model.name }} - </button> - } - </div> - </div> - @if (!subSettings?.preferredModel) { - <span class="ps-2 text-secondary">{{ getPreferredModelNameParent() }}</span> - } -</div> diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.html deleted file mode 100644 index 1c8df0c86cd0..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.html +++ /dev/null @@ -1,23 +0,0 @@ -<div class="mb-3 mt-3"> - <h4 jhiTranslate="artemisApp.iris.settings.subSettings.template.title"></h4> - @if (parentSubSettings) { - <div class="form-check form-switch"> - <input - class="form-check-input" - type="checkbox" - id="inheritTemplate{{ subSettings?.type ?? '' }}" - [checked]="!subSettings?.template" - (change)="onInheritTemplateChanged()" - /> - <label class="form-check-label" for="inheritTemplate{{ subSettings?.type ?? '' }}" jhiTranslate="artemisApp.iris.settings.subSettings.template.inherit"></label> - </div> - } - <textarea - id="template-editor{{ subSettings?.type ?? '' }}" - class="form-control" - rows="25" - [(ngModel)]="templateContent" - (change)="onTemplateChanged()" - [disabled]="!subSettings?.template" - ></textarea> -</div> diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.ts deleted file mode 100644 index b3814d7696e1..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisCompetencyGenerationSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; - -@Component({ - selector: 'jhi-iris-competency-generation-sub-settings-update', - templateUrl: './iris-competency-generation-sub-settings-update.component.html', -}) -export class IrisCompetencyGenerationSubSettingsUpdateComponent implements OnInit, OnChanges { - @Input() - subSettings: IrisCompetencyGenerationSubSettings; - - @Input() - parentSubSettings?: IrisCompetencyGenerationSubSettings; - - @Output() - onChanges = new EventEmitter<IrisSubSettings>(); - - previousTemplate?: IrisTemplate; - - isAdmin: boolean; - - templateContent: string; - - ngOnInit(): void { - this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.subSettings || changes.parentSubSettings) { - this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - } - - onInheritTemplateChanged() { - if (this.subSettings.template) { - this.previousTemplate = this.subSettings.template; - this.subSettings.template = undefined; - this.templateContent = this.parentSubSettings?.template?.content ?? ''; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = ''; - this.subSettings.template = this.previousTemplate ?? irisTemplate; - } - } - - onTemplateChanged() { - if (this.subSettings.template) { - this.subSettings.template.content = this.templateContent; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = this.templateContent; - this.subSettings.template = irisTemplate; - } - } -} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html deleted file mode 100644 index 0f9b1feb0bc1..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html +++ /dev/null @@ -1,10 +0,0 @@ -<div class="mb-3 mt-3"> - <h4 jhiTranslate="artemisApp.iris.settings.subSettings.template.title"></h4> - @if (parentSubSettings) { - <div class="form-check form-switch"> - <input class="form-check-input" type="checkbox" id="inheritTemplate" [checked]="!subSettings?.template" (change)="onInheritTemplateChanged()" /> - <label class="form-check-label" for="inheritTemplate" jhiTranslate="artemisApp.iris.settings.subSettings.template.inherit"></label> - </div> - } - <textarea id="template-editor" class="form-control" rows="25" [(ngModel)]="templateContent" (change)="onTemplateChanged()" [disabled]="!subSettings?.template"></textarea> -</div> diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts deleted file mode 100644 index ea7e98eac424..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisHestiaSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; - -@Component({ - selector: 'jhi-iris-hestia-sub-settings-update', - templateUrl: './iris-hestia-sub-settings-update.component.html', -}) -export class IrisHestiaSubSettingsUpdateComponent implements OnInit, OnChanges { - @Input() - subSettings: IrisHestiaSubSettings; - - @Input() - parentSubSettings?: IrisHestiaSubSettings; - - @Output() - onChanges = new EventEmitter<IrisSubSettings>(); - - previousTemplate?: IrisTemplate; - - isAdmin: boolean; - - templateContent: string; - - ngOnInit(): void { - this.templateContent = this.subSettings.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.subSettings || changes.parentSubSettings) { - this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; - } - } - - onInheritTemplateChanged() { - if (this.subSettings.template) { - this.previousTemplate = this.subSettings.template; - this.subSettings.template = undefined; - this.templateContent = this.parentSubSettings?.template?.content ?? ''; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = ''; - this.subSettings.template = this.previousTemplate ?? irisTemplate; - } - } - - onTemplateChanged() { - if (this.subSettings.template) { - this.subSettings.template.content = this.templateContent; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = this.templateContent; - this.subSettings.template = irisTemplate; - } - } -} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html index 2c21391e690b..53e9644fec41 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html @@ -26,12 +26,6 @@ <h3 jhiTranslate="artemisApp.iris.settings.subSettings.chatSettings"></h3> [settingsType]="settingsType" (onChanges)="isDirty = true" /> - <jhi-iris-chat-sub-settings-update - [subSettings]="irisSettings?.irisChatSettings" - [parentSubSettings]="parentIrisSettings?.irisChatSettings" - [rateLimitSettable]="settingsType === GLOBAL" - (onChanges)="isDirty = true" - /> </div> @if (settingsType !== EXERCISE) { <div> @@ -44,11 +38,6 @@ <h3 jhiTranslate="artemisApp.iris.settings.subSettings.hestiaSettings"></h3> [settingsType]="settingsType" (onChanges)="isDirty = true" /> - <jhi-iris-hestia-sub-settings-update - [subSettings]="irisSettings?.irisHestiaSettings!" - [parentSubSettings]="parentIrisSettings?.irisHestiaSettings" - (onChanges)="isDirty = true" - /> </div> } @if (settingsType !== EXERCISE) { @@ -62,11 +51,6 @@ <h3 jhiTranslate="artemisApp.iris.settings.subSettings.competencyGenerationSetti [settingsType]="settingsType" (onChanges)="isDirty = true" /> - <jhi-iris-competency-generation-sub-settings-update - [subSettings]="irisSettings?.irisCompetencyGenerationSettings!" - [parentSubSettings]="parentIrisSettings?.irisCompetencyGenerationSettings" - (onChanges)="isDirty = true" - /> </div> } </div> diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts index f4f46c7ce255..edacfddfd735 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts @@ -76,7 +76,8 @@ export class IrisSettingsUpdateComponent implements OnInit, DoCheck, ComponentCa loadIrisSettings(): void { this.isLoading = true; this.loadIrisSettingsObservable().subscribe((settings) => { - this.loadIrisModels(); + //this.loadIrisModels(); + this.isLoading = false; if (!settings) { this.alertService.error('artemisApp.iris.settings.error.noSettings'); } 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 bfa4d64ea9a2..932fb8442f9e 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 @@ -88,7 +88,7 @@ export class AttachmentUnitFormComponent implements OnInit, OnChanges { name: [undefined as string | undefined, [Validators.required, Validators.maxLength(255)]], description: [undefined as string | undefined, [Validators.maxLength(1000)]], releaseDate: [undefined as dayjs.Dayjs | undefined], - version: [{ value: 1, disabled: true, readOnly: true }], + version: [{ value: 1, disabled: true }], updateNotificationText: [undefined as string | undefined, [Validators.maxLength(1000)]], competencies: [undefined as Competency[] | undefined], }); diff --git a/src/main/webapp/app/localci/build-agents/build-agents.component.html b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html similarity index 71% rename from src/main/webapp/app/localci/build-agents/build-agents.component.html rename to src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html index 2d8f22e20997..3261d51b249d 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html @@ -1,108 +1,96 @@ -<div style="padding-bottom: 60px"> - <h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.title"></h3> - @if (buildAgents) { - <p>{{ buildAgents.length }} online agent(s): {{ currentBuilds }} of {{ buildCapacity }} build jobs running</p> - <div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3"></div> - <jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="buildAgent" [allEntities]="buildAgents!"> - <ng-template let-settings="settings" let-controls="controls"> - <ngx-datatable - class="bootstrap" - [limit]="settings.limit" - [sortType]="settings.sortType" - [columnMode]="settings.columnMode" - [headerHeight]="settings.headerHeight" - [footerHeight]="settings.footerHeight" - [rowHeight]="settings.rowHeight" - [rows]="settings.rows" - [rowClass]="settings.rowClass" - [scrollbarH]="settings.scrollbarH" - > - <ngx-datatable-column prop="name" [minWidth]="150"> - <ng-template ngx-datatable-header-template> - <span class="datatable-header-cell-wrapper" (click)="controls.onSort('name')"> - <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.name"></span> - <fa-icon [icon]="controls.iconForSortPropField('name')" /> - </span> - </ng-template> - <ng-template ngx-datatable-cell-template let-value="value"> - <span>{{ value }}</span> - </ng-template> - </ngx-datatable-column> - <ngx-datatable-column prop="status" [minWidth]="150"> - <ng-template ngx-datatable-header-template> - <span class="datatable-header-cell-wrapper" (click)="controls.onSort('status')"> - <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.status"></span> - <fa-icon [icon]="controls.iconForSortPropField('status')" /> - </span> - </ng-template> - <ng-template ngx-datatable-cell-template let-value="value"> - @if (value) { - <span jhiTranslate="artemisApp.buildAgents.running"></span> - } @else { - <span jhiTranslate="artemisApp.buildAgents.idle"></span> - } - </ng-template> - </ngx-datatable-column> - <ngx-datatable-column prop="maxNumberOfConcurrentBuildJobs" [minWidth]="100"> - <ng-template ngx-datatable-header-template> - <span class="datatable-header-cell-wrapper" (click)="controls.onSort('maxNumberOfConcurrentBuildJobs')"> - <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.maxNumberOfConcurrentBuildJobs"></span> - <fa-icon [icon]="controls.iconForSortPropField('maxNumberOfConcurrentBuildJobs')" /> - </span> - </ng-template> - <ng-template ngx-datatable-cell-template let-value="value"> - <span>{{ value }}</span> - </ng-template> - </ngx-datatable-column> - <ngx-datatable-column prop="numberOfCurrentBuildJobs" [minWidth]="100"> - <ng-template ngx-datatable-header-template> - <span class="datatable-header-cell-wrapper" (click)="controls.onSort('numberOfCurrentBuildJobs')"> - <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.numberOfCurrentBuildJobs"></span> - <fa-icon [icon]="controls.iconForSortPropField('numberOfCurrentBuildJobs')" /> - </span> - </ng-template> - <ng-template ngx-datatable-cell-template let-value="value" let-row="row"> - <span style="margin-right: 20px">{{ value }}</span> - @if (value > 0) { - <button class="btn btn-danger btn-sm" (click)="cancelAllBuildJobs(row.name)"> - <fa-icon [icon]="faTimes" /> - <span jhiTranslate="artemisApp.buildQueue.cancelAll"></span> - </button> - } - </ng-template> - </ngx-datatable-column> - <ngx-datatable-column prop="runningBuildJobs" [minWidth]="250"> - <ng-template ngx-datatable-header-template> - <span class="datatable-header-cell-wrapper" (click)="controls.onSort('runningBuildJobs')"> - <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.runningBuildJobs"></span> - <fa-icon [icon]="controls.iconForSortPropField('runningBuildJobs')" /> - </span> - </ng-template> - <ng-template ngx-datatable-cell-template let-value="value"> - <div> - @for (job of value; track job.id) { - <div style="display: flex; align-items: center"> - <span style="width: 200px; margin-right: 10px; text-align: left">{{ job.id }}</span> - <button style="margin-bottom: 2px" class="btn btn-danger btn-sm" (click)="cancelBuildJob(job.id)"> - <fa-icon [icon]="faTimes" /> - </button> - </div> +<div> + @if (buildAgent) { + <div style="padding-bottom: 40px"> + <div style="display: flex; align-items: center; padding-bottom: 20px"> + <h3 id="build-agent-heading" jhiTranslate="artemisApp.buildAgents.details"></h3> + <span class="h3">:</span> + <h3 id="build-agent-name" class="h3" style="margin-left: 20px">{{ buildAgent.name }}</h3> + </div> + <jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="buildAgent" [allEntities]="[buildAgent!]"> + <ng-template let-settings="settings" let-controls="controls"> + <ngx-datatable + class="bootstrap" + [limit]="settings.limit" + [sortType]="settings.sortType" + [columnMode]="settings.columnMode" + [headerHeight]="settings.headerHeight" + [footerHeight]="settings.footerHeight" + [rowHeight]="settings.rowHeight" + [rows]="settings.rows" + [rowClass]="settings.rowClass" + [scrollbarH]="settings.scrollbarH" + > + <ngx-datatable-column prop="status" [minWidth]="150"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('status')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.status"></span> + <fa-icon [icon]="controls.iconForSortPropField('status')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + @if (value) { + <span jhiTranslate="artemisApp.buildAgents.running"></span> + } @else { + <span jhiTranslate="artemisApp.buildAgents.idle"></span> } - </div> - </ng-template> - </ngx-datatable-column> - </ngx-datatable> - </ng-template> - </jhi-data-table> - } -</div> -<div style="padding-bottom: 60px"> - <h3 id="build-agents-recent-builds-heading" jhiTranslate="artemisApp.buildAgents.recentBuildJobs"></h3> - @for (agent of buildAgents; track agent.id) { - <h5 id="build-agent-recent-builds-heading" style="padding-top: 30px">{{ agent.name }}</h5> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="maxNumberOfConcurrentBuildJobs" [minWidth]="100"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('maxNumberOfConcurrentBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.maxNumberOfConcurrentBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('maxNumberOfConcurrentBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + <span>{{ value }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="numberOfCurrentBuildJobs" [minWidth]="100"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('numberOfCurrentBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.numberOfCurrentBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('numberOfCurrentBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value" let-row="row"> + <span style="margin-right: 20px">{{ value }}</span> + @if (value > 0) { + <button class="btn btn-danger btn-sm" (click)="cancelAllBuildJobs()"> + <fa-icon [icon]="faTimes" /> + <span jhiTranslate="artemisApp.buildQueue.cancelAll"></span> + </button> + } + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="runningBuildJobs" [minWidth]="250"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('runningBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.runningBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('runningBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + <div> + @for (job of value; track job.id) { + <div style="display: flex; align-items: center"> + <span style="width: 200px; margin-right: 10px; text-align: left">{{ job.id }}</span> + <button style="margin-bottom: 2px" class="btn btn-danger btn-sm" (click)="cancelBuildJob(job.id)"> + <fa-icon [icon]="faTimes" /> + </button> + </div> + } + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> + </ng-template> + </jhi-data-table> + </div> + <h4 id="build-agents-recent-builds-heading" style="padding-top: 30px" jhiTranslate="artemisApp.buildAgents.recentBuildJobs"></h4> <div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3"></div> - @if (agent.recentBuildJobs) { - <jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="build" [allEntities]="agent.recentBuildJobs!"> + @if (buildAgent.recentBuildJobs) { + <jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="build" [allEntities]="buildAgent.recentBuildJobs!"> <ng-template let-settings="settings" let-controls="controls"> <ngx-datatable class="bootstrap" diff --git a/src/main/webapp/app/localci/build-agents/build-agents.component.scss b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.scss similarity index 100% rename from src/main/webapp/app/localci/build-agents/build-agents.component.scss rename to src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.scss diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts new file mode 100644 index 000000000000..05bb0f2689c8 --- /dev/null +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts @@ -0,0 +1,108 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { BuildAgent } from 'app/entities/build-agent.model'; +import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; +import { Subscription } from 'rxjs'; +import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; +import dayjs from 'dayjs/esm'; +import { TriggeredByPushTo } from 'app/entities/repository-info.model'; +import { ActivatedRoute } from '@angular/router'; +import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; + +@Component({ + selector: 'jhi-build-agent-details', + templateUrl: './build-agent-details.component.html', + styleUrl: './build-agent-details.component.scss', +}) +export class BuildAgentDetailsComponent implements OnInit, OnDestroy { + protected readonly TriggeredByPushTo = TriggeredByPushTo; + buildAgent: BuildAgent; + agentName: string; + websocketSubscription: Subscription; + restSubscription: Subscription; + paramSub: Subscription; + channel: string; + + //icons + faCircleCheck = faCircleCheck; + faExclamationCircle = faExclamationCircle; + faExclamationTriangle = faExclamationTriangle; + faTimes = faTimes; + + constructor( + private websocketService: JhiWebsocketService, + private buildAgentsService: BuildAgentsService, + private route: ActivatedRoute, + private buildQueueService: BuildQueueService, + ) {} + + ngOnInit() { + this.paramSub = this.route.queryParams.subscribe((params) => { + this.agentName = params['agentName']; + this.channel = `/topic/admin/build-agent/${this.agentName}`; + this.load(); + this.initWebsocketSubscription(); + }); + } + + /** + * This method is used to unsubscribe from the websocket channels when the component is destroyed. + */ + ngOnDestroy() { + this.websocketService.unsubscribe(this.channel); + this.websocketSubscription?.unsubscribe(); + this.restSubscription?.unsubscribe(); + } + + /** + * This method is used to initialize the websocket subscription for the build agents. It subscribes to the channel for the build agents. + */ + initWebsocketSubscription() { + this.websocketService.subscribe(this.channel); + this.websocketSubscription = this.websocketService.receive(this.channel).subscribe((buildAgent) => { + this.updateBuildAgent(buildAgent); + }); + } + + /** + * This method is used to load the build agents. + */ + load() { + this.restSubscription = this.buildAgentsService.getBuildAgentDetails(this.agentName).subscribe((buildAgent) => { + this.updateBuildAgent(buildAgent); + }); + } + + private updateBuildAgent(buildAgent: BuildAgent) { + this.buildAgent = buildAgent; + this.setRecentBuildJobsDuration(); + } + + setRecentBuildJobsDuration() { + const recentBuildJobs = this.buildAgent.recentBuildJobs; + if (recentBuildJobs) { + for (const buildJob of recentBuildJobs) { + if (buildJob.jobTimingInfo?.buildStartDate && buildJob.jobTimingInfo?.buildCompletionDate) { + const start = dayjs(buildJob.jobTimingInfo.buildStartDate); + const end = dayjs(buildJob.jobTimingInfo.buildCompletionDate); + buildJob.jobTimingInfo.buildDuration = end.diff(start, 'milliseconds') / 1000; + } + } + } + } + + cancelBuildJob(buildJobId: string) { + this.buildQueueService.cancelBuildJob(buildJobId).subscribe(); + } + + cancelAllBuildJobs() { + if (this.buildAgent.name) { + this.buildQueueService.cancelAllRunningBuildJobsForAgent(this.buildAgent.name).subscribe(); + } + } + + viewBuildLogs(resultId: number): void { + const url = `/api/build-log/${resultId}`; + window.open(url, '_blank'); + } +} diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html new file mode 100644 index 000000000000..a9b878d14d33 --- /dev/null +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html @@ -0,0 +1,103 @@ +<div style="padding-bottom: 60px"> + <h3 id="build-agents-heading" jhiTranslate="artemisApp.buildAgents.summary"></h3> + @if (buildAgents) { + <p> + {{ buildAgents.length }} <span jhiTranslate="artemisApp.buildAgents.onlineAgents"></span>: {{ currentBuilds }} <span jhiTranslate="artemisApp.buildAgents.of"></span> + {{ buildCapacity }} <span jhiTranslate="artemisApp.buildAgents.buildJobsRunning"></span> + </p> + <div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3"></div> + <jhi-data-table [showPageSizeDropdown]="false" [showSearchField]="false" entityType="buildAgent" [allEntities]="buildAgents!"> + <ng-template let-settings="settings" let-controls="controls"> + <ngx-datatable + class="bootstrap" + [limit]="settings.limit" + [sortType]="settings.sortType" + [columnMode]="settings.columnMode" + [headerHeight]="settings.headerHeight" + [footerHeight]="settings.footerHeight" + [rowHeight]="settings.rowHeight" + [rows]="settings.rows" + [rowClass]="settings.rowClass" + [scrollbarH]="settings.scrollbarH" + > + <ngx-datatable-column prop="name" [minWidth]="150"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('name')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.name"></span> + <fa-icon [icon]="controls.iconForSortPropField('name')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + <a [routerLink]="['/admin/build-agents/details']" [queryParams]="{ agentName: value }"> + {{ value }} + </a> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="status" [minWidth]="150"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('status')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.status"></span> + <fa-icon [icon]="controls.iconForSortPropField('status')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + @if (value) { + <span jhiTranslate="artemisApp.buildAgents.running"></span> + } @else { + <span jhiTranslate="artemisApp.buildAgents.idle"></span> + } + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="maxNumberOfConcurrentBuildJobs" [minWidth]="100"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('maxNumberOfConcurrentBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.maxNumberOfConcurrentBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('maxNumberOfConcurrentBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + <span>{{ value }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="numberOfCurrentBuildJobs" [minWidth]="100"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('numberOfCurrentBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.numberOfCurrentBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('numberOfCurrentBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value" let-row="row"> + <span style="margin-right: 20px">{{ value }}</span> + @if (value > 0) { + <button class="btn btn-danger btn-sm" (click)="cancelAllBuildJobs(row.name)"> + <fa-icon [icon]="faTimes" /> + <span jhiTranslate="artemisApp.buildQueue.cancelAll"></span> + </button> + } + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column prop="runningBuildJobs" [minWidth]="250"> + <ng-template ngx-datatable-header-template> + <span class="datatable-header-cell-wrapper" (click)="controls.onSort('runningBuildJobs')"> + <span class="datatable-header-cell-label bold sortable" jhiTranslate="artemisApp.buildAgents.runningBuildJobs"></span> + <fa-icon [icon]="controls.iconForSortPropField('runningBuildJobs')" /> + </span> + </ng-template> + <ng-template ngx-datatable-cell-template let-value="value"> + <div> + @for (job of value; track job.id) { + <div style="display: flex; align-items: center"> + <span style="width: 200px; margin-right: 10px; text-align: left">{{ job.id }}</span> + <button style="margin-bottom: 2px" class="btn btn-danger btn-sm" (click)="cancelBuildJob(job.id)"> + <fa-icon [icon]="faTimes" /> + </button> + </div> + } + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> + </ng-template> + </jhi-data-table> + } +</div> diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.scss b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.scss new file mode 100644 index 000000000000..b410f3ce1e04 --- /dev/null +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.scss @@ -0,0 +1,4 @@ +.wrap-long-text { + word-break: break-all; /* This will break the text at any character */ + overflow-wrap: break-word; /* This will only break the text at whitespace */ +} diff --git a/src/main/webapp/app/localci/build-agents/build-agents.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts similarity index 63% rename from src/main/webapp/app/localci/build-agents/build-agents.component.ts rename to src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index 064b8a608522..cac89931b3be 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -3,38 +3,36 @@ import { BuildAgent } from 'app/entities/build-agent.model'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; -import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; -import dayjs from 'dayjs/esm'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; -import { TriggeredByPushTo } from 'app/entities/repository-info.model'; +import { Router } from '@angular/router'; @Component({ selector: 'jhi-build-agents', - templateUrl: './build-agents.component.html', - styleUrl: './build-agents.component.scss', + templateUrl: './build-agent-summary.component.html', + styleUrl: './build-agent-summary.component.scss', }) -export class BuildAgentsComponent implements OnInit, OnDestroy { - protected readonly TriggeredByPushTo = TriggeredByPushTo; +export class BuildAgentSummaryComponent implements OnInit, OnDestroy { buildAgents: BuildAgent[] = []; buildCapacity = 0; currentBuilds = 0; channel: string = '/topic/admin/build-agents'; websocketSubscription: Subscription; restSubscription: Subscription; + routerLink: string; //icons - faCircleCheck = faCircleCheck; - faExclamationCircle = faExclamationCircle; - faExclamationTriangle = faExclamationTriangle; faTimes = faTimes; constructor( private websocketService: JhiWebsocketService, private buildAgentsService: BuildAgentsService, private buildQueueService: BuildQueueService, + private router: Router, ) {} ngOnInit() { + this.routerLink = this.router.url; this.load(); this.initWebsocketSubscription(); } @@ -60,7 +58,6 @@ export class BuildAgentsComponent implements OnInit, OnDestroy { private updateBuildAgents(buildAgents: BuildAgent[]) { this.buildAgents = buildAgents; - this.setRecentBuildJobsDuration(buildAgents); this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0); } @@ -69,26 +66,11 @@ export class BuildAgentsComponent implements OnInit, OnDestroy { * This method is used to load the build agents. */ load() { - this.restSubscription = this.buildAgentsService.getBuildAgents().subscribe((buildAgents) => { + this.restSubscription = this.buildAgentsService.getBuildAgentSummary().subscribe((buildAgents) => { this.updateBuildAgents(buildAgents); }); } - setRecentBuildJobsDuration(buildAgents: BuildAgent[]) { - for (const buildAgent of buildAgents) { - const recentBuildJobs = buildAgent.recentBuildJobs; - if (recentBuildJobs) { - for (const buildJob of recentBuildJobs) { - if (buildJob.jobTimingInfo?.buildStartDate && buildJob.jobTimingInfo?.buildCompletionDate) { - const start = dayjs(buildJob.jobTimingInfo.buildStartDate); - const end = dayjs(buildJob.jobTimingInfo.buildCompletionDate); - buildJob.jobTimingInfo.buildDuration = end.diff(start, 'milliseconds') / 1000; - } - } - } - } - } - cancelBuildJob(buildJobId: string) { this.buildQueueService.cancelBuildJob(buildJobId).subscribe(); } @@ -99,9 +81,4 @@ export class BuildAgentsComponent implements OnInit, OnDestroy { this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgent.name).subscribe(); } } - - viewBuildLogs(resultId: number): void { - const url = `/api/build-log/${resultId}`; - window.open(url, '_blank'); - } } diff --git a/src/main/webapp/app/localci/build-agents/build-agents.service.ts b/src/main/webapp/app/localci/build-agents/build-agents.service.ts index 5ffd3095036a..1acca8c34375 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.service.ts +++ b/src/main/webapp/app/localci/build-agents/build-agents.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, throwError } from 'rxjs'; import { BuildAgent } from 'app/entities/build-agent.model'; +import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class BuildAgentsService { @@ -12,7 +13,18 @@ export class BuildAgentsService { /** * Get all build agents */ - getBuildAgents(): Observable<BuildAgent[]> { + getBuildAgentSummary(): Observable<BuildAgent[]> { return this.http.get<BuildAgent[]>(`${this.adminResourceUrl}/build-agents`); } + + /** + * Get build agent details + */ + getBuildAgentDetails(agentName: string): Observable<BuildAgent> { + return this.http.get<BuildAgent>(`${this.adminResourceUrl}/build-agent`, { params: { agentName } }).pipe( + catchError((err) => { + return throwError(() => new Error(`Failed to fetch build agent details ${agentName}\n${err.message}`)); + }), + ); + } } diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.html b/src/main/webapp/app/localci/build-queue/build-queue.component.html index 18f76b588fdd..529a5cab07d0 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.html +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.html @@ -35,7 +35,9 @@ <h3 id="build-queue-running-heading" jhiTranslate="artemisApp.buildAgents.runnin </span> </ng-template> <ng-template ngx-datatable-cell-template let-value="value"> - <span>{{ value }}</span> + <a [routerLink]="['/admin/build-agents/details']" [queryParams]="{ agentName: value }"> + {{ value }} + </a> </ng-template> </ngx-datatable-column> <ngx-datatable-column prop="name" [minWidth]="150" [width]="200"> diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.html b/src/main/webapp/app/localvc/repository-view/repository-view.component.html index 4c97d6f15afa..876027ad3372 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.html +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.html @@ -13,12 +13,21 @@ <h1>Source</h1> </a> } @if (exercise?.allowOfflineIde) { - <jhi-clone-repo-button [loading]="!!exercise.loading" [smallButtons]="false" [repositoryUri]="repositoryUri" [exercise]="exercise" style="padding-left: 15px" /> + <jhi-clone-repo-button [loading]="!!exercise.loading" [smallButtons]="false" [repositoryUri]="repositoryUri" [exercise]="exercise" /> + } + + @if (exercise.id) { + @if (repositoryType === 'USER') { + <jhi-programming-exercise-student-repo-download [buttonSize]="ButtonSize.MEDIUM" [exerciseId]="exercise.id" [participationId]="participation.id!" /> + } @else { + <jhi-programming-exercise-instructor-repo-download [repositoryType]="repositoryType" [exerciseId]="exercise.id" [buttonSize]="ButtonSize.MEDIUM" /> + } } </div> </div> <jhi-code-editor-container [useMonacoEditor]="true" + [forRepositoryView]="true" [editable]="false" [buildable]="false" [participation]="participation" diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts index 12f4d8d4c23e..9056a26834f3 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { DomainService } from 'app/exercises/programming/shared/code-editor/service/code-editor-domain.service'; import { ExerciseType, getCourseFromExercise } from 'app/entities/exercise.model'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; @@ -13,8 +13,8 @@ import { ProgrammingExerciseParticipationService } from 'app/exercises/programmi import { Result } from 'app/entities/result.model'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { faClockRotateLeft } from '@fortawesome/free-solid-svg-icons'; -import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; -import { Router } from '@angular/router'; +import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { ButtonSize } from 'app/shared/components/button.component'; @Component({ selector: 'jhi-repository-view', @@ -25,6 +25,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { PROGRAMMING = ExerciseType.PROGRAMMING; protected readonly FeatureToggle = FeatureToggle; + protected readonly ButtonSize = ButtonSize; readonly getCourseFromExercise = getCourseFromExercise; @@ -38,6 +39,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { showEditorInstructions = true; routeCommitHistory: string; repositoryUri: string; + repositoryType: ProgrammingExerciseInstructorRepositoryType | 'USER'; result: Result; @@ -79,11 +81,11 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { this.participationCouldNotBeFetched = false; const exerciseId = Number(params['exerciseId']); const participationId = Number(params['participationId']); - if (participationId) { + this.repositoryType = participationId ? 'USER' : params['repositoryType']; + if (this.repositoryType === 'USER') { this.loadStudentParticipation(participationId); } else { - const repositoryType = params['repositoryType']; - this.loadDifferentParticipation(repositoryType, exerciseId); + this.loadDifferentParticipation(this.repositoryType, exerciseId); } }); } @@ -92,10 +94,10 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { * Load the template, solution or test participation. Set the domain and repositoryUri accordingly. * If the participation can't be fetched, set the error state. The test repository does not have a participation. * Only the domain is set. - * @param repositoryType - * @param exerciseId + * @param repositoryType The instructor repository type. + * @param exerciseId The id of the exercise */ - private loadDifferentParticipation(repositoryType: string, exerciseId: number) { + private loadDifferentParticipation(repositoryType: ProgrammingExerciseInstructorRepositoryType, exerciseId: number) { this.differentParticipationSub = this.programmingExerciseService .findWithTemplateAndSolutionParticipationAndLatestResults(exerciseId) .pipe( diff --git a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html index 149201b3be60..3f55cf2e3fb6 100644 --- a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html +++ b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html @@ -5,7 +5,7 @@ </div> @if (exerciseSelected) { - <div class="vw-100 module-bg rounded-3 pb-3"> + <div class="vw-100 module-bg rounded-3"> <router-outlet /> </div> } @else { diff --git a/src/main/webapp/app/overview/course-lectures/course-lectures.component.html b/src/main/webapp/app/overview/course-lectures/course-lectures.component.html index ca048b9b7362..1337167a1aca 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lectures.component.html +++ b/src/main/webapp/app/overview/course-lectures/course-lectures.component.html @@ -5,7 +5,7 @@ </div> @if (lectureSelected) { - <div class="vw-100 module-bg rounded-3 pb-3"> + <div class="vw-100 module-bg rounded-3"> <router-outlet /> </div> } @else { diff --git a/src/main/webapp/app/overview/course-lectures/course-lectures.component.ts b/src/main/webapp/app/overview/course-lectures/course-lectures.component.ts index 93d1811f4dbb..b180ae9eec8a 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lectures.component.ts +++ b/src/main/webapp/app/overview/course-lectures/course-lectures.component.ts @@ -41,7 +41,7 @@ export class CourseLecturesComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.isCollapsed = this.courseOverviewService.getSidebarCollapseStateFromStorage('lectures'); + this.isCollapsed = this.courseOverviewService.getSidebarCollapseStateFromStorage('lecture'); this.parentParamSubscription = this.route.parent!.params.subscribe((params) => { this.courseId = parseInt(params.courseId, 10); }); @@ -57,7 +57,7 @@ export class CourseLecturesComponent implements OnInit, OnDestroy { const lastSelectedLecture = this.getLastSelectedLecture(); this.paramSubscription = this.route.params.subscribe((params) => { const lectureId = parseInt(params.lectureId, 10); - // If no exercise is selected navigate to the upcoming exercise + // If no lecture is selected navigate to the upcoming lecture if (!lectureId && lastSelectedLecture) { this.router.navigate([lastSelectedLecture], { relativeTo: this.route, replaceUrl: true }); } else if (!lectureId && upcomingLecture) { diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index cf229d320798..dc8f0793eef5 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -142,7 +142,10 @@ </button> </div> </div> - <div class="course-body-container mx-3" [ngClass]="{ 'module-bg p-3 rounded rounded-3': !hasSidebar }"> + <div + class="course-body-container mx-3" + [ngClass]="{ 'module-bg p-3 rounded rounded-3 scrollable-content': !hasSidebar, 'content-height-dev': !isProduction || isTestServer }" + > @if (!hasSidebar) { <ng-container class="d-flex ms-auto" #controlsViewContainer /> } diff --git a/src/main/webapp/app/overview/course-overview.component.scss b/src/main/webapp/app/overview/course-overview.component.scss index 9df8520487e9..8715d526342c 100644 --- a/src/main/webapp/app/overview/course-overview.component.scss +++ b/src/main/webapp/app/overview/course-overview.component.scss @@ -221,7 +221,7 @@ jhi-secured-image { .btn-sidebar-collapse { background-color: var(--link-item-bg); &:hover { - background-color: var(--gray-200); + background-color: var(--sidebar-card-selected-bg); color: var(--primary); } &:focus { diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 10cb8a1cf8fa..76d95264aa67 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -61,6 +61,7 @@ import { CourseUnenrollmentModalComponent } from './course-unenrollment-modal.co import { CourseExercisesComponent } from './course-exercises/course-exercises.component'; import { CourseLecturesComponent } from './course-lectures/course-lectures.component'; import { facSidebar } from '../../content/icons/icons'; +import { CourseTutorialGroupsComponent } from './course-tutorial-groups/course-tutorial-groups.component'; interface CourseActionItem { title: string; @@ -124,7 +125,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit private conversationServiceInstantiated = false; private checkedForUnreadMessages = false; - activatedComponentReference: CourseExercisesComponent | CourseLecturesComponent; + activatedComponentReference: CourseExercisesComponent | CourseLecturesComponent | CourseTutorialGroupsComponent; // Rendered embedded view for controls in the bar so we can destroy it if needed private controlsEmbeddedView?: EmbeddedViewRef<any>; @@ -523,7 +524,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.tryRenderControls(); }) || undefined; } - if (componentRef instanceof CourseExercisesComponent || componentRef instanceof CourseLecturesComponent) { + if (componentRef instanceof CourseExercisesComponent || componentRef instanceof CourseLecturesComponent || componentRef instanceof CourseTutorialGroupsComponent) { this.activatedComponentReference = componentRef; } diff --git a/src/main/webapp/app/overview/course-overview.scss b/src/main/webapp/app/overview/course-overview.scss index 14514e718247..a290dab112be 100644 --- a/src/main/webapp/app/overview/course-overview.scss +++ b/src/main/webapp/app/overview/course-overview.scss @@ -1,5 +1,3 @@ -$header-height: 84px; - /* ========================================================================== Course Info Bar ========================================================================== */ @@ -208,23 +206,3 @@ canvas#complete-chart { opacity: 1; } } - -.module-bg { - background-color: var(--module-bg); -} - -.scrollable-content { - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height); - overflow-y: auto; - - &.content-height-dev { - height: calc(100vh - var(--sidebar-footer-height-dev) - $header-height); - } - @media (max-width: 768px) { - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height) !important; - } -} - -.sidebar-collapsed { - display: none; -} diff --git a/src/main/webapp/app/overview/course-overview.service.ts b/src/main/webapp/app/overview/course-overview.service.ts index eb69438c2859..2e50544c28be 100644 --- a/src/main/webapp/app/overview/course-overview.service.ts +++ b/src/main/webapp/app/overview/course-overview.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { Exercise, getIcon } from 'app/entities/exercise.model'; import { Lecture } from 'app/entities/lecture.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { AccordionGroups, SidebarCardElement, TimeGroupCategory } from 'app/types/sidebar'; @@ -19,8 +21,17 @@ const DEFAULT_UNIT_GROUPS: AccordionGroups = { providedIn: 'root', }) export class CourseOverviewService { - constructor(private participationService: ParticipationService) {} - + constructor( + private participationService: ParticipationService, + private translate: TranslateService, + ) {} + + getUpcomingTutorialGroup(tutorialGroups: TutorialGroup[] | undefined): TutorialGroup | undefined { + if (tutorialGroups && tutorialGroups.length) { + const upcomingTutorialGroup = tutorialGroups?.reduce((a, b) => ((a?.nextSession?.start?.valueOf() ?? 0) > (b?.nextSession?.start?.valueOf() ?? 0) ? a : b)); + return upcomingTutorialGroup; + } + } getUpcomingLecture(lectures: Lecture[] | undefined): Lecture | undefined { if (lectures && lectures.length) { const upcomingLecture = lectures?.reduce((a, b) => ((a?.startDate?.valueOf() ?? 0) > (b?.startDate?.valueOf() ?? 0) ? a : b)); @@ -34,7 +45,7 @@ export class CourseOverviewService { } } - getCorrespondingGroupByDate(date: dayjs.Dayjs | undefined): TimeGroupCategory { + getCorrespondingExerciseGroupByDate(date: dayjs.Dayjs | undefined): TimeGroupCategory { if (!date) { return 'noDate'; } @@ -55,11 +66,31 @@ export class CourseOverviewService { return 'future'; } + getCorrespondingLectureGroupByDate(startDate: dayjs.Dayjs | undefined, endDate?: dayjs.Dayjs | undefined): TimeGroupCategory { + if (!startDate) { + return 'noDate'; + } + + const now = dayjs(); + const isStartDateWithinLastWeek = startDate.isBetween(now.subtract(1, 'week'), now); + const isDateInThePast = endDate ? endDate.isBefore(now) : startDate.isBefore(now.subtract(1, 'week')); + + if (isDateInThePast) { + return 'past'; + } + + const isDateCurrent = endDate ? startDate.isBefore(now) && endDate.isAfter(now) : isStartDateWithinLastWeek; + if (isDateCurrent) { + return 'current'; + } + return 'future'; + } + groupExercisesByDueDate(sortedExercises: Exercise[]): AccordionGroups { const groupedExerciseGroups = cloneDeep(DEFAULT_UNIT_GROUPS) as AccordionGroups; for (const exercise of sortedExercises) { - const exerciseGroup = this.getCorrespondingGroupByDate(exercise.dueDate); + const exerciseGroup = this.getCorrespondingExerciseGroupByDate(exercise.dueDate); const exerciseCardItem = this.mapExerciseToSidebarCardElement(exercise); groupedExerciseGroups[exerciseGroup].entityData.push(exerciseCardItem); } @@ -71,7 +102,7 @@ export class CourseOverviewService { const groupedLectureGroups = cloneDeep(DEFAULT_UNIT_GROUPS) as AccordionGroups; for (const lecture of sortedLectures) { - const lectureGroup = this.getCorrespondingGroupByDate(lecture.startDate); + const lectureGroup = this.getCorrespondingLectureGroupByDate(lecture.startDate, lecture?.endDate); const lectureCardItem = this.mapLectureToSidebarCardElement(lecture); groupedLectureGroups[lectureGroup].entityData.push(lectureCardItem); } @@ -82,6 +113,9 @@ export class CourseOverviewService { mapLecturesToSidebarCardElements(lectures: Lecture[]) { return lectures.map((lecture) => this.mapLectureToSidebarCardElement(lecture)); } + mapTutorialGroupsToSidebarCardElements(tutorialGroups: Lecture[]) { + return tutorialGroups.map((tutorialGroup) => this.mapTutorialGroupToSidebarCardElement(tutorialGroup)); + } mapExercisesToSidebarCardElements(exercises: Exercise[]) { return exercises.map((exercise) => this.mapExerciseToSidebarCardElement(exercise)); @@ -91,15 +125,34 @@ export class CourseOverviewService { const lectureCardItem: SidebarCardElement = { title: lecture.title ?? '', id: lecture.id ?? '', - subtitleLeft: lecture.startDate?.format('MMM DD, YYYY') ?? 'No date associated', + subtitleLeft: lecture.startDate?.format('MMM DD, YYYY') ?? this.translate.instant('artemisApp.courseOverview.sidebar.noDate'), }; return lectureCardItem; } + mapTutorialGroupToSidebarCardElement(tutorialGroup: TutorialGroup): SidebarCardElement { + const tutorialGroupCardItem: SidebarCardElement = { + title: tutorialGroup.title ?? '', + id: tutorialGroup.id ?? '', + subtitleLeft: tutorialGroup.nextSession?.start?.format('MMM DD, YYYY') ?? this.translate.instant('artemisApp.courseOverview.sidebar.noUpcomingSession'), + subtitleRight: this.getUtilization(tutorialGroup), + }; + return tutorialGroupCardItem; + } + + getUtilization(tutorialGroup: TutorialGroup): string { + if (tutorialGroup.capacity && tutorialGroup.averageAttendance) { + const utilization = Math.round((tutorialGroup.averageAttendance / tutorialGroup.capacity) * 100); + return this.translate.instant('artemisApp.entities.tutorialGroup.utilization') + ': ' + utilization + '%'; + } else { + return tutorialGroup?.averageAttendance ? 'Ø ' + this.translate.instant('artemisApp.entities.tutorialGroup.attendance') + ': ' + tutorialGroup.averageAttendance : ''; + } + } + mapExerciseToSidebarCardElement(exercise: Exercise): SidebarCardElement { const exerciseCardItem: SidebarCardElement = { title: exercise.title ?? '', id: exercise.id ?? '', - subtitleLeft: exercise.dueDate?.format('MMM DD, YYYY') ?? 'No due date', + subtitleLeft: exercise.dueDate?.format('MMM DD, YYYY') ?? this.translate.instant('artemisApp.courseOverview.sidebar.noDueDate'), type: exercise.type, icon: getIcon(exercise.type), difficulty: exercise.difficulty, diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.html b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.html deleted file mode 100644 index 219ddba8ee4e..000000000000 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.html +++ /dev/null @@ -1,12 +0,0 @@ -<div class="row"> - <div class="col-12"> - <h2>{{ 'artemisApp.pages.courseTutorialGroupOverview.title' | artemisTranslate }}</h2> - @if (tutorialGroups && tutorialGroups.length > 0) { - <jhi-tutorial-groups-table - [tutorialGroups]="tutorialGroups" - [course]="course" - [showChannelColumn]="configuration?.useTutorialGroupChannels! && isMessagingEnabled(course)" - /> - } - </div> -</div> diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.ts b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.ts deleted file mode 100644 index 7696c8a2f85e..000000000000 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; -import { Course, isMessagingEnabled } from 'app/entities/course.model'; -import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; - -@Component({ - selector: 'jhi-course-tutorial-groups-overview', - templateUrl: './course-tutorial-groups-overview.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CourseTutorialGroupsOverviewComponent { - @Input() - course: Course; - @Input() - tutorialGroups: TutorialGroup[] = []; - @Input() - configuration?: TutorialGroupsConfiguration; - - readonly isMessagingEnabled = isMessagingEnabled; -} diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.html b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.html deleted file mode 100644 index 9a30a75536c4..000000000000 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.html +++ /dev/null @@ -1,20 +0,0 @@ -<div class="row"> - <div class="col-12"> - <h2>{{ 'artemisApp.pages.courseTutorialGroups.title' | artemisTranslate }}</h2> - <div> - @if (registeredTutorialGroups && registeredTutorialGroups.length > 0) { - <div class="d-flex flex-wrap"> - @for (registeredTutorialGroup of registeredTutorialGroups; track registeredTutorialGroup) { - <jhi-course-tutorial-group-card - [tutorialGroup]="registeredTutorialGroup" - [course]="course" - [showChannelLink]="configuration?.useTutorialGroupChannels ?? false" - /> - } - </div> - } @else { - <span>{{ 'artemisApp.pages.courseTutorialGroups.noRegistrations' | artemisTranslate }}</span> - } - </div> - </div> -</div> diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.ts b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.ts deleted file mode 100644 index c3fe1ba88dd6..000000000000 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; -import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; -import { Course } from 'app/entities/course.model'; - -@Component({ - selector: 'jhi-course-tutorial-groups-registered', - templateUrl: './course-tutorial-groups-registered.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CourseTutorialGroupsRegisteredComponent { - @Input() - registeredTutorialGroups: TutorialGroup[] = []; - @Input() - course: Course; - - @Input() - configuration?: TutorialGroupsConfiguration; -} diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html index aa15d7e2e562..da05a8bc018b 100644 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html +++ b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.html @@ -1,45 +1,17 @@ -<ng-template #controls> - <div class="btn-group" role="group"> - <input - type="radio" - class="btn-check" - name="filter" - id="registered-filter" - autocomplete="off" - [value]="'registered'" - [ngModel]="selectedFilter" - (ngModelChange)="onFilterChange($event)" - /> - <label class="btn btn-outline-secondary" for="registered-filter">{{ 'artemisApp.pages.courseTutorialGroups.registeredFilter' | artemisTranslate }}</label> - <input - type="radio" - class="btn-check" - name="filter" - id="all-filter" - autocomplete="off" - [value]="'all'" - [ngModel]="selectedFilter" - (ngModelChange)="onFilterChange($event)" - /> - <label class="btn btn-outline-secondary" for="all-filter">{{ 'artemisApp.pages.courseTutorialGroups.allFilter' | artemisTranslate }}</label> - </div> -</ng-template> -<div class="my-2"> - <div class="row"> - <div class="col-12" [ngClass]="{ 'col-xl-8': tutorialGroupFreeDays && tutorialGroupFreeDays.length > 0 }"> - @switch (selectedFilter) { - @case ('all') { - <jhi-course-tutorial-groups-overview [tutorialGroups]="tutorialGroups" [course]="course" [configuration]="configuration" /> - } - @case ('registered') { - <jhi-course-tutorial-groups-registered [registeredTutorialGroups]="registeredTutorialGroups" [course]="course" [configuration]="configuration" /> - } - } +<div class="d-flex justify-content-between gap-3"> + @if (course) { + <div [ngClass]="{ 'sidebar-collapsed': isCollapsed }"> + <jhi-sidebar [itemSelected]="tutorialGroupSelected" [courseId]="courseId" [sidebarData]="sidebarData" /> </div> - @if (tutorialGroupFreeDays && tutorialGroupFreeDays.length > 0) { - <div class="col-12 col-xl-4"> - <jhi-tutorial-group-free-days-overview [tutorialGroupFreeDays]="tutorialGroupFreeDays" /> + + @if (tutorialGroupSelected) { + <div class="vw-100 module-bg rounded-3"> + <router-outlet /> + </div> + } @else { + <div class="vw-100 module-bg rounded-3 px-3 d-flex justify-content-between"> + <div class="col-12 text-center my-auto">Please Select a Tutorial Group.</div> </div> } - </div> + } </div> diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.ts b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.ts index bba317da3171..08083e557e61 100644 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.ts +++ b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.component.ts @@ -1,6 +1,5 @@ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { Subject, finalize } from 'rxjs'; -import { BarControlConfiguration } from 'app/shared/tab-bar/tab-bar'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { Subject, Subscription, finalize } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -13,43 +12,54 @@ import { AlertService } from 'app/core/util/alert.service'; import { TutorialGroupFreePeriod } from 'app/entities/tutorial-group/tutorial-group-free-day.model'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; +import { AccordionGroups, SidebarCardElement, SidebarData, TutorialGroupCategory } from 'app/types/sidebar'; +import { CourseOverviewService } from '../course-overview.service'; +import { cloneDeep } from 'lodash-es'; -type filter = 'all' | 'registered'; +const TUTORIAL_UNIT_GROUPS: AccordionGroups = { + registered: { entityData: [] }, + further: { entityData: [] }, + all: { entityData: [] }, +}; @Component({ selector: 'jhi-course-tutorial-groups', templateUrl: './course-tutorial-groups.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CourseTutorialGroupsComponent implements AfterViewInit, OnInit, OnDestroy { +export class CourseTutorialGroupsComponent implements OnInit, OnDestroy { ngUnsubscribe = new Subject<void>(); - @ViewChild('controls', { static: false }) private controls: TemplateRef<any>; - public readonly controlConfiguration: BarControlConfiguration = { - subject: new Subject<TemplateRef<any>>(), - }; tutorialGroups: TutorialGroup[] = []; courseId: number; - course: Course; + course?: Course; configuration?: TutorialGroupsConfiguration; isLoading = false; tutorialGroupFreeDays: TutorialGroupFreePeriod[] = []; + isCollapsed: boolean = false; - selectedFilter: filter = 'registered'; + tutorialGroupSelected: boolean = true; + sidebarData: SidebarData; + sortedTutorialGroups: TutorialGroup[] = []; + accordionTutorialGroupsGroups: AccordionGroups = TUTORIAL_UNIT_GROUPS; + sidebarTutorialGroups: SidebarCardElement[] = []; + private paramSubscription: Subscription; constructor( private router: Router, private courseStorageService: CourseStorageService, private courseManagementService: CourseManagementService, private tutorialGroupService: TutorialGroupsService, - private activatedRoute: ActivatedRoute, + private route: ActivatedRoute, private alertService: AlertService, private cdr: ChangeDetectorRef, + private courseOverviewService: CourseOverviewService, ) {} ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); + this.paramSubscription?.unsubscribe(); } get registeredTutorialGroups() { @@ -61,34 +71,34 @@ export class CourseTutorialGroupsComponent implements AfterViewInit, OnInit, OnD } ngOnInit(): void { - this.activatedRoute.parent?.parent?.paramMap + this.isCollapsed = this.courseOverviewService.getSidebarCollapseStateFromStorage('tutorialGroup'); + + this.route.parent?.paramMap .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((parentParams) => { this.courseId = Number(parentParams.get('courseId')); if (this.courseId) { this.setCourse(); this.setTutorialGroups(); + this.prepareSidebarData(); this.subscribeToCourseUpdates(); } }) .add(() => this.cdr.detectChanges()); - this.subscribeToQueryParameter(); - this.updateQueryParameters(); - } - - ngAfterViewInit(): void { - this.renderTopBarControls(); - } - subscribeToQueryParameter() { - this.activatedRoute.queryParams - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((queryParams) => { - if (queryParams.filter) { - this.selectedFilter = queryParams.filter as filter; - } - }) - .add(() => this.cdr.detectChanges()); + const upcomingTutorialGroup = this.courseOverviewService.getUpcomingTutorialGroup(this.course?.tutorialGroups); + const lastSelectedTutorialGroup = this.getLastSelectedTutorialGroup(); + this.paramSubscription = this.route.params.subscribe((params) => { + const tutorialGroupId = parseInt(params.tutorialGroupId, 10); + // If no tutorial group is selected navigate to the upcoming tutorial group + if (!tutorialGroupId && lastSelectedTutorialGroup) { + this.router.navigate([lastSelectedTutorialGroup], { relativeTo: this.route, replaceUrl: true }); + } else if (!tutorialGroupId && upcomingTutorialGroup) { + this.router.navigate([upcomingTutorialGroup.id], { relativeTo: this.route, replaceUrl: true }); + } else { + this.tutorialGroupSelected = tutorialGroupId ? true : false; + } + }); } subscribeToCourseUpdates() { @@ -100,10 +110,56 @@ export class CourseTutorialGroupsComponent implements AfterViewInit, OnInit, OnD this.configuration = course?.tutorialGroupsConfiguration; this.setFreeDays(); this.setTutorialGroups(); + this.prepareSidebarData(); }) .add(() => this.cdr.detectChanges()); } + prepareSidebarData() { + if (!this.course?.tutorialGroups) { + return; + } + this.sidebarTutorialGroups = this.courseOverviewService.mapTutorialGroupsToSidebarCardElements(this.tutorialGroups); + this.accordionTutorialGroupsGroups = this.groupTutorialGroupsByRegistration(); + this.updateSidebarData(); + } + + groupTutorialGroupsByRegistration(): AccordionGroups { + const groupedTutorialGroupGroups = cloneDeep(TUTORIAL_UNIT_GROUPS) as AccordionGroups; + let tutorialGroupCategory: TutorialGroupCategory; + + const hasUserAtLeastOneTutorialGroup = this.tutorialGroups.some((tutorialGroup) => tutorialGroup.isUserRegistered || tutorialGroup.isUserTutor); + this.tutorialGroups.forEach((tutorialGroup) => { + const tutorialGroupCardItem = this.courseOverviewService.mapTutorialGroupToSidebarCardElement(tutorialGroup); + if (!hasUserAtLeastOneTutorialGroup) { + tutorialGroupCategory = 'all'; + } else { + tutorialGroupCategory = tutorialGroup.isUserTutor || tutorialGroup.isUserRegistered ? 'registered' : 'further'; + } + groupedTutorialGroupGroups[tutorialGroupCategory].entityData.push(tutorialGroupCardItem); + }); + return groupedTutorialGroupGroups; + } + + updateSidebarData() { + this.sidebarData = { + groupByCategory: true, + storageId: 'tutorialGroup', + groupedData: this.accordionTutorialGroupsGroups, + ungroupedData: this.sidebarTutorialGroups, + }; + } + + toggleSidebar() { + this.isCollapsed = !this.isCollapsed; + this.courseOverviewService.setSidebarCollapseState('tutorialGroup', this.isCollapsed); + this.cdr.detectChanges(); + } + + getLastSelectedTutorialGroup(): string | null { + return sessionStorage.getItem('sidebar.lastSelectedItem.tutorialGroup.byCourse.' + this.courseId); + } + private setFreeDays() { if (this.course?.tutorialGroupsConfiguration?.tutorialGroupFreePeriods) { this.tutorialGroupFreeDays = this.course.tutorialGroupsConfiguration.tutorialGroupFreePeriods; @@ -198,28 +254,4 @@ export class CourseTutorialGroupsComponent implements AfterViewInit, OnInit, OnD }) .add(() => this.cdr.detectChanges()); } - - renderTopBarControls() { - if (this.controls) { - this.controlConfiguration.subject!.next(this.controls); - } - } - - updateQueryParameters() { - this.router - .navigate([], { - relativeTo: this.activatedRoute, - queryParams: { - filter: this.selectedFilter, - }, - replaceUrl: true, - queryParamsHandling: 'merge', - }) - .finally(() => this.cdr.detectChanges()); - } - - onFilterChange(newFilter: filter) { - this.selectedFilter = newFilter; - this.updateQueryParameters(); - } } diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.module.ts b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.module.ts index f1613ca30ddf..af87e1b67370 100644 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.module.ts +++ b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.module.ts @@ -1,15 +1,13 @@ import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; import { CourseTutorialGroupsComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups.component'; -import { routes } from './course-tutorial-groups.route'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseTutorialGroupCardComponent } from './course-tutorial-group-card/course-tutorial-group-card.component'; -import { CourseTutorialGroupsOverviewComponent } from './course-tutorial-groups-overview/course-tutorial-groups-overview.component'; import { ArtemisTutorialGroupsSharedModule } from 'app/course/tutorial-groups/shared/tutorial-groups-shared.module'; -import { CourseTutorialGroupsRegisteredComponent } from './course-tutorial-groups-registered/course-tutorial-groups-registered.component'; +import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; +import { ArtemisAppRoutingModule } from 'app/app-routing.module'; @NgModule({ - imports: [RouterModule.forChild(routes), ArtemisSharedModule, ArtemisTutorialGroupsSharedModule], - declarations: [CourseTutorialGroupsComponent, CourseTutorialGroupCardComponent, CourseTutorialGroupsOverviewComponent, CourseTutorialGroupsRegisteredComponent], + imports: [ArtemisAppRoutingModule, ArtemisSharedModule, ArtemisTutorialGroupsSharedModule, ArtemisSidebarModule], + declarations: [CourseTutorialGroupsComponent, CourseTutorialGroupCardComponent], }) export class CourseTutorialGroupsModule {} diff --git a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.route.ts b/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.route.ts deleted file mode 100644 index 8190b957524b..000000000000 --- a/src/main/webapp/app/overview/course-tutorial-groups/course-tutorial-groups.route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Routes } from '@angular/router'; -import { CourseTutorialGroupsComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups.component'; -import { Authority } from 'app/shared/constants/authority.constants'; -import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; -import { CourseTutorialGroupsOverviewComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component'; - -// parent: /courses/:courseId/tutorial-groups -export const routes: Routes = [ - { - path: '', - component: CourseTutorialGroupsComponent, - data: { - authorities: [Authority.USER], - pageTitle: 'overview.tutorialGroups', - }, - canActivate: [UserRouteAccessService], - }, - { - path: 'overview', - component: CourseTutorialGroupsOverviewComponent, - data: { - authorities: [Authority.USER], - pageTitle: 'artemisApp.pages.courseTutorialGroupOverview.title', - }, - canActivate: [UserRouteAccessService], - }, -]; diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 70a23fc9d87c..3d788143fa47 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -6,6 +6,7 @@ import { NgModule } from '@angular/core'; import { Authority } from 'app/shared/constants/authority.constants'; import { CourseExercisesComponent } from 'app/overview/course-exercises/course-exercises.component'; import { CourseOverviewComponent } from './course-overview.component'; +import { CourseTutorialGroupsComponent } from './course-tutorial-groups/course-tutorial-groups.component'; const routes: Routes = [ { @@ -122,11 +123,24 @@ const routes: Routes = [ }, { path: 'tutorial-groups', - loadChildren: () => import('./course-tutorial-groups/course-tutorial-groups.module').then((m) => m.CourseTutorialGroupsModule), + component: CourseTutorialGroupsComponent, data: { authorities: [Authority.USER], pageTitle: 'overview.tutorialGroups', + hasSidebar: true, + }, + canActivate: [UserRouteAccessService], + }, + { + path: 'tutorial-groups/:tutorialGroupId', + component: CourseTutorialGroupsComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.tutorialGroups', + hasSidebar: true, }, + canActivate: [UserRouteAccessService], + loadChildren: () => import('../overview/tutorial-group-details/course-tutorial-group-details.module').then((m) => m.CourseTutorialGroupDetailsModule), }, { path: 'exams', diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.html b/src/main/webapp/app/overview/discussion-section/discussion-section.component.html index d903307f36ae..c55b6bcc2e7b 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.html +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.html @@ -6,7 +6,7 @@ <div class="draggable-left"> <fa-icon [icon]="faGripLinesVertical" /> </div> - <div class="card"> + <div class="card mb-3"> <!-- header --> <div class="card-header text-white" [ngbTooltip]="'artemisApp.metis.communication.hide' | artemisTranslate" (click)="collapsed = true"> <div class="row flex-grow-1"> diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index c6cf2f4548ce..4c2a9f3389b1 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -138,20 +138,30 @@ <span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.openRepository"></span> </a> } - @if (exercise.allowManualFeedbackRequests) { - <span tabindex="0" [ngbTooltip]="'artemisApp.exerciseActions.requestFeedbackTooltip' | artemisTranslate"> - <button + @if (exercise.allowFeedbackRequests) { + @if (athenaEnabled) { + <a class="btn btn-primary" + (click)="requestFeedback()" [class.btn-sm]="smallButtons" - jhi-exercise-action-button - [overwriteDisabled]="isFeedbackRequestButtonDisabled()" [id]="'request-feedback-' + exercise.id" + [ngbTooltip]="'artemisApp.exerciseActions.requestAutomaticFeedbackTooltip' | artemisTranslate" + > + <fa-icon [icon]="faPenSquare" [fixedWidth]="true" /> + <span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestAutomaticFeedback">Send automatic feedback request</span> + </a> + } @else { + <a + class="btn btn-primary" (click)="requestFeedback()" + [class.btn-sm]="smallButtons" + [id]="'request-feedback-' + exercise.id" + [ngbTooltip]="'artemisApp.exerciseActions.requestManualFeedbackTooltip' | artemisTranslate" > - <fa-icon [icon]="faComment" /> - <span class="d-none d-md-inline">{{ 'artemisApp.exerciseActions.requestFeedback' | artemisTranslate }}</span> - </button> - </span> + <fa-icon [icon]="faPenSquare" [fixedWidth]="true" /> + <span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestManualFeedback">Send manual feedback request</span> + </a> + } } </ng-container> } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index 56f09cc8443d..74fcd9198a68 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -11,14 +11,15 @@ import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ArtemisQuizService } from 'app/shared/quiz/quiz.service'; import { finalize } from 'rxjs/operators'; -import { faCodeBranch, faComment, faEye, faFolderOpen, faPlayCircle, faRedo, faUsers } from '@fortawesome/free-solid-svg-icons'; +import { faCodeBranch, faEye, faFolderOpen, faPenSquare, faPlayCircle, faRedo, faUsers } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { TranslateService } from '@ngx-translate/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import dayjs from 'dayjs/esm'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_ATHENA, PROFILE_LOCALVC } from 'app/app.constants'; +import { AssessmentType } from 'app/entities/assessment-type.model'; @Component({ selector: 'jhi-exercise-details-student-actions', @@ -52,16 +53,20 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges beforeDueDate: boolean; editorLabel?: string; localVCEnabled = false; + athenaEnabled = false; + routerLink: string; repositoryLink: string; // Icons - faComment = faComment; faFolderOpen = faFolderOpen; faUsers = faUsers; faEye = faEye; faPlayCircle = faPlayCircle; faRedo = faRedo; faCodeBranch = faCodeBranch; + faPenSquare = faPenSquare; + + private feedbackSent = false; constructor( private alertService: AlertService, @@ -88,6 +93,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.programmingExercise = this.exercise as ProgrammingExercise; this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles?.includes(PROFILE_LOCALVC); + this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); }); } else if (this.exercise.type === ExerciseType.MODELING) { this.editorLabel = 'openModelingEditor'; @@ -203,19 +209,9 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } - private feedbackSent = false; - - isFeedbackRequestButtonDisabled(): boolean { - const showUngradedResults = true; - const latestResult = this.gradedParticipation?.results && this.gradedParticipation.results.find(({ rated }) => showUngradedResults || rated === true); - const allHiddenTestsPassed = latestResult?.score !== undefined && latestResult.score >= 100; - - const requestAlreadySent = (this.gradedParticipation?.individualDueDate && this.gradedParticipation.individualDueDate.isBefore(Date.now())) ?? false; - - return !allHiddenTestsPassed || requestAlreadySent || this.feedbackSent; - } - requestFeedback() { + if (!this.assureConditionsSatisfied()) return; + const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); if (!window.confirm(confirmLockRepository)) { return; @@ -282,4 +278,54 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges const participations = this.exercise.studentParticipations; return participations?.length ? participations[0].team?.id : this.exercise.studentAssignedTeamId; } + + buildPlanUrl(participation: StudentParticipation) { + return (participation as ProgrammingExerciseStudentParticipation).buildPlanUrl; + } + + /** + * Checks if the conditions for requesting automatic non-graded feedback are satisfied. + * The student can request automatic non-graded feedback under the following conditions: + * 1. They have a graded submission. + * 2. The deadline for the exercise has not been exceeded. + * 3. There is no already pending feedback request. + * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. + */ + assureConditionsSatisfied(): boolean { + this.updateParticipations(); + const latestResult = this.gradedParticipation?.results && this.gradedParticipation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); + const someHiddenTestsPassed = latestResult?.score !== undefined; + const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.notEnoughPoints'); + if (!someHiddenTestsPassed) { + window.alert(testsNotPassedWarning); + return false; + } + + const afterDueDate = !this.exercise.dueDate || dayjs().isSameOrAfter(this.exercise.dueDate); + const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); + if (afterDueDate) { + window.alert(dueDateWarning); + return false; + } + + const requestAlreadySent = (this.gradedParticipation?.individualDueDate && this.gradedParticipation.individualDueDate.isBefore(Date.now())) ?? false; + const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); + if (requestAlreadySent) { + window.alert(requestAlreadySentWarning); + return false; + } + + if (this.gradedParticipation?.results) { + const athenaResults = this.gradedParticipation.results.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA'); + const countOfSuccessfulRequests = athenaResults.filter((result) => result.successful === true).length; + + if (countOfSuccessfulRequests >= 3) { + const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); + window.alert(rateLimitExceededWarning); + return false; + } + } + + return true; + } } diff --git a/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.html b/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.html index 61629818e09c..2f537f3e7187 100644 --- a/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.html +++ b/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.html @@ -1,7 +1,7 @@ -<jhi-loading-indicator-container [isLoading]="isLoading"> +<jhi-loading-indicator-container [isLoading]="(isLoading$ | async) ?? false"> <div class="row justify-content-center"> @if (tutorialGroup && course) { - <div class="col-12"> + <div class="col-12 p-3 scrollable-content" [ngClass]="{ 'content-height-dev': !isProduction || isTestServer }"> <jhi-tutorial-group-detail [tutorialGroup]="tutorialGroup" [course]="course" /> </div> } diff --git a/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.ts b/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.ts index fb6d22954490..047a8426ddee 100644 --- a/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.ts +++ b/src/main/webapp/app/overview/tutorial-group-details/course-tutorial-group-detail/course-tutorial-group-detail.component.ts @@ -1,53 +1,78 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { Course } from 'app/entities/course.model'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { TutorialGroupsService } from 'app/course/tutorial-groups/services/tutorial-groups.service'; import { AlertService } from 'app/core/util/alert.service'; -import { finalize, forkJoin, switchMap, take } from 'rxjs'; +import { BehaviorSubject, Subscription, catchError, combineLatest, forkJoin, switchMap, tap } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { onError } from 'app/shared/util/global.utils'; import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; @Component({ selector: 'jhi-course-tutorial-group-detail', templateUrl: './course-tutorial-group-detail.component.html', styleUrls: ['./course-tutorial-group-detail.component.scss'], }) -export class CourseTutorialGroupDetailComponent implements OnInit { - isLoading = false; - tutorialGroup: TutorialGroup; - course: Course; +export class CourseTutorialGroupDetailComponent implements OnInit, OnDestroy { + isLoading$ = new BehaviorSubject<boolean>(false); + tutorialGroup?: TutorialGroup; + course?: Course; + private paramsSubscription: Subscription; + profileSubscription?: Subscription; + isProduction = true; + isTestServer = false; constructor( - private activatedRoute: ActivatedRoute, - private router: Router, + private route: ActivatedRoute, private tutorialGroupService: TutorialGroupsService, private alertService: AlertService, private courseManagementService: CourseManagementService, + private profileService: ProfileService, ) {} ngOnInit(): void { - this.isLoading = true; - this.activatedRoute.paramMap - .pipe( - take(1), - switchMap((params) => { - const tutorialGroupId = Number(params.get('tutorialGroupId')); - const courseId = Number(params.get('courseId')); - return forkJoin({ - courseResult: this.courseManagementService.find(courseId), - tutorialGroupResult: this.tutorialGroupService.getOneOfCourse(courseId, tutorialGroupId), - }); - }), - finalize(() => (this.isLoading = false)), - ) - .subscribe({ - next: ({ courseResult, tutorialGroupResult }) => { - this.tutorialGroup = tutorialGroupResult.body!; - this.course = courseResult.body!; - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); + const courseIdParams$ = this.route.parent?.parent?.params; + const tutorialGroupIdParams$ = this.route.params; + if (courseIdParams$) { + this.paramsSubscription = combineLatest([courseIdParams$, tutorialGroupIdParams$]) + .pipe( + switchMap(([courseIdParams, tutorialGroupIdParams]) => { + this.isLoading$.next(true); + const tutorialGroupId = parseInt(tutorialGroupIdParams.tutorialGroupId, 10); + const courseId = parseInt(courseIdParams.courseId, 10); + return forkJoin({ + courseResult: this.courseManagementService.find(courseId), + tutorialGroupResult: this.tutorialGroupService.getOneOfCourse(courseId, tutorialGroupId), + }); + }), + tap({ + next: () => { + this.isLoading$.next(false); + }, + }), + catchError((error: HttpErrorResponse) => { + this.isLoading$.next(false); + onError(this.alertService, error); + throw error; + }), + ) + .subscribe({ + next: ({ courseResult, tutorialGroupResult }) => { + this.tutorialGroup = tutorialGroupResult.body ?? undefined; + this.course = courseResult.body ?? undefined; + }, + }); + } + this.profileSubscription = this.profileService.getProfileInfo()?.subscribe((profileInfo) => { + this.isProduction = profileInfo?.inProduction; + this.isTestServer = profileInfo.testServer ?? false; + }); + } + + ngOnDestroy(): void { + this.paramsSubscription?.unsubscribe(); + this.profileSubscription?.unsubscribe(); } } diff --git a/src/main/webapp/app/shared/confirm-entity-name/confirm-entity-name.component.ts b/src/main/webapp/app/shared/confirm-entity-name/confirm-entity-name.component.ts index 027a23667755..deaad28cc270 100644 --- a/src/main/webapp/app/shared/confirm-entity-name/confirm-entity-name.component.ts +++ b/src/main/webapp/app/shared/confirm-entity-name/confirm-entity-name.component.ts @@ -46,7 +46,7 @@ export class ConfirmEntityNameComponent implements OnInit, OnDestroy, ControlVal ngOnInit() { this.control = this.fb.control('', { nonNullable: true, - validators: [Validators.required, (control: FormControl) => Validators.pattern(this.entityName)(control)], + validators: [Validators.required, this.compareWithEntityName.bind(this)], }); } @@ -89,4 +89,11 @@ export class ConfirmEntityNameComponent implements OnInit, OnDestroy, ControlVal registerOnValidatorChange(fn: () => void) { this.onValidatorChange = fn; } + + private compareWithEntityName(control: FormControl): ValidationErrors | null { + if (control.value !== this.entityName) { + return { invalidName: true }; + } + return null; + } } diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.html b/src/main/webapp/app/shared/layouts/navbar/navbar.component.html index 1d502434ba6d..241e24eab50c 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.html +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.html @@ -347,14 +347,26 @@ <h6 class="dropdown-header fw-medium" jhiTranslate="global.menu.guidedTutorial"> > } @if (breadcrumb && !breadcrumb.translate) { - <a - class="breadcrumb-link" - id="bread-crumb-plain-{{ i }}" - [routerLink]="breadcrumb.uri" - routerLinkActive="active" - [routerLinkActiveOptions]="{ exact: true }" - >{{ breadcrumb.label }}</a - > + @if (isBuildAgentDetails) { + <a + class="breadcrumb-link" + id="bread-crumb-{{ i }}" + [routerLink]="breadcrumb.uri" + routerLinkActive="active" + [routerLinkActiveOptions]="{ exact: true }" + [queryParams]="{ agentName: agentName }" + >{{ breadcrumb.label }}</a + > + } @else { + <a + class="breadcrumb-link" + id="bread-crumb-plain-{{ i }}" + [routerLink]="breadcrumb.uri" + routerLinkActive="active" + [routerLinkActiveOptions]="{ exact: true }" + >{{ breadcrumb.label }}</a + > + } } </li> } diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index c54efb8c36ff..62dc6ec05f25 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -76,6 +76,7 @@ export class NavbarComponent implements OnInit, OnDestroy { gitBranchName: string; gitTimestamp: string; gitUsername: string; + isBuildAgentDetails = false; languages = LANGUAGES; openApiEnabled?: boolean; modalRef: NgbModalRef; @@ -94,6 +95,7 @@ export class NavbarComponent implements OnInit, OnDestroy { localCIActive: boolean = false; ltiEnabled: boolean; standardizedCompetenciesEnabled = false; + agentName?: string; courseTitle?: string; exerciseTitle?: string; @@ -130,6 +132,7 @@ export class NavbarComponent implements OnInit, OnDestroy { private standardizedCompetencySubscription: Subscription; private authStateSubscription: Subscription; private routerEventSubscription: Subscription; + private queryParamsSubscription: Subscription; private studentExam?: StudentExam; private examId?: number; private routeExamId = 0; @@ -256,6 +259,7 @@ export class NavbarComponent implements OnInit, OnDestroy { if (this.standardizedCompetencySubscription) { this.standardizedCompetencySubscription.unsubscribe(); } + this.queryParamsSubscription?.unsubscribe(); } breadcrumbTranslation = { @@ -561,6 +565,7 @@ export class NavbarComponent implements OnInit, OnDestroy { */ private addBreadcrumbForUrlSegment(currentPath: string, segment: string): void { const isStudentPath = currentPath.startsWith('/courses'); + this.isBuildAgentDetails = currentPath.startsWith('/admin/build-agents/') && segment == 'details'; if (isStudentPath) { if (segment === 'repository') { @@ -572,6 +577,15 @@ export class NavbarComponent implements OnInit, OnDestroy { return; } } + + if (this.isBuildAgentDetails) { + this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { + this.agentName = params['agentName']; + if (this.agentName) { + segment = decodeURIComponent(this.agentName); + } + }); + } // When we're not dealing with an ID we need to translate the current part // The translation might still depend on the previous parts switch (segment) { diff --git a/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.html b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.html index 689664eec13d..3620e354289e 100644 --- a/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.html +++ b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.html @@ -1,5 +1,5 @@ @if (isLoading) { - <div class="d-flex justify-content-center"> + <div class="d-flex justify-content-center spinner"> <div class="spinner-border" role="status"> <span class="sr-only">{{ 'loading' | artemisTranslate }}</span> </div> diff --git a/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.scss b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.scss new file mode 100644 index 000000000000..890d651d207c --- /dev/null +++ b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.scss @@ -0,0 +1,11 @@ +/* Prevents the spinner from appearing when data loads very quickly.*/ +.spinner { + animation: delayAnimation 0s 0.5s forwards; + opacity: 0; +} + +@keyframes delayAnimation { + to { + opacity: 1; + } +} diff --git a/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.ts b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.ts index 91422e2eb5b1..dfdcd7924075 100644 --- a/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.ts +++ b/src/main/webapp/app/shared/loading-indicator-container/loading-indicator-container.component.ts @@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core'; @Component({ selector: 'jhi-loading-indicator-container', templateUrl: './loading-indicator-container.component.html', - styles: [], + styleUrls: ['./loading-indicator-container.component.scss'], }) export class LoadingIndicatorContainerComponent { @Input() isLoading = false; diff --git a/src/main/webapp/app/shared/metis/post/post.component.html b/src/main/webapp/app/shared/metis/post/post.component.html index c425ba0de80c..695d10fa63ae 100644 --- a/src/main/webapp/app/shared/metis/post/post.component.html +++ b/src/main/webapp/app/shared/metis/post/post.component.html @@ -1,24 +1,23 @@ -<jhi-post-header - [previewMode]="previewMode" - [readOnlyMode]="readOnlyMode" - [posting]="posting" - [isCourseMessagesPage]="isCourseMessagesPage" - [isCommunicationPage]="isCommunicationPage" - [hasChannelModerationRights]="hasChannelModerationRights" - (isModalOpen)="displayInlineInput = true" - [lastReadDate]="lastReadDate" -/> +<div class="d-flex align-items-center"> + <jhi-post-header + [previewMode]="previewMode" + [readOnlyMode]="readOnlyMode" + [posting]="posting" + [isCourseMessagesPage]="isCourseMessagesPage" + [isCommunicationPage]="isCommunicationPage" + [hasChannelModerationRights]="hasChannelModerationRights" + (isModalOpen)="displayInlineInput = true" + [lastReadDate]="lastReadDate" + /> + <div class="resolution-indicator ps-3"> + @if (posting.resolved) { + <fa-icon [icon]="faCheckSquare" iconSize="xs" class="col-auto pe-0 resolved" [ngbTooltip]="'artemisApp.metis.post.postMarkedAsResolvedTooltip' | artemisTranslate" /> + } + </div> +</div> <div class="row align-items-center"> <div class="col"> <div class="mb-1"> - @if (posting.resolved) { - <fa-icon - [icon]="faCheckSquare" - iconSize="xs" - class="col-auto pe-0 resolved" - [ngbTooltip]="'artemisApp.metis.post.postMarkedAsResolvedTooltip' | artemisTranslate" - /> - } @if (showAnnouncementIcon) { <fa-icon [icon]="faBullhorn" diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.scss b/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.scss new file mode 100644 index 000000000000..85f7d08ca87a --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.scss @@ -0,0 +1,11 @@ +/* + * Hide the drag handles of the hidden lines, as this feature would result in unintuitive behavior. + */ + +.diff-hidden-lines .top { + visibility: hidden; +} + +.diff-hidden-lines .bottom { + visibility: hidden; +} diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.ts b/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.ts new file mode 100644 index 000000000000..0356eb08bd93 --- /dev/null +++ b/src/main/webapp/app/shared/monaco-editor/monaco-diff-editor.component.ts @@ -0,0 +1,198 @@ +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core'; +import { Theme, ThemeService } from 'app/core/theme/theme.service'; + +import * as monaco from 'monaco-editor'; +import { Subscription } from 'rxjs'; + +export type MonacoEditorDiffText = { original: string; modified: string }; +@Component({ + selector: 'jhi-monaco-diff-editor', + template: '', + styleUrls: ['monaco-diff-editor.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class MonacoDiffEditorComponent implements OnInit, OnDestroy { + private _editor: monaco.editor.IStandaloneDiffEditor; + monacoDiffEditorContainerElement: HTMLElement; + themeSubscription?: Subscription; + listeners: monaco.IDisposable[] = []; + resizeObserver?: ResizeObserver; + + @Input() + set allowSplitView(value: boolean) { + this._editor.updateOptions({ + renderSideBySide: value, + }); + } + + @Output() + onReadyForDisplayChange = new EventEmitter<boolean>(); + + constructor( + private themeService: ThemeService, + elementRef: ElementRef, + renderer: Renderer2, + ) { + /* + * The constructor injects the editor along with its container into the empty template of this component. + * This makes the editor available immediately (not just after ngOnInit), preventing errors when the methods + * of this component are called. + */ + this.monacoDiffEditorContainerElement = renderer.createElement('div'); + this._editor = monaco.editor.createDiffEditor(this.monacoDiffEditorContainerElement, { + glyphMargin: true, + minimap: { enabled: false }, + readOnly: true, + renderSideBySide: true, + scrollBeyondLastLine: false, + stickyScroll: { + enabled: false, + }, + renderOverviewRuler: false, + scrollbar: { + vertical: 'hidden', + handleMouseWheel: true, + alwaysConsumeMouseWheel: false, + }, + hideUnchangedRegions: { + enabled: true, + }, + fontSize: 12, + }); + renderer.appendChild(elementRef.nativeElement, this.monacoDiffEditorContainerElement); + this.setupDiffListener(); + this.setupContentHeightListeners(); + } + + ngOnInit(): void { + this.resizeObserver = new ResizeObserver(() => { + this.layout(); + }); + this.resizeObserver.observe(this.monacoDiffEditorContainerElement); + this.themeSubscription = this.themeService.getCurrentThemeObservable().subscribe((theme) => this.changeTheme(theme)); + } + + ngOnDestroy(): void { + this.themeSubscription?.unsubscribe(); + this.resizeObserver?.disconnect(); + this.listeners.forEach((listener) => { + listener.dispose(); + }); + this._editor.dispose(); + } + + /** + * Sets up a listener that responds to changes in the diff. It will signal via {@link onReadyForDisplayChange} that + * the component is ready to display the diff. + */ + setupDiffListener(): void { + const diffListener = this._editor.onDidUpdateDiff(() => { + this.adjustHeightAndLayout(this.getMaximumContentHeight()); + this.onReadyForDisplayChange.emit(true); + }); + + this.listeners.push(diffListener); + } + + /** + * Sets up listeners that adjust the height of the editor to the height of its current content. + */ + setupContentHeightListeners(): void { + const editors = [this._editor.getOriginalEditor(), this._editor.getModifiedEditor()]; + + editors.forEach((editor) => { + // Called e.g. when the content of the editor changes. + const contentSizeListener = editor.onDidContentSizeChange((e: monaco.editor.IContentSizeChangedEvent) => { + if (e.contentHeightChanged) { + // Using the content height of the larger editor here ensures that neither of the editors break out of the container. + this.adjustHeightAndLayout(this.getMaximumContentHeight()); + } + }); + + // Called when the user reveals or collapses a hidden region. + const hiddenAreaListener = editor.onDidChangeHiddenAreas(() => { + this.adjustHeightAndLayout(this.getContentHeightOfEditor(editor)); + }); + + this.listeners.push(contentSizeListener, hiddenAreaListener); + }); + } + + /** + * Adjusts the height of the editor to fit the new content height. + * @param newContentHeight + */ + adjustHeightAndLayout(newContentHeight: number) { + this.monacoDiffEditorContainerElement.style.height = newContentHeight + 'px'; + this.layout(); + } + + /** + * Adjusts this editor to fit its container. + */ + layout(): void { + this._editor.layout(); + } + + /** + * Sets the theme of all Monaco editors according to the Artemis theme. + * As of now, it is not possible to have two editors with different themes. + * @param artemisTheme The active Artemis theme. + */ + changeTheme(artemisTheme: Theme): void { + monaco.editor.setTheme(artemisTheme === Theme.DARK ? 'vs-dark' : 'vs-light'); + } + + /** + * Updates the files displayed in this editor. When this happens, {@link onReadyForDisplayChange} will signal that the editor is not + * ready to display the diff (as it must be computed first). This will later be change by the appropriate listener. + * @param original The content of the original file, if available. + * @param originalFileName The name of the original file, if available. The name is used to determine the syntax highlighting of the left editor. + * @param modified The content of the modified file, if available. + * @param modifiedFileName The name of the modified file, if available. The name is used to determine the syntax highlighting of the right editor. + */ + setFileContents(original?: string, originalFileName?: string, modified?: string, modifiedFileName?: string): void { + this.onReadyForDisplayChange.emit(false); + const originalModelUri = monaco.Uri.parse(`inmemory://model/original-${this._editor.getId()}/${originalFileName ?? 'left'}`); + const modifiedFileUri = monaco.Uri.parse(`inmemory://model/modified-${this._editor.getId()}/${modifiedFileName ?? 'right'}`); + const originalModel = monaco.editor.getModel(originalModelUri) ?? monaco.editor.createModel(original ?? '', undefined, originalModelUri); + const modifiedModel = monaco.editor.getModel(modifiedFileUri) ?? monaco.editor.createModel(modified ?? '', undefined, modifiedFileUri); + + originalModel.setValue(original ?? ''); + modifiedModel.setValue(modified ?? ''); + + monaco.editor.setModelLanguage(originalModel, originalModel.getLanguageId()); + monaco.editor.setModelLanguage(modifiedModel, modifiedModel.getLanguageId()); + + const newModel = { + original: originalModel, + modified: modifiedModel, + }; + + this._editor.setModel(newModel); + } + + /** + * Returns the content height of the larger of the two editors in this view. + */ + getMaximumContentHeight(): number { + return Math.max(this.getContentHeightOfEditor(this._editor.getOriginalEditor()), this.getContentHeightOfEditor(this._editor.getModifiedEditor())); + } + + /** + * Returns the content height of the provided editor. + * @param editor The editor whose content height should be retrieved. + */ + getContentHeightOfEditor(editor: monaco.editor.ICodeEditor): number { + return editor.getContentHeight(); + } + + /** + * Returns the text (original and modified) currently stored in the editor. + */ + getText(): MonacoEditorDiffText { + const original = this._editor.getOriginalEditor().getValue(); + const modified = this._editor.getModifiedEditor().getValue(); + return { original, modified }; + } +} diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor.module.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor.module.ts index e9fcd6e23ec7..68cf7d361da7 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor.module.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor.module.ts @@ -1,8 +1,9 @@ import { NgModule } from '@angular/core'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; +import { MonacoDiffEditorComponent } from 'app/shared/monaco-editor/monaco-diff-editor.component'; @NgModule({ - declarations: [MonacoEditorComponent], - exports: [MonacoEditorComponent], + declarations: [MonacoEditorComponent, MonacoDiffEditorComponent], + exports: [MonacoEditorComponent, MonacoDiffEditorComponent], }) export class MonacoEditorModule {} diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html index 5304337fbc2e..8cb9b3ffa274 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html @@ -6,7 +6,7 @@ > <div id="test-accordion-item-header" class="control-label d-flex align-items-center justify-content-between py-2 bg-module" (click)="toggleGroupCategoryCollapse(groupKey)"> <div class="my-2"> - {{ 'artemisApp.courseOverview.exerciseList.' + groupKey | artemisTranslate | titlecase }} + {{ 'artemisApp.courseOverview.sidebar.' + groupKey | artemisTranslate | titlecase }} ({{ (groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue)?.length }}) </div> <div class="icon-container pe-3"> diff --git a/src/main/webapp/app/shared/sidebar/sidebar.component.html b/src/main/webapp/app/shared/sidebar/sidebar.component.html index 75d6094755d0..dc738d1bf160 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar.component.html @@ -10,10 +10,10 @@ <div [ngClass]="{ 'content-height-dev': !isProduction || isTestServer }" jhiTranslate="artemisApp.courseOverview.general.noDataFound" - class="mt-2 text-center scrollable-content" + class="mt-2 text-center scrollable-item-content" ></div> } @else { - <div class="scrollable-content my-2" [ngClass]="{ 'content-height-dev': !isProduction || isTestServer }"> + <div class="scrollable-item-content my-2" [ngClass]="{ 'content-height-dev': !isProduction || isTestServer }"> @if (sidebarData?.groupByCategory && sidebarData.groupedData) { <jhi-sidebar-accordion [searchValue]="searchValue" diff --git a/src/main/webapp/app/shared/sidebar/sidebar.component.scss b/src/main/webapp/app/shared/sidebar/sidebar.component.scss index d5c02dc0c07b..b1739678ec6c 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar.component.scss +++ b/src/main/webapp/app/shared/sidebar/sidebar.component.scss @@ -1,13 +1,12 @@ $search-height: 97px; -$header-height: 68px; -.scrollable-content { - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height - $search-height); +.scrollable-item-content { + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height) - $search-height); &.content-height-dev { - height: calc(100vh - var(--sidebar-footer-height-dev) - $header-height - $search-height); + height: calc(100vh - var(--sidebar-footer-height-dev) - var(--header-height) - $search-height); } @media (max-width: 768px) { - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height - $search-height) !important; + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height) - $search-height) !important; } overflow-y: auto; @@ -15,12 +14,12 @@ $header-height: 68px; .sidebar { background-color: var(--module-bg); - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height); + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height)); &.sidebar-height-dev { - height: calc(100vh - var(--sidebar-footer-height-dev) - $header-height); + height: calc(100vh - var(--sidebar-footer-height-dev) - var(--header-height)); } @media (max-width: 768px) { - height: calc(100vh - var(--sidebar-footer-height-prod) - $header-height) !important; + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height)) !important; } } diff --git a/src/main/webapp/app/shared/standardized-competencies/knowledge-area-tree.component.html b/src/main/webapp/app/shared/standardized-competencies/knowledge-area-tree.component.html index 89c592906e39..230445b7ced1 100644 --- a/src/main/webapp/app/shared/standardized-competencies/knowledge-area-tree.component.html +++ b/src/main/webapp/app/shared/standardized-competencies/knowledge-area-tree.component.html @@ -13,7 +13,7 @@ <h5 class="mb-0">{{ knowledgeArea.title }}</h5> <ng-container matTreeNodeOutlet /> @for (competency of knowledgeArea.competencies; track competency.id) { <div class="card tree-card" [class.d-none]="!competency.isVisible"> - <ng-container [ngTemplateOutlet]="competencyTemplate" [ngTemplateOutletContext]="{ competency: competency }" /> + <ng-container [ngTemplateOutlet]="competencyTemplate" [ngTemplateOutletContext]="{ competency: competency, knowledgeArea: knowledgeArea }" /> </div> } @if (!knowledgeArea.children?.length && !knowledgeArea.competencies?.length) { diff --git a/src/main/webapp/app/types/sidebar.ts b/src/main/webapp/app/types/sidebar.ts index 97d4c9faaf28..1edf3b222707 100644 --- a/src/main/webapp/app/types/sidebar.ts +++ b/src/main/webapp/app/types/sidebar.ts @@ -1,10 +1,12 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { DifficultyLevel, Exercise } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; + export type TimeGroupCategory = 'past' | 'current' | 'future' | 'noDate'; +export type TutorialGroupCategory = 'all' | 'registered' | 'further'; export type SidebarTypes = 'exercise' | 'default'; -export type AccordionGroups = Record<TimeGroupCategory, { entityData: SidebarCardElement[] }>; +export type AccordionGroups = Record<TimeGroupCategory | TutorialGroupCategory | string, { entityData: SidebarCardElement[] }>; export type ExerciseCollapseState = Record<TimeGroupCategory, boolean>; export interface SidebarData { diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss index 970ecfe5e4b8..1882805d2d93 100644 --- a/src/main/webapp/content/scss/global.scss +++ b/src/main/webapp/content/scss/global.scss @@ -1029,3 +1029,24 @@ body.transparent-background { max-width: 600px !important; width: 600px !important; } + +/* Course Overview Content Pages with Sidebar */ +.module-bg { + background-color: var(--module-bg); +} + +.scrollable-content { + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height)); + overflow-y: auto; + + &.content-height-dev { + height: calc(100vh - var(--sidebar-footer-height-dev) - var(--header-height)); + } + @media (max-width: 768px) { + height: calc(100vh - var(--sidebar-footer-height-prod) - var(--header-height)) !important; + } +} + +.sidebar-collapsed { + display: none; +} diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index bc2022bf0c31..792db0e64aab 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -127,9 +127,14 @@ $neutral-dark-l-20: lighten($neutral-dark, 20%); // Footer $footer-bg: $neutral-dark; +$footer-height-dev: 3rem; +$footer-height-prod: 2rem; +$sidebar-footer-height-dev: 134px; +$sidebar-footer-height-prod: 118px; //Module and Navigationbar $module-bg: $neutral-dark; +$header-height: 68px; $course-image-bg: $body-bg; $link-item-bg: $gray-900; $sidebar-card-selected-bg: #353d47; diff --git a/src/main/webapp/content/scss/themes/_default-variables.scss b/src/main/webapp/content/scss/themes/_default-variables.scss index ae099ddeac2b..f2190a6cc399 100644 --- a/src/main/webapp/content/scss/themes/_default-variables.scss +++ b/src/main/webapp/content/scss/themes/_default-variables.scss @@ -51,11 +51,12 @@ $border-color: #dee2e6; $footer-bg: $white; $footer-height-dev: 3rem; $footer-height-prod: 2rem; -$sidebar-footer-height-dev: 148px; -$sidebar-footer-height-prod: 132px; +$sidebar-footer-height-dev: 134px; +$sidebar-footer-height-prod: 118px; //Module and Navigationbar $module-bg: $white; +$header-height: 68px; $course-image-bg: $body-bg; $link-item-color: $primary; $link-item-bg: $gray-100; diff --git a/src/main/webapp/i18n/de/buildAgents.json b/src/main/webapp/i18n/de/buildAgents.json index c977f9e439d7..cabe778eee21 100644 --- a/src/main/webapp/i18n/de/buildAgents.json +++ b/src/main/webapp/i18n/de/buildAgents.json @@ -2,6 +2,8 @@ "artemisApp": { "buildAgents": { "title": "Build Agenten", + "summary": "Build Agenten Zusammenfassung", + "details": "Build Agenten Details", "name": "Name", "maxNumberOfConcurrentBuildJobs": "Maximale Anzahl an parallelen Build Jobs", "numberOfCurrentBuildJobs": "Anzahl aktueller Build Jobs", @@ -9,7 +11,10 @@ "status": "Status", "recentBuildJobs": "Letzte Build Jobs", "running": "Laufend", - "idle": "Inaktiv" + "idle": "Inaktiv", + "onlineAgents": "online Agent(en)", + "of": "von", + "buildJobsRunning": "Build Jobs laufen" } } } diff --git a/src/main/webapp/i18n/de/courseTutorialGroups.json b/src/main/webapp/i18n/de/courseTutorialGroups.json index d8f01331c4b9..4f469efac3fb 100644 --- a/src/main/webapp/i18n/de/courseTutorialGroups.json +++ b/src/main/webapp/i18n/de/courseTutorialGroups.json @@ -6,9 +6,7 @@ "pages": { "courseTutorialGroups": { "title": "Deine Übungsgruppen", - "noRegistrations": "Du bist in keiner Übungsgruppe angemeldet.", - "allFilter": "Alle Übungsgruppen anzeigen", - "registeredFilter": "Meine Übungsgruppen anzeigen" + "noRegistrations": "Du bist in keiner Übungsgruppe angemeldet." }, "courseTutorialGroupDetail": { "title": "Übungsgruppen-Details", diff --git a/src/main/webapp/i18n/de/editor.json b/src/main/webapp/i18n/de/editor.json index d1e3273b8409..d548da95d0f2 100644 --- a/src/main/webapp/i18n/de/editor.json +++ b/src/main/webapp/i18n/de/editor.json @@ -33,6 +33,7 @@ "building": "Build und Tests werden ausgeführt...", "buildFailed": "Build gescheitert", "noBuildOutput": "Keine Build-Ergebnisse verfügbar.", + "generatingFeedback": "Feedback wird generiert...", "selectFile": "Wähle eine Datei aus, um zu beginnen!", "binaryFileSelected": "Diese Datei ist eine Binärdatei, die nicht angezeigt werden kann.", "downloadBuildResult": "Build-Ergebnis herunterladen", diff --git a/src/main/webapp/i18n/de/error.json b/src/main/webapp/i18n/de/error.json index d80d84009768..a1f8d3a79e70 100644 --- a/src/main/webapp/i18n/de/error.json +++ b/src/main/webapp/i18n/de/error.json @@ -93,6 +93,7 @@ "exportFailed": "Aufgabenexport fehlgeschlagen", "unableToImportProgrammingExercise": "Fehler beim Importieren der Programmieraufgabe.", "titleAlreadyExists": "Es existiert bereits eine Programmieraufgabe mit demselben Titel, bitte nutze einen anderen Titel.", - "shortnameAlreadyExists": "Es existiert bereits eine Programmieraufgabe mit demselben Kurznamen, bitte nutze einen anderen Kurznamen." + "shortnameAlreadyExists": "Es existiert bereits eine Programmieraufgabe mit demselben Kurznamen, bitte nutze einen anderen Kurznamen.", + "courseTitleTooLong": "Der Kurstitel ist zu lang (max. 255 Zeichen), bitte nutze einen kürzeren Kurstitel." } } diff --git a/src/main/webapp/i18n/de/exercise-actions.json b/src/main/webapp/i18n/de/exercise-actions.json index 8fbce9b93411..26fd26970b57 100644 --- a/src/main/webapp/i18n/de/exercise-actions.json +++ b/src/main/webapp/i18n/de/exercise-actions.json @@ -10,8 +10,10 @@ "statistics": "Quiz-Statistiken", "startExercise": "Aufgabe starten", "resumeExercise": "Aufgabe fortführen", - "requestFeedback": "Feedback anfragen", - "requestFeedbackTooltip": "Sendet eine Feedbackanfrage vor der Einreichungsfrist. Diese Option ist erst möglich wenn 100% in den öffentlichen Tests erreicht wurden.", + "requestManualFeedback": "Feedback anfragen", + "requestManualFeedbackTooltip": "Sendet eine Feedbackanfrage vor der Einreichungsfrist.", + "requestAutomaticFeedback": "AI Feedback anfragen", + "requestAutomaticFeedbackTooltip": "Das automatisch generierte Feedback kann Fehler beinhalten. Überprüfe wichtige Informationen.", "resumeExercisePractice": "Üben von Aufgabe fortführen", "resumeExerciseError": "Fehler beim Versuch die Aufgabe fortzuführen", "openCodeEditor": "Programmiereditor öffnen", diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index 4480649770ee..7982f0aa227f 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -167,7 +167,10 @@ "resumeProgrammingExercise": "Die Aufgabe wurde wieder aufgenommen. Du kannst nun weiterarbeiten!", "feedbackRequestSent": "Deine Feedbackanfrage wurde gesendet.", "feedbackRequestAlreadySent": "Deine Feedbackanfrage wurde bereits gesendet.", - "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten wenn ein Tutor die Feedbackanfrage beantwortet hat.", + "notEnoughPoints": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", + "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten wenn deine Feedbackanfrage beantwortet wird.", + "feedbackRequestAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Anfragen einreichen.", + "maxAthenaResultsReached": "Du hast die maximale Anzahl an KI-Feedbackanfragen erreicht.", "startError": "<strong>Uh oh! Etwas ist schief gelaufen... Bitte versuch es in wenigen Minuten noch einmal die Aufgabe zu starten.</strong>", "name": "Name", "studentId": "Login", diff --git a/src/main/webapp/i18n/de/health.json b/src/main/webapp/i18n/de/health.json index 8b94ded7652b..e477bcfa6f36 100644 --- a/src/main/webapp/i18n/de/health.json +++ b/src/main/webapp/i18n/de/health.json @@ -28,7 +28,7 @@ "websocketBroker": "Websocket Broker (Server -> Broker)", "athena": "Athena", "apollon": "Apollon Conversion Server", - "iris": "Pyris Server" + "pyris": "Pyris" }, "table": { "service": "Dienst Name", diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index ca3ea320c34f..b48f7bc7944d 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -366,6 +366,7 @@ "title": "Code-Style normalisieren", "tooltip": "Standardisiere Zeilenumbrüche und Kodierung für Konsistenz" }, + "successMessageRepo": "Das Repository wurde erfolgreich exportiert. Die generierte zip-Datei wird jetzt heruntergeladen.", "successMessageRepos": "Die Repositories wurden erfolgreich exportiert. Die generierte zip-Datei wird jetzt heruntergeladen.", "successMessageExercise": "Die Programmierübung wurde erfolgreich exportiert. Die generierte zip-Datei wird jetzt heruntergeladen.", "notFoundMessageRepos": "Keine Repositories heruntergeladen für Aufgabe {{exerciseId}}: Es sind noch keine Abgaben der angegebenen Studenten vorhanden." @@ -414,7 +415,8 @@ "assessmentDueDate": "Einreichungsfrist der Bewertung", "assessmentDueDateTooltip": "Die Frist für manuelle Reviews. Sobald das Datum erreicht ist, werden alle manuellen Ergebnisse an die Studierenden veröffentlicht!", "notSet": "nicht gesetzt", - "allowFeedbackRequests": "Feedbackanfragen erlauben", + "allowFeedbackRequests": "Automatische nicht bewertete Feedbackanfragen/manuelle Feedbackanfragen erlauben", + "allowFeedbackRequestsTooltip": "Studierende können vor dem Einreichungsfrist um Feedback bitten. Die Anfragen werden von Athena bearbeitet oder, falls Athena nicht erreicht werden kann, können Studierende manuelle Feedbackanfragen stellen.", "manualFeedbackRequests": "Anfragen nach manuellem Feedback", "manualFeedbackRequestsTooltip": "Studierende können manuelle Korrekturen vor der Einreichungsfrist anfragen, um Feedback zu erhalten.", "feedbackRequestsEnabled": "Korrekturanfragen sind möglich", @@ -539,6 +541,11 @@ "diffReport": { "button": "Anzeigen", "tooltip": "Zeigt den detaillierten Git-Diff zwischen den Template- und Lösungs-Repositories.", + "splitView": { + "enable": "Getrennte Ansicht aktivieren", + "disable": "Getrennte Ansicht deaktivieren", + "tooltip": "In der getrennten Ansicht werden beide Versionen jeder geänderten Datei nebeneinander angezeigt. Beachte, dass die getrennte Ansicht nur dann angezeigt wird, wenn dein Browserfenster breit genug ist." + }, "tooltipBetweenSubmissions": "Zeigt den detaillierten Git-Diff zwischen der aktuellen und der vorherigen Abgabe.", "title": "Template-Lösungs-Diff", "titleForSubmissions": "Abgaben-Diff", @@ -549,6 +556,11 @@ "previousCommit": "Vorheriger Commit", "currentSubmission": "Aktuelle Abgabe", "currentCommit": "Aktueller Commit", + "fileChange": { + "created": "Erstellt", + "deleted": "Gelöscht", + "renamed": "Umbenannt" + }, "errorWhileFetchingRepos": "Fehler beim Abrufen der Repositories. Bitte überprüfe deine Internetverbindung und öffne das Popup erneut.", "404": "Template-Lösungs-Diff wurde noch nicht generiert. Bitte pusche etwas in die Template- oder Lösungs-Repositories, um ihn zu erstellen.", "lineStatLabel": "Zeilen, die zwischen Vorlage und Lösung hinzugefügt/entfernt wurden", diff --git a/src/main/webapp/i18n/de/repository.json b/src/main/webapp/i18n/de/repository.json index 40b54bb977d1..dc84114ca1b1 100644 --- a/src/main/webapp/i18n/de/repository.json +++ b/src/main/webapp/i18n/de/repository.json @@ -11,7 +11,8 @@ "commitHash": "Commit Hash", "author": "Autor", "date": "Datum", - "commit": "Commit" + "commit": "Commit", + "empty": "Keine Änderungen" } } } diff --git a/src/main/webapp/i18n/de/result.json b/src/main/webapp/i18n/de/result.json index 6af723bc0af9..e414943fe65e 100644 --- a/src/main/webapp/i18n/de/result.json +++ b/src/main/webapp/i18n/de/result.json @@ -45,7 +45,15 @@ "programming": "{{ relativeScore }}%, {{ buildAndTestMessage }}, {{ points }} Punkte", "programmingCodeIssues": "{{ relativeScore }}%, {{ buildAndTestMessage }}, {{ numberOfIssues }} Issues, {{ points }} Punkte", "programmingShort": "{{ relativeScore }}%, {{ buildAndTestMessage }}", - "short": "{{ relativeScore }}%" + "short": "{{ relativeScore }}%", + "automaticAIFeedbackSuccessful": "Feedback", + "automaticAIFeedbackFailed": "Feedback", + "automaticAIFeedbackInProgress": "Feedbackanfrage", + "automaticAIFeedbackTimedOut": "Feedbackanfrage ist ohne Ergebnis abgelaufen", + "automaticAIFeedbackSuccessfulTooltip": "AI-Feedbackanfrage war erfolgreich. Beachte, dass die Ergebnisse Fehler beinhalten können. Verwende sie nur zur Orientierung.", + "automaticAIFeedbackFailedTooltip": "AI-Feedbackanfrage ist aufgrund eines internen Fehlers fehlgeschlagen. Bitte versuche es in Kürze erneut oder kontaktiere die Artemis-Administratoren, sollte der Fehler wieder auftreten.", + "automaticAIFeedbackInProgressTooltip": "AI-Feedback wird generiert.", + "automaticAIFeedbackTimedOutTooltip": "Die Zeit für die Generierung des AI-Feedbacks ist abgelaufen. Bitte versuche es in Kürze erneut oder kontaktiere die Artemis-Administratoren, sollte der Fehler wieder auftreten." }, "completionDate": "Abgabedatum", "successful": "Erfolgreich", diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 03e6080e350b..32c959eafff6 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -34,6 +34,17 @@ "general": { "noDataFound": "Keine Einträge gefunden." }, + "sidebar": { + "past": "Vorangegangen", + "current": "Aktuell", + "future": "Zukünftig", + "noDueDate": "Unbefristet", + "noDate": "Ohne Datum", + "all": "Übungsgruppen", + "registered": "Meine Übungsgruppen", + "further": "Weitere Übungsgruppen", + "noUpcomingSession": "Kein Termin" + }, "menu": { "exercises": "Aufgaben", "statistics": "Statistiken", diff --git a/src/main/webapp/i18n/de/tutorialGroups.json b/src/main/webapp/i18n/de/tutorialGroups.json index e1c4dfdccf98..4152fd564eb1 100644 --- a/src/main/webapp/i18n/de/tutorialGroups.json +++ b/src/main/webapp/i18n/de/tutorialGroups.json @@ -48,6 +48,7 @@ "channelWithName": "Kanal: {{ channel }}", "channelReverse": "Kanal der Übungsgruppe {{ title }}", "utilization": "Auslastung", + "attendance": "Anwesenheit", "utilizationHelp": "Durchschnittliche Anwesenheit geteilt durch Kapazität (falls definiert). Die durchschnittliche Anwesenheit bezieht sich auf die letzten drei Sitzungen. Falls keine Anwesenheit eingetragen wurde, wird die entsprechende Sitzung ignoriert und die Berechnung mit zwei, bzw. einer Sitzung durchgeführt.", "utilizationHelpDetail": "Durchschnittliche Anwesenheit geteilt durch Kapazität", "averageAttendance": "Duchschnittliche Anwesenheit: {{ averageAttendance }}", diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index 193f0060fdf4..195a549cda0f 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -2,6 +2,8 @@ "artemisApp": { "buildAgents": { "title": "Build Agents", + "summary": "Build Agents Summary", + "details": "Build Agents Details", "name": "Name", "maxNumberOfConcurrentBuildJobs": "Max # of concurrent build jobs", "numberOfCurrentBuildJobs": "# of current build jobs", @@ -9,7 +11,10 @@ "status": "Status", "recentBuildJobs": "Recent build jobs", "running": "Running", - "idle": "Idle" + "idle": "Idle", + "onlineAgents": "online agent(s)", + "of": "of", + "buildJobsRunning": "build jobs running" } } } diff --git a/src/main/webapp/i18n/en/courseTutorialGroups.json b/src/main/webapp/i18n/en/courseTutorialGroups.json index 4ed318decfa4..821df1da9772 100644 --- a/src/main/webapp/i18n/en/courseTutorialGroups.json +++ b/src/main/webapp/i18n/en/courseTutorialGroups.json @@ -6,9 +6,7 @@ "pages": { "courseTutorialGroups": { "title": "Your Tutorial Groups", - "noRegistrations": "You are not registered to any tutorial group in this course.", - "allFilter": "Show all Tutorial Groups", - "registeredFilter": "Show my Tutorial Groups" + "noRegistrations": "You are not registered to any tutorial group in this course." }, "courseTutorialGroupDetail": { "title": "Tutorial Group Details", diff --git a/src/main/webapp/i18n/en/error.json b/src/main/webapp/i18n/en/error.json index b97c736acfa8..b7a08b0eaf7e 100644 --- a/src/main/webapp/i18n/en/error.json +++ b/src/main/webapp/i18n/en/error.json @@ -93,6 +93,7 @@ "exportFailed": "Exercise export failed", "unableToImportProgrammingExercise": "Unable to import programming exercise.", "titleAlreadyExists": "A programming exercise with the same title already exists. Please choose a different title.", - "shortnameAlreadyExists": "A programming exercise with the same short name already exists. Please choose a different short name." + "shortnameAlreadyExists": "A programming exercise with the same short name already exists. Please choose a different short name.", + "courseTitleTooLong": "The course title is too long (max. 255 characters). Please choose a shorter title." } } diff --git a/src/main/webapp/i18n/en/exercise-actions.json b/src/main/webapp/i18n/en/exercise-actions.json index 2b760fef79f1..a9d557b3103e 100644 --- a/src/main/webapp/i18n/en/exercise-actions.json +++ b/src/main/webapp/i18n/en/exercise-actions.json @@ -10,8 +10,10 @@ "statistics": "Quiz Statistics", "startExercise": "Start exercise", "resumeExercise": "Resume exercise", - "requestFeedback": "Request feedback", - "requestFeedbackTooltip": "Send a manual feedback request before the due date for review. This option is only possible if you reached 100% in the public tests.", + "requestManualFeedback": "Request feedback", + "requestManualFeedbackTooltip": "Send a manual feedback request before the due date for review.", + "requestAutomaticFeedback": "Request AI feedback", + "requestAutomaticFeedbackTooltip": "AI-Based Feedback can include mistakes. Consider checking important information.", "resumeExercisePractice": "Resume practice in exercise", "resumeExerciseError": "Error trying to resume the exercise", "openCodeEditor": "Open code editor", diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index 9b344211fd47..ca43c1f4e32d 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -167,7 +167,10 @@ "resumeProgrammingExercise": "The exercise has been resumed. You can now continue working on the exercise!", "feedbackRequestSent": "Your feedback request has been sent.", "feedbackRequestAlreadySent": "Your feedback request has already been sent.", - "lockRepositoryWarning": "Your repository will be locked. You can only continue working after a tutor has answered your feedback request.", + "notEnoughPoints": "You have to submit your work at least once.", + "lockRepositoryWarning": "Your repository will be locked. You can only continue working after you receive an answer.", + "feedbackRequestAfterDueDate": "You cannot submit feedback requests after the due date.", + "maxAthenaResultsReached": "You have reached the maximum number of AI feedback requests.", "startError": "<strong>Uh oh! Something went wrong... Please try again to start the exercise in a few minutes.</strong>", "name": "Name", "studentId": "Login", diff --git a/src/main/webapp/i18n/en/health.json b/src/main/webapp/i18n/en/health.json index 500125834a45..3b624f50044c 100644 --- a/src/main/webapp/i18n/en/health.json +++ b/src/main/webapp/i18n/en/health.json @@ -28,7 +28,7 @@ "websocketBroker": "Websocket Broker (Server -> Broker)", "athena": "Athena", "apollon": "Apollon Conversion Server", - "iris": "Pyris Server" + "pyris": "Pyris" }, "table": { "service": "Service name", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index b7db739ff25b..7cc5dfcca58a 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -368,9 +368,10 @@ "title": "Normalize code style", "tooltip": "Standardize line endings and encoding for consistency" }, + "successMessageRepo": "Export of repo was successful. The exported zip file is currently being downloaded.", "successMessageRepos": "Export of repos was successful. The exported zip file with all repositories is currently being downloaded.", "successMessageExercise": "Export of exercise was successful. The exported zip file with all exercise material is currently being downloaded.", - "notFoundMessageRepos": "No Repsoitories exported for exercise {{exerciseId}}: There are no submission of the given students." + "notFoundMessageRepos": "No repos exported for exercise {{exerciseId}}: There are no submissions from the given students." }, "resubmitOnFailedSubmission": "An automatic assessment for this participant's last submission could not be generated. Click the button to submit again. This will trigger a new build run that tests the last submission.", "resubmit": "Resubmit the last submission. This will execute a build run on the participant's repository without making a commit.", @@ -416,7 +417,8 @@ "assessmentDueDate": "Assessment Due Date", "assessmentDueDateTooltip": "The due date for manual reviews. As soon as the specified date passes, all manual assessments will be released to students!", "notSet": "not set", - "allowFeedbackRequests": "Allow feedback requests", + "allowFeedbackRequests": "Allow automatic non graded feedback requests/manual feedback requests", + "allowFeedbackRequestsTooltip": "Students can request feedback before the due date. The requests will be processed by Athena, or, if Athena cannot be reached, students can send manual feedback requests.", "manualFeedbackRequests": "Manual feedback requests", "manualFeedbackRequestsTooltip": "Students can request manual feedback before the due date to receive feedback.", "complaintOnAutomaticAssessment": "Complaint on Automatic Assessment", @@ -541,6 +543,11 @@ "diffReport": { "button": "Show", "tooltip": "Shows the detailed git-diff between the template and solution repositories.", + "splitView": { + "enable": "Enable split view", + "disable": "Disable split view", + "tooltip": "In the split view, both versions of each changed file are displayed next to each other. Note that the split view will only be shown if your browser window is wide enough." + }, "tooltipBetweenSubmissions": "Shows the detailed git-diff between the current and previous submission.", "title": "Template-Solution-Diff", "titleForSubmissions": "Submission-Diff", @@ -551,6 +558,11 @@ "previousCommit": "Previous Commit", "currentSubmission": "Current Submission", "currentCommit": "Current Commit", + "fileChange": { + "created": "Created", + "deleted": "Deleted", + "renamed": "Renamed" + }, "errorWhileFetchingRepos": "An error occurred while fetching the repositories. Please check your internet connection and reopen the modal.", "404": "Template-Solution-Diff has not been generated yet. Please do a push to the template or solution repository to generate it.", "lineStatLabel": "Lines added/removed between template and solution", diff --git a/src/main/webapp/i18n/en/repository.json b/src/main/webapp/i18n/en/repository.json index 4d56b90283ee..af2445ae730b 100644 --- a/src/main/webapp/i18n/en/repository.json +++ b/src/main/webapp/i18n/en/repository.json @@ -11,7 +11,8 @@ "commitHash": "Commit Hash", "author": "Author", "date": "Date", - "commit": "Commit" + "commit": "Commit", + "empty": "No changes" } } } diff --git a/src/main/webapp/i18n/en/result.json b/src/main/webapp/i18n/en/result.json index 57d2b9dc6892..ec1e7aafac87 100644 --- a/src/main/webapp/i18n/en/result.json +++ b/src/main/webapp/i18n/en/result.json @@ -45,7 +45,15 @@ "programming": "{{ relativeScore }}%, {{ buildAndTestMessage }}, {{ points }} points", "programmingCodeIssues": "{{ relativeScore }}%, {{ buildAndTestMessage }}, {{ numberOfIssues }} issues, {{ points }} points", "programmingShort": "{{ relativeScore }}%, {{ buildAndTestMessage }}", - "short": "{{ relativeScore }}%" + "short": "{{ relativeScore }}%", + "automaticAIFeedbackSuccessful": "Feedback", + "automaticAIFeedbackFailed": "Feedback", + "automaticAIFeedbackInProgress": "AI Feedback request is being processed", + "automaticAIFeedbackTimedOut": "Feedback timed out.", + "automaticAIFeedbackSuccessfulTooltip": "AI-based feedback can include mistakes. Consider checking important information.", + "automaticAIFeedbackFailedTooltip": "AI Feedback Request failed due to an internal error. Please try again shortly or contact the Artemis administrators if the issue persists.", + "automaticAIFeedbackInProgressTooltip": "AI Feedback is being generated.", + "automaticAIFeedbackTimedOutTooltip": "Time for generating AI Feedback has expired. Please try again shortly or contact Artemis administrators if the issue persists." }, "completionDate": "Completion Date", "successful": "Successful", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 06e941905c15..9c710890a7f2 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -34,6 +34,17 @@ "general": { "noDataFound": "No data found." }, + "sidebar": { + "past": "Past", + "current": "Current", + "future": "Future", + "noDueDate": "No due date", + "noDate": "No date", + "all": "Tutorial Groups", + "registered": "My Tutorial Groups", + "further": "Further Tutorial Groups", + "noUpcomingSession": "No upcoming Session" + }, "menu": { "exercises": "Exercises", "statistics": "Statistics", diff --git a/src/main/webapp/i18n/en/tutorialGroups.json b/src/main/webapp/i18n/en/tutorialGroups.json index 178ab7352d84..f3b51ff7c851 100644 --- a/src/main/webapp/i18n/en/tutorialGroups.json +++ b/src/main/webapp/i18n/en/tutorialGroups.json @@ -48,6 +48,7 @@ "channelWithName": "Channel: {{ channel }}", "channelReverse": "Channel of the tutorial group {{ title }}", "utilization": "Utilization", + "attendance": "Attendance", "utilizationHelp": "Average attendance divided by capacity (if defined). The average attendance considers the last three sessions. If no attendance is entered, the corresponding session is ignored and the calculation is performed with two or one session.", "utilizationHelpDetail": "Average attendance divided by capacity", "averageAttendance": "Average attendance: {{ averageAttendance }}", diff --git a/src/test/cypress/e2e/course/CourseCommunication.cy.ts b/src/test/cypress/e2e/course/CourseCommunication.cy.ts index ccebd8495b7c..c215eab1a540 100644 --- a/src/test/cypress/e2e/course/CourseCommunication.cy.ts +++ b/src/test/cypress/e2e/course/CourseCommunication.cy.ts @@ -47,7 +47,7 @@ courseConfigsToTest.forEach((configToTest) => { }); }); - it.only('instructor should be able to select answer', () => { + it.skip('instructor should be able to select answer', () => { const content = 'Answer Post Content'; cy.login(studentOne, `/courses/${course.id}/discussion`); communicationAPIRequest.createCourseWideMessage(course, courseWideRandomChannel.id!, content).then((response) => { diff --git a/src/test/cypress/e2e/exam/ExamAssessment.cy.ts b/src/test/cypress/e2e/exam/ExamAssessment.cy.ts index 4587a0a2c65b..1af0388c1681 100644 --- a/src/test/cypress/e2e/exam/ExamAssessment.cy.ts +++ b/src/test/cypress/e2e/exam/ExamAssessment.cy.ts @@ -140,7 +140,7 @@ describe('Exam assessment', () => { prepareExam(course, examEnd, ExerciseType.QUIZ); }); - it('Assesses quiz automatically', () => { + it.skip('Assesses quiz automatically', () => { cy.login(instructor); examManagement.verifySubmitted(course.id!, exam.id!, studentOneName); if (dayjs().isBefore(examEnd)) { diff --git a/src/test/cypress/fixtures/exercise/programming/python/template.json b/src/test/cypress/fixtures/exercise/programming/python/template.json index 4ec6960209c0..cb0962ce3c00 100644 --- a/src/test/cypress/fixtures/exercise/programming/python/template.json +++ b/src/test/cypress/fixtures/exercise/programming/python/template.json @@ -1,6 +1,6 @@ { "allowComplaintsForAutomaticAssessments": false, - "allowManualFeedbackRequests": false, + "allowFeedbackRequests": false, "allowOfflineIde": false, "allowOnlineEditor": true, "assessmentDueDateError": false, diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java index 48b37a1d7a1d..d0c00b45cf2b 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; -import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; +import de.tum.in.www1.artemis.exercise.programming.MockDelegate; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.ModelingSubmissionService; import de.tum.in.www1.artemis.service.TextBlockService; diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractAthenaTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractAthenaTest.java index bc6a7db7977d..80a8a71e339c 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractAthenaTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractAthenaTest.java @@ -9,7 +9,7 @@ /** * Base class for Athena tests providing common functionality */ -public abstract class AbstractAthenaTest extends AbstractSpringIntegrationIndependentTest { +public abstract class AbstractAthenaTest extends AbstractSpringIntegrationJenkinsGitlabTest { @Autowired protected AthenaRequestMockProvider athenaRequestMockProvider; diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java index 775f33ba9499..c2cc702ca0a7 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationJenkinsGitlabTest.java @@ -43,6 +43,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabService; import de.tum.in.www1.artemis.service.connectors.jenkins.JenkinsService; +import de.tum.in.www1.artemis.service.programming.ProgrammingMessagingService; @ResourceLock("AbstractSpringIntegrationJenkinsGitlabTest") // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! @@ -64,6 +65,9 @@ public abstract class AbstractSpringIntegrationJenkinsGitlabTest extends Abstrac @SpyBean protected JenkinsServer jenkinsServer; + @SpyBean + protected ProgrammingMessagingService programmingMessagingService; + @Autowired protected JenkinsRequestMockProvider jenkinsRequestMockProvider; diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java index b01bda5603eb..a49574d304ff 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationLocalCILocalVCTest.java @@ -59,7 +59,8 @@ "artemis.version-control.local-vcs-repo-path=${java.io.tmpdir}", "artemis.build-logs-path=${java.io.tmpdir}/build-logs", "artemis.continuous-integration.specify-concurrent-builds=true", "artemis.continuous-integration.concurrent-build-size=1", "artemis.continuous-integration.asynchronous=false", "artemis.continuous-integration.build.images.java.default=dummy-docker-image", - "artemis.continuous-integration.image-cleanup.enabled=true", "spring.liquibase.enabled=true" }) + "artemis.continuous-integration.image-cleanup.enabled=true", "artemis.continuous-integration.image-cleanup.disk-space-threshold-mb=1000000000", + "spring.liquibase.enabled=true" }) @ContextConfiguration(classes = LocalCITestConfiguration.class) public abstract class AbstractSpringIntegrationLocalCILocalVCTest extends AbstractArtemisIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/BuildPlanIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/BuildPlanIntegrationTest.java index bd066489eaa3..db8ab14bb24a 100644 --- a/src/test/java/de/tum/in/www1/artemis/BuildPlanIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/BuildPlanIntegrationTest.java @@ -14,7 +14,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.ProjectType; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.BuildPlanRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.service.programming.ProgrammingTriggerService; diff --git a/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java index 3d18ef4b1d7d..121147ac159a 100644 --- a/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LongFeedbackResourceIntegrationTest.java @@ -14,7 +14,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.user.UserUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java index 417ab7993f11..327143f249bf 100644 --- a/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/Lti13LaunchIntegrationTest.java @@ -22,10 +22,8 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.in.www1.artemis.config.lti.CustomLti13Configurer; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.user.UserUtilService; -import de.tum.in.www1.artemis.web.rest.open.PublicLtiResource; import io.jsonwebtoken.Jwts; /** @@ -74,7 +72,7 @@ void redirectProxy() throws Exception { body.put("id_token", VALID_ID_TOKEN); body.put("state", VALID_STATE); - URI header = request.postForm(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.FOUND); + URI header = request.postForm("/api/public/lti13/auth-callback", body, HttpStatus.FOUND); validateRedirect(header, VALID_ID_TOKEN); } @@ -85,7 +83,7 @@ void redirectProxyNoState() throws Exception { Map<String, Object> body = new HashMap<>(); body.put("id_token", VALID_ID_TOKEN); - request.postFormWithoutLocation(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.BAD_REQUEST); + request.postFormWithoutLocation("/api/public/lti13/auth-callback", body, HttpStatus.BAD_REQUEST); } @Test @@ -94,7 +92,7 @@ void redirectProxyNoToken() throws Exception { Map<String, Object> body = new HashMap<>(); body.put("state", VALID_STATE); - request.postFormWithoutLocation(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.BAD_REQUEST); + request.postFormWithoutLocation("/api/public/lti13/auth-callback", body, HttpStatus.BAD_REQUEST); } @Test @@ -104,7 +102,7 @@ void redirectProxyInvalidToken() throws Exception { body.put("state", VALID_STATE); body.put("id_token", "invalid-token"); - request.postFormWithoutLocation(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.BAD_REQUEST); + request.postFormWithoutLocation("/api/public/lti13/auth-callback", body, HttpStatus.BAD_REQUEST); } @Test @@ -114,7 +112,7 @@ void redirectProxyOutdatedToken() throws Exception { body.put("state", VALID_STATE); body.put("id_token", OUTDATED_TOKEN); - request.postFormWithoutLocation(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.BAD_REQUEST); + request.postFormWithoutLocation("/api/public/lti13/auth-callback", body, HttpStatus.BAD_REQUEST); } @Test @@ -126,19 +124,19 @@ void redirectProxyTokenInvalidSignature() throws Exception { body.put("state", VALID_STATE); body.put("id_token", invalidSignatureToken); - URI header = request.postForm(CustomLti13Configurer.LTI13_LOGIN_REDIRECT_PROXY_PATH, body, HttpStatus.FOUND); + URI header = request.postForm("/api/public/lti13/auth-callback", body, HttpStatus.FOUND); validateRedirect(header, invalidSignatureToken); } @Test @WithMockUser(value = "student1", roles = "USER") void oidcFlowFails_noRequestCached() throws Exception { - String ltiLaunchUri = CustomLti13Configurer.LTI13_LOGIN_PATH + "?id_token=some-token&state=some-state"; + String ltiLaunchUri = "/api/public/lti13/auth-login?id_token=some-token&state=some-state"; request.get(ltiLaunchUri, HttpStatus.INTERNAL_SERVER_ERROR, Object.class); } private void validateRedirect(URI locationHeader, String token) { - assertThat(locationHeader.getPath()).isEqualTo(PublicLtiResource.LOGIN_REDIRECT_CLIENT_PATH); + assertThat(locationHeader.getPath()).isEqualTo("/lti/launch"); List<NameValuePair> params = URLEncodedUtils.parse(locationHeader, StandardCharsets.UTF_8); assertUriParamsContain(params, "id_token", token); diff --git a/src/test/java/de/tum/in/www1/artemis/LtiDeepLinkingIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LtiDeepLinkingIntegrationTest.java index 2245af49968c..69cb5b774564 100644 --- a/src/test/java/de/tum/in/www1/artemis/LtiDeepLinkingIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LtiDeepLinkingIntegrationTest.java @@ -32,7 +32,7 @@ import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.lti.Claims; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.user.UserUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java index 56f8dfa326ae..e90a3bebf6c0 100644 --- a/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LtiIntegrationTest.java @@ -12,7 +12,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -20,6 +19,8 @@ import org.hibernate.Hibernate; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; @@ -60,13 +61,14 @@ void getAllConfiguredLtiPlatformsAsAdmin() throws Exception { LtiPlatformConfiguration platform1 = new LtiPlatformConfiguration(); platform1.setId(1L); fillLtiPlatformConfig(platform1); + ltiPlatformConfigurationRepository.save(platform1); LtiPlatformConfiguration platform2 = new LtiPlatformConfiguration(); platform1.setId(2L); fillLtiPlatformConfig(platform2); + ltiPlatformConfigurationRepository.save(platform2); - List<LtiPlatformConfiguration> expectedPlatforms = Arrays.asList(platform1, platform2); - doReturn(expectedPlatforms).when(ltiPlatformConfigurationRepository).findAll(); + Page<LtiPlatformConfiguration> expectedPlatforms = ltiPlatformConfigurationRepository.findAll(Pageable.unpaged()); MvcResult mvcResult = request.performMvcRequest(get("/api/lti-platforms")).andExpect(status().isOk()).andReturn(); @@ -75,8 +77,7 @@ void getAllConfiguredLtiPlatformsAsAdmin() throws Exception { // Empty block intended for type inference by Jackson's ObjectMapper }); - assertThat(actualPlatforms).hasSize(expectedPlatforms.size()); - assertThat(actualPlatforms).usingRecursiveComparison().isEqualTo(expectedPlatforms); + assertThat(actualPlatforms).hasSize(expectedPlatforms.getSize()); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/LtiQuizIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/LtiQuizIntegrationTest.java index 1822f3151e34..0d0103d60d6d 100644 --- a/src/test/java/de/tum/in/www1/artemis/LtiQuizIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/LtiQuizIntegrationTest.java @@ -35,13 +35,13 @@ import de.tum.in.www1.artemis.domain.quiz.DragAndDropQuestion; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.QuizExerciseRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; -import de.tum.in.www1.artemis.service.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; diff --git a/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java index 6bdc51929d23..a2648b226167 100644 --- a/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/ManagementResourceIntegrationTest.java @@ -24,8 +24,8 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.PersistenceAuditEventRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java index 5a18c6e443c6..f1aee80325b8 100644 --- a/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/OAuth2JWKSIntegrationTest.java @@ -7,9 +7,8 @@ import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithAnonymousUser; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import de.tum.in.www1.artemis.course.CourseFactory; import de.tum.in.www1.artemis.domain.Course; @@ -33,8 +32,8 @@ class OAuth2JWKSIntegrationTest extends AbstractSpringIntegrationIndependentTest void getKeysetIsPublicAndReturnsJson() throws Exception { String keyset = request.get("/.well-known/jwks.json", HttpStatus.OK, String.class); - JsonObject jsonKeyset = JsonParser.parseString(keyset).getAsJsonObject(); - assertThat(jsonKeyset).isNotNull(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonKeyset = objectMapper.readTree(keyset); assertThat(jsonKeyset.get("keys")).isNotNull(); } @@ -57,9 +56,11 @@ void getKeysetHasKey() throws Exception { oAuth2JWKSService.updateKey(TEST_PREFIX + "registrationId"); String keyset = request.get("/.well-known/jwks.json", HttpStatus.OK, String.class); - JsonObject jsonKeyset = JsonParser.parseString(keyset).getAsJsonObject(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonKeyset = objectMapper.readTree(keyset); + assertThat(jsonKeyset).isNotNull(); - JsonElement keys = jsonKeyset.get("keys"); + JsonNode keys = jsonKeyset.get("keys"); assertThat(keys).isNotNull(); } } diff --git a/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java index 98c1f24c4499..6b794c2f05ba 100644 --- a/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/StatisticsIntegrationTest.java @@ -32,9 +32,9 @@ import de.tum.in.www1.artemis.domain.metis.AnswerPost; import de.tum.in.www1.artemis.domain.metis.Post; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.GradingScaleRepository; import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/architecture/AbstractArchitectureTest.java b/src/test/java/de/tum/in/www1/artemis/architecture/AbstractArchitectureTest.java index fe378cb6271f..915cbdf7d9b0 100644 --- a/src/test/java/de/tum/in/www1/artemis/architecture/AbstractArchitectureTest.java +++ b/src/test/java/de/tum/in/www1/artemis/architecture/AbstractArchitectureTest.java @@ -4,6 +4,7 @@ import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.base.DescribedPredicate.or; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.core.domain.properties.HasType.Predicates.rawType; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; import static org.assertj.core.api.Assertions.assertThat; @@ -22,6 +23,7 @@ import com.tngtech.archunit.core.domain.JavaCodeUnit; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaParameter; +import com.tngtech.archunit.core.domain.properties.HasAnnotations; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.lang.ArchCondition; @@ -102,6 +104,10 @@ protected DescribedPredicate<? super JavaCodeUnit> declaredClassSimpleName(Strin return equalTo(name).as("Declared in class with simple name " + name).onResultOf(unit -> unit.getOwner().getSimpleName()); } + protected <T extends HasAnnotations<T>> JavaAnnotation<? extends T> findJavaAnnotation(T item, Class<?> annotationClass) { + return item.getAnnotations().stream().filter(rawType(annotationClass)).findAny().orElseThrow(); + } + protected ArchCondition<JavaMethod> haveAllParametersAnnotatedWithUnless(DescribedPredicate<? super JavaAnnotation<?>> annotationPredicate, DescribedPredicate<JavaClass> exception) { return new ArchCondition<>("have all parameters annotated with " + annotationPredicate.getDescription()) { diff --git a/src/test/java/de/tum/in/www1/artemis/architecture/ArchitectureTest.java b/src/test/java/de/tum/in/www1/artemis/architecture/ArchitectureTest.java index d5f6a746b323..ee3d238df751 100644 --- a/src/test/java/de/tum/in/www1/artemis/architecture/ArchitectureTest.java +++ b/src/test/java/de/tum/in/www1/artemis/architecture/ArchitectureTest.java @@ -14,7 +14,6 @@ import static com.tngtech.archunit.core.domain.JavaCodeUnit.Predicates.constructor; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameMatching; import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; -import static com.tngtech.archunit.core.domain.properties.HasType.Predicates.rawType; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; @@ -209,7 +208,7 @@ void testGsonExclusion() { var gsonUsageRule = noClasses().should().accessClassesThat().resideInAnyPackage("com.google.gson..").because("we use an alternative JSON parsing library."); var result = gsonUsageRule.evaluate(allClasses); // TODO: reduce the following number to 0 - assertThat(result.getFailureReport().getDetails()).hasSizeLessThanOrEqualTo(816); + assertThat(result.getFailureReport().getDetails()).hasSize(733); } /** @@ -253,7 +252,7 @@ private <T extends HasAnnotations<T>> ArchCondition<T> useJsonIncludeNonEmpty() @Override public void check(T item, ConditionEvents events) { - var annotation = item.getAnnotations().stream().filter(rawType(JsonInclude.class)).findAny().orElseThrow(); + var annotation = findJavaAnnotation(item, JsonInclude.class); var valueProperty = annotation.tryGetExplicitlyDeclaredProperty("value"); if (valueProperty.isEmpty()) { // @JsonInclude() is ok since it allows explicitly including properties diff --git a/src/test/java/de/tum/in/www1/artemis/architecture/ResourceArchitectureTest.java b/src/test/java/de/tum/in/www1/artemis/architecture/ResourceArchitectureTest.java index 3ef7b9410ea3..3ef93f9d1519 100644 --- a/src/test/java/de/tum/in/www1/artemis/architecture/ResourceArchitectureTest.java +++ b/src/test/java/de/tum/in/www1/artemis/architecture/ResourceArchitectureTest.java @@ -1,15 +1,34 @@ package de.tum.in.www1.artemis.architecture; +import static com.tngtech.archunit.lang.ConditionEvent.createMessage; +import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.domain.properties.HasAnnotations; +import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation; +import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; import de.tum.in.www1.artemis.web.rest.ogparser.LinkPreviewResource; @@ -37,4 +56,63 @@ void allPublicMethodsShouldReturnResponseEntity() { JavaClasses classes = classesExcept(allClasses, LinkPreviewResource.class); rule.check(classes); } + + private static final Set<Class<? extends Annotation>> annotationClasses = Set.of(GetMapping.class, PatchMapping.class, PostMapping.class, PutMapping.class, DeleteMapping.class, + RequestMapping.class); + + @Test + void shouldCorrectlyUseRequestMappingAnnotations() { + classes().that().areAnnotatedWith(RequestMapping.class).should(haveCorrectRequestMappingPathForClasses()).check(productionClasses); + for (var annotation : annotationClasses) { + methods().that().areAnnotatedWith(annotation).should(haveCorrectRequestMappingPathForMethods(annotation)).allowEmptyShould(true).check(productionClasses); + } + } + + private ArchCondition<JavaClass> haveCorrectRequestMappingPathForClasses() { + return new ArchCondition<>("correctly use @RequestMapping") { + + @Override + public void check(JavaClass javaClass, ConditionEvents conditionEvents) { + testRequestAnnotation(javaClass, RequestMapping.class, conditionEvents, value -> { + if (value.startsWith("/")) { + conditionEvents.add(violated(javaClass, createMessage(javaClass, "The @RequestMapping path value should not start with /"))); + } + if (!value.endsWith("/")) { + conditionEvents.add(violated(javaClass, createMessage(javaClass, "The @RequestMapping path value should always end with /"))); + } + }); + } + }; + } + + private ArchCondition<JavaMethod> haveCorrectRequestMappingPathForMethods(Class<?> annotationClass) { + return new ArchCondition<>("correctly use @RequestMapping") { + + @Override + public void check(JavaMethod javaMethod, ConditionEvents conditionEvents) { + testRequestAnnotation(javaMethod, annotationClass, conditionEvents, value -> { + if (value.startsWith("/")) { + conditionEvents.add(violated(javaMethod, createMessage(javaMethod, "The @RequestMapping path value should not start with /"))); + } + if (value.endsWith("/")) { + conditionEvents.add(violated(javaMethod, createMessage(javaMethod, "The @RequestMapping path value should not end with /"))); + } + }); + } + }; + } + + private <T extends HasAnnotations<T> & HasDescription & HasSourceCodeLocation> void testRequestAnnotation(T item, Class<?> annotationClass, ConditionEvents conditionEvents, + Consumer<String> tester) { + var annotation = findJavaAnnotation(item, annotationClass); + var valueProperty = annotation.tryGetExplicitlyDeclaredProperty("value"); + if (valueProperty.isEmpty()) { + conditionEvents.add(violated(item, createMessage(item, "RequestMapping should declare a path value."))); + return; + } + String[] values = ((String[]) valueProperty.get()); + for (String value : values) { + tester.accept(value); + } + } } diff --git a/src/test/java/de/tum/in/www1/artemis/aspects/EnforceRoleInExerciseTest.java b/src/test/java/de/tum/in/www1/artemis/aspects/EnforceRoleInExerciseTest.java index b899669a1d80..b7a7f71cdeb7 100644 --- a/src/test/java/de/tum/in/www1/artemis/aspects/EnforceRoleInExerciseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/aspects/EnforceRoleInExerciseTest.java @@ -10,7 +10,7 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; class EnforceRoleInExerciseTest extends AbstractEnforceRoleInResourceTest { diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java index 3c52fecf014d..1db0f785ac0d 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentComplaintIntegrationTest.java @@ -40,10 +40,10 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamFactory; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ComplaintRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java index 73d33f502525..0e82384191c7 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/AssessmentTeamComplaintIntegrationTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ComplaintRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java index 733fba08f22e..64259293b1b6 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ComplaintResponseIntegrationTest.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.repository.ComplaintRepository; import de.tum.in.www1.artemis.repository.ComplaintResponseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java index 622bb0de98ea..9a3c1551c790 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ExampleSubmissionIntegrationTest.java @@ -38,8 +38,8 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.TutorParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java index 81b1875f7ec4..d99d19a20ce6 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.domain.Team; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; import de.tum.in.www1.artemis.repository.TeamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java index f4fdf3a857c5..6956d4067e55 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/GradeStepIntegrationTest.java @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.GradingScaleRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java index dbf5935ac0ca..47497c601bc8 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ParticipantScoreIntegrationTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java index e64f182aaafe..9587063a35fa 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/RatingResourceIntegrationTest.java @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.service.RatingService; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 3a6c60b4d528..0cd8b1efc177 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -53,12 +53,12 @@ import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.GradingCriterionUtil; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java index 7e31bbae4836..e4698d3fdd03 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorEffortIntegrationTest.java @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.analytics.TextAssessmentEvent; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.statistics.tutor.effort.TutorEffort; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.TextAssessmentEventRepository; import de.tum.in.www1.artemis.repository.TextSubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java index c85e0eb84d29..3908eac3f15d 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/TutorLeaderboardServiceIntegrationTest.java @@ -17,7 +17,7 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.UserRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java index 141ee8bde7eb..74542e17be8d 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/InternalAuthenticationIntegrationTest.java @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.AuthorityRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/LdapAuthenticationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/LdapAuthenticationIntegrationTest.java index 678b97964c76..5288deda51f7 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/LdapAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/LdapAuthenticationIntegrationTest.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.AuthorityRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java index 0c4f4a787335..7a8f66f26724 100644 --- a/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/authentication/UserJenkinsGitlabIntegrationTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabPersonalAccessTokenManagementService; import de.tum.in.www1.artemis.service.connectors.gitlab.GitLabUserManagementService; diff --git a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java index b02cc90fa721..36f87ba49ddb 100644 --- a/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/bonus/BonusIntegrationTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.repository.BonusRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java index e2061a7074a8..d413c9197ad8 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.domain.lecture.TextUnit; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java index 568837ddcc50..524cad5cf8de 100644 --- a/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java +++ b/src/test/java/de/tum/in/www1/artemis/config/MetricsBeanTest.java @@ -22,9 +22,9 @@ import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java index 26afb3a71a72..089da4b67b31 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java @@ -6,6 +6,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreators.withException; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; +import java.io.IOException; import java.net.SocketTimeoutException; import org.mockito.MockitoAnnotations; @@ -199,7 +200,7 @@ public void mockGetFeedbackSuggestionsAndExpect(String moduleType, RequestMatche suggestion = suggestion.put("indexStart", 3).put("indexEnd", 9); } else if (moduleType.equals("programming")) { - suggestion = suggestion.put("lineStart", 3).put("lineEnd", 4); + suggestion = suggestion.put("lineStart", 3).put("lineEnd", 4).put("description", "invoke infinite compression here").put("filePath", "client.cpp"); } else if (moduleType.equals("modeling")) { suggestion = suggestion.put("credits", 0); @@ -214,6 +215,25 @@ else if (moduleType.equals("modeling")) { responseActions.andRespond(withSuccess(node.toString(), MediaType.APPLICATION_JSON)); } + /** + * Mocks the /feedback_suggestions API from Athena used to retrieve feedback suggestions for a submission + * Makes the endpoint fail + * + * @param moduleType The type of the module: "text" or "programming" + * @param expectedContents The expected contents of the request + */ + public void mockGetFeedbackSuggestionsWithFailure(String moduleType, RequestMatcher... expectedContents) { + ResponseActions responseActions = mockServer + .expect(ExpectedCount.once(), requestTo(athenaUrl + "/modules/" + moduleType + "/" + getTestModuleName(moduleType) + "/feedback_suggestions")) + .andExpect(method(HttpMethod.POST)).andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + for (RequestMatcher matcher : expectedContents) { + responseActions.andExpect(matcher); + } + + responseActions.andRespond(withException(new IOException("Service unavailable"))); + } + /** * Mocks the /modules API from Athena used to retrieve all available feedback suggestion modules * Makes the endpoint return an empty module list. diff --git a/src/test/java/de/tum/in/www1/artemis/connector/GitlabRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/GitlabRequestMockProvider.java index 76499d8e3a65..8bbeb53530e7 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/GitlabRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/GitlabRequestMockProvider.java @@ -94,7 +94,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.UriService; diff --git a/src/test/java/de/tum/in/www1/artemis/connector/IrisRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/IrisRequestMockProvider.java index 053da2d2e4a9..b24ec863b1bb 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/IrisRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/IrisRequestMockProvider.java @@ -3,11 +3,12 @@ import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withRawStatus; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import java.net.URL; -import java.time.ZonedDateTime; import java.util.Map; +import java.util.function.Consumer; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; @@ -15,19 +16,22 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.stereotype.Component; import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.response.MockRestResponseCreators; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisErrorResponseDTO; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisMessageResponseV2DTO; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisModelDTO; -import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisStatusDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisErrorResponseDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisHealthStatusDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.PyrisModelDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatPipelineExecutionDTO; @Component @Profile("iris") @@ -41,16 +45,13 @@ public class IrisRequestMockProvider { private MockRestServiceServer shortTimeoutMockServer; - @Value("${artemis.iris.url}/api/v1/messages") - private URL messagesApiV1URL; - - @Value("${artemis.iris.url}/api/v2/messages") - private URL messagesApiV2URL; + @Value("${artemis.iris.url}/api/v1/pipelines") + private URL pipelinesApiURL; @Value("${artemis.iris.url}/api/v1/models") private URL modelsApiURL; - @Value("${artemis.iris.url}/api/v1/health") + @Value("${artemis.iris.url}/api/v1/health/") private URL healthApiURL; @Autowired @@ -58,7 +59,7 @@ public class IrisRequestMockProvider { private AutoCloseable closeable; - public IrisRequestMockProvider(@Qualifier("irisRestTemplate") RestTemplate restTemplate, @Qualifier("shortTimeoutIrisRestTemplate") RestTemplate shortTimeoutRestTemplate) { + public IrisRequestMockProvider(@Qualifier("pyrisRestTemplate") RestTemplate restTemplate, @Qualifier("shortTimeoutPyrisRestTemplate") RestTemplate shortTimeoutRestTemplate) { this.restTemplate = restTemplate; this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; } @@ -79,28 +80,18 @@ public void reset() throws Exception { } } - /** - * Mocks a message with empty response from the Pyris message endpoint - */ - public void mockEmptyResponse() { - // @formatter:off - mockServer.expect(ExpectedCount.once(), requestTo(messagesApiV1URL.toString())) - .andExpect(method(HttpMethod.POST)) - .andRespond(withSuccess()); - // @formatter:on + public void mockRunResponse(Consumer<PyrisTutorChatPipelineExecutionDTO> responseConsumer) { + mockServer.expect(ExpectedCount.once(), requestTo(pipelinesApiURL + "/tutor-chat/default/run")).andExpect(method(HttpMethod.POST)).andRespond(request -> { + var mockRequest = (MockClientHttpRequest) request; + var dto = mapper.readValue(mockRequest.getBodyAsString(), PyrisTutorChatPipelineExecutionDTO.class); + responseConsumer.accept(dto); + return MockRestResponseCreators.withRawStatus(HttpStatus.ACCEPTED.value()).createResponse(request); + }); } - /** - * Mocks a message response from the Pyris V2 message endpoint - * - * @param responseContent The content of the response - * @throws JsonProcessingException If the response content cannot be serialized to JSON - */ - public void mockMessageV2Response(Map<?, ?> responseContent) throws JsonProcessingException { - var dto = new IrisMessageResponseV2DTO(null, ZonedDateTime.now(), mapper.valueToTree(responseContent)); - var json = mapper.writeValueAsString(dto); - - mockCustomJsonResponse(messagesApiV2URL, json); + public void mockRunError(int httpStatus) { + mockServer.expect(ExpectedCount.once(), requestTo(pipelinesApiURL + "/tutor-chat/default/run")).andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.valueOf(httpStatus))); } public void mockCustomJsonResponse(URL requestUrl, String responseJson) { @@ -111,16 +102,8 @@ public void mockCustomJsonResponse(URL requestUrl, String responseJson) { // @formatter:on } - public void mockMessageV1Error(int status) throws JsonProcessingException { - mockMessageError(messagesApiV1URL, status); - } - - public void mockMessageV2Error(int status) throws JsonProcessingException { - mockMessageError(messagesApiV2URL, status); - } - private void mockMessageError(URL requestUrl, int status) throws JsonProcessingException { - var json = Map.of("detail", new IrisErrorResponseDTO("Test error")); + var json = Map.of("detail", new PyrisErrorResponseDTO("Test error")); // @formatter:off mockServer.expect(ExpectedCount.once(), requestTo(requestUrl.toString())) .andExpect(method(HttpMethod.POST)) @@ -129,8 +112,8 @@ private void mockMessageError(URL requestUrl, int status) throws JsonProcessingE } public void mockModelsResponse() throws JsonProcessingException { - var irisModelDTO = new IrisModelDTO("TEST_MODEL", "Test model", "Test description"); - var irisModelDTOArray = new IrisModelDTO[] { irisModelDTO }; + var irisModelDTO = new PyrisModelDTO("TEST_MODEL", "Test model", "Test description"); + var irisModelDTOArray = new PyrisModelDTO[] { irisModelDTO }; // @formatter:off mockServer.expect(ExpectedCount.once(), requestTo(modelsApiURL.toString())) .andExpect(method(HttpMethod.GET)) @@ -139,8 +122,9 @@ public void mockModelsResponse() throws JsonProcessingException { } public void mockStatusResponse() throws JsonProcessingException { - var irisStatusDTOArray = new IrisStatusDTO[] { new IrisStatusDTO("TEST_MODEL_UP", IrisStatusDTO.ModelStatus.UP), - new IrisStatusDTO("TEST_MODEL_DOWN", IrisStatusDTO.ModelStatus.DOWN), new IrisStatusDTO("TEST_MODEL_NA", IrisStatusDTO.ModelStatus.NOT_AVAILABLE) }; + var irisStatusDTOArray = new PyrisHealthStatusDTO[] { new PyrisHealthStatusDTO("TEST_MODEL_UP", PyrisHealthStatusDTO.ModelStatus.UP), + new PyrisHealthStatusDTO("TEST_MODEL_DOWN", PyrisHealthStatusDTO.ModelStatus.DOWN), + new PyrisHealthStatusDTO("TEST_MODEL_NA", PyrisHealthStatusDTO.ModelStatus.NOT_AVAILABLE) }; // @formatter:off shortTimeoutMockServer.expect(ExpectedCount.once(), requestTo(healthApiURL.toString())) .andExpect(method(HttpMethod.GET)) diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java index 71e0092de3ee..79ec3ac7bace 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/Lti13ServiceTest.java @@ -38,8 +38,10 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; @@ -144,8 +146,8 @@ void performLaunch_exerciseFound() { when(oidcIdToken.getClaim("sub")).thenReturn("1"); when(oidcIdToken.getClaim("iss")).thenReturn("https://otherDomain.com"); when(oidcIdToken.getClaim(Claims.LTI_DEPLOYMENT_ID)).thenReturn("1"); - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("id", "resourceLinkUrl"); + ObjectNode jsonObject = new ObjectMapper().createObjectNode(); + jsonObject.put("id", "resourceLinkUrl"); when(oidcIdToken.getClaim(Claims.RESOURCE_LINK)).thenReturn(jsonObject); prepareForPerformLaunch(result.courseId(), result.exerciseId()); @@ -415,7 +417,7 @@ void onNewResultTokenFails() { } @Test - void onNewResult() { + void onNewResult() throws JsonProcessingException { Result result = new Result(); double scoreGiven = 60D; result.setScore(scoreGiven); @@ -463,15 +465,15 @@ void onNewResult() { assertThat(authHeaders).as("Score publish request must contain an Authorization header").isNotNull(); assertThat(authHeaders).as("Score publish request must contain the corresponding Authorization Bearer token").contains("Bearer " + accessToken); - JsonObject body = JsonParser.parseString(Objects.requireNonNull(httpEntity.getBody())).getAsJsonObject(); - assertThat(body.get("userId").getAsString()).as("Invalid parameter in score publish request: userId").isEqualTo(launch.getSub()); - assertThat(body.get("timestamp").getAsString()).as("Parameter missing in score publish request: timestamp").isNotNull(); - assertThat(body.get("activityProgress").getAsString()).as("Parameter missing in score publish request: activityProgress").isNotNull(); - assertThat(body.get("gradingProgress").getAsString()).as("Parameter missing in score publish request: gradingProgress").isNotNull(); + JsonNode body = new ObjectMapper().readTree(Objects.requireNonNull(httpEntity.getBody())); + assertThat(body.get("userId").asText()).as("Invalid parameter in score publish request: userId").isEqualTo(launch.getSub()); + assertThat(body.get("timestamp").asText()).as("Parameter missing in score publish request: timestamp").isNotNull(); + assertThat(body.get("activityProgress").asText()).as("Parameter missing in score publish request: activityProgress").isNotNull(); + assertThat(body.get("gradingProgress").asText()).as("Parameter missing in score publish request: gradingProgress").isNotNull(); - assertThat(body.get("comment").getAsString()).as("Invalid parameter in score publish request: comment").isEqualTo("Good job. Not so good"); - assertThat(body.get("scoreGiven").getAsDouble()).as("Invalid parameter in score publish request: scoreGiven").isEqualTo(scoreGiven); - assertThat(body.get("scoreMaximum").getAsDouble()).as("Invalid parameter in score publish request: scoreMaximum").isEqualTo(100d); + assertThat(body.get("comment").asText()).as("Invalid parameter in score publish request: comment").isEqualTo("Good job. Not so good"); + assertThat(body.get("scoreGiven").asDouble()).as("Invalid parameter in score publish request: scoreGiven").isEqualTo(scoreGiven); + assertThat(body.get("scoreMaximum").asDouble()).as("Invalid parameter in score publish request: scoreMaximum").isEqualTo(100d); assertThat(launch.getScoreLineItemUrl() + "/scores").as("Score publish request was sent to a wrong URI").isEqualTo(urlCapture.getValue()); diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/LtiDynamicRegistrationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/LtiDynamicRegistrationServiceTest.java index 635a0539d6cb..0879a4599d4e 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/LtiDynamicRegistrationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/LtiDynamicRegistrationServiceTest.java @@ -67,12 +67,7 @@ void init() { openIdConfigurationUrl = "url"; registrationToken = "token"; - platformConfiguration = new Lti13PlatformConfiguration(); - platformConfiguration.setTokenEndpoint("token"); - platformConfiguration.setAuthorizationEndpoint("auth"); - platformConfiguration.setRegistrationEndpoint("register"); - platformConfiguration.setJwksUri("jwks"); - + platformConfiguration = new Lti13PlatformConfiguration(null, "token", "auth", "jwks", "register"); clientRegistrationResponse = new Lti13ClientRegistration(); } @@ -95,7 +90,7 @@ void badRequestWhenGetPlatformConfigurationFails() { @Test void badRequestWhenPlatformConfigurationEmpty() { - Lti13PlatformConfiguration platformConfiguration = new Lti13PlatformConfiguration(); + Lti13PlatformConfiguration platformConfiguration = new Lti13PlatformConfiguration(null, null, null, null, null); when(restTemplate.getForEntity(openIdConfigurationUrl, Lti13PlatformConfiguration.class)).thenReturn(ResponseEntity.accepted().body(platformConfiguration)); assertThatExceptionOfType(BadRequestAlertException.class) @@ -104,10 +99,7 @@ void badRequestWhenPlatformConfigurationEmpty() { @Test void badRequestWhenRegistrationEndpointEmpty() { - Lti13PlatformConfiguration platformConfiguration = new Lti13PlatformConfiguration(); - platformConfiguration.setAuthorizationEndpoint("auth"); - platformConfiguration.setJwksUri("uri"); - platformConfiguration.setTokenEndpoint("token"); + Lti13PlatformConfiguration platformConfiguration = new Lti13PlatformConfiguration(null, "token", "auth", "uri", null); when(restTemplate.getForEntity(openIdConfigurationUrl, Lti13PlatformConfiguration.class)).thenReturn(ResponseEntity.accepted().body(platformConfiguration)); @@ -120,7 +112,7 @@ void badRequestWhenGetPostClientRegistrationFails() { when(restTemplate.getForEntity(eq(openIdConfigurationUrl), any())).thenReturn(ResponseEntity.accepted().body(platformConfiguration)); - doThrow(HttpClientErrorException.class).when(restTemplate).postForEntity(eq(platformConfiguration.getRegistrationEndpoint()), any(), eq(Lti13ClientRegistration.class)); + doThrow(HttpClientErrorException.class).when(restTemplate).postForEntity(eq(platformConfiguration.registrationEndpoint()), any(), eq(Lti13ClientRegistration.class)); assertThatExceptionOfType(BadRequestAlertException.class) .isThrownBy(() -> ltiDynamicRegistrationService.performDynamicRegistration(openIdConfigurationUrl, registrationToken)); @@ -130,7 +122,7 @@ void badRequestWhenGetPostClientRegistrationFails() { void performDynamicRegistrationSuccess() { when(restTemplate.getForEntity(openIdConfigurationUrl, Lti13PlatformConfiguration.class)).thenReturn(ResponseEntity.accepted().body(platformConfiguration)); - when(restTemplate.postForEntity(eq(platformConfiguration.getRegistrationEndpoint()), any(), eq(Lti13ClientRegistration.class))) + when(restTemplate.postForEntity(eq(platformConfiguration.registrationEndpoint()), any(), eq(Lti13ClientRegistration.class))) .thenReturn(ResponseEntity.accepted().body(clientRegistrationResponse)); ltiDynamicRegistrationService.performDynamicRegistration(openIdConfigurationUrl, registrationToken); @@ -143,7 +135,7 @@ void performDynamicRegistrationSuccess() { void performDynamicRegistrationSuccessWithoutToken() { when(restTemplate.getForEntity(openIdConfigurationUrl, Lti13PlatformConfiguration.class)).thenReturn(ResponseEntity.accepted().body(platformConfiguration)); - when(restTemplate.postForEntity(eq(platformConfiguration.getRegistrationEndpoint()), any(), eq(Lti13ClientRegistration.class))) + when(restTemplate.postForEntity(eq(platformConfiguration.registrationEndpoint()), any(), eq(Lti13ClientRegistration.class))) .thenReturn(ResponseEntity.accepted().body(clientRegistrationResponse)); ltiDynamicRegistrationService.performDynamicRegistration(openIdConfigurationUrl, null); diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 60bc22d073dd..f66759843e21 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -104,12 +104,12 @@ import de.tum.in.www1.artemis.exam.ExamFactory; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.MockDelegate; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; @@ -347,7 +347,7 @@ private void adjustCourseGroups(Course course, String suffix) { // Test public void testCreateCourseWithPermission() throws Exception { assertThatThrownBy(() -> courseRepo.findByIdElseThrow(Long.MAX_VALUE)).isInstanceOf(EntityNotFoundException.class); - assertThatThrownBy(() -> courseRepo.findByIdWithExercisesAndLecturesElseThrow(Long.MAX_VALUE)).isInstanceOf(EntityNotFoundException.class); + assertThatThrownBy(() -> courseRepo.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(Long.MAX_VALUE)).isInstanceOf(EntityNotFoundException.class); assertThatThrownBy(() -> courseRepo.findWithEagerOrganizationsElseThrow(Long.MAX_VALUE)).isInstanceOf(EntityNotFoundException.class); assertThatThrownBy(() -> courseRepo.findByIdWithEagerExercisesElseThrow(Long.MAX_VALUE)).isInstanceOf(EntityNotFoundException.class); diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java index 76c8cd87562d..2bd7d6ae3498 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.nio.charset.Charset; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -13,6 +14,7 @@ import java.util.Optional; import java.util.Set; +import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -52,14 +54,14 @@ import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseFactory; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureFactory; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.organization.OrganizationUtilService; @@ -81,6 +83,7 @@ import de.tum.in.www1.artemis.repository.TextSubmissionRepository; import de.tum.in.www1.artemis.repository.TutorParticipationRepository; import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.ModelingSubmissionService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.TestResourceUtils; @@ -785,7 +788,7 @@ public Course addCourseWithModelingAndTextAndFileUploadExercise() { * @return The generated course */ public Course addCourseWithExercisesAndSubmissions(String userPrefix, String suffix, int numberOfExercises, int numberOfSubmissionPerExercise, int numberOfAssessments, - int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel) { + int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel) throws IOException { return addCourseWithExercisesAndSubmissions("short", userPrefix, suffix, numberOfExercises, numberOfSubmissionPerExercise, numberOfAssessments, numberOfComplaints, typeComplaint, numberComplaintResponses, validModel, true); } @@ -811,7 +814,8 @@ public Course addCourseWithExercisesAndSubmissions(String userPrefix, String suf * @return The generated course. */ public Course addCourseWithExercisesAndSubmissionsWithAssessmentDueDatesInTheFuture(String shortName, String userPrefix, String suffix, int numberOfExercises, - int numberOfSubmissionPerExercise, int numberOfAssessments, int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel) { + int numberOfSubmissionPerExercise, int numberOfAssessments, int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel) + throws IOException { return addCourseWithExercisesAndSubmissions(shortName, userPrefix, suffix, numberOfExercises, numberOfSubmissionPerExercise, numberOfAssessments, numberOfComplaints, typeComplaint, numberComplaintResponses, validModel, false); } @@ -838,7 +842,8 @@ public Course addCourseWithExercisesAndSubmissionsWithAssessmentDueDatesInTheFut * @return The generated course. */ public Course addCourseWithExercisesAndSubmissions(String courseShortName, String userPrefix, String suffix, int numberOfExercises, int numberOfSubmissionPerExercise, - int numberOfAssessments, int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel, boolean assessmentDueDateInThePast) { + int numberOfAssessments, int numberOfComplaints, boolean typeComplaint, int numberComplaintResponses, String validModel, boolean assessmentDueDateInThePast) + throws IOException { Course course = CourseFactory.generateCourse(null, courseShortName, pastTimestamp, futureFutureTimestamp, new HashSet<>(), userPrefix + "student" + suffix, userPrefix + "tutor" + suffix, userPrefix + "editor" + suffix, userPrefix + "instructor" + suffix); ZonedDateTime assessmentDueDate; @@ -852,15 +857,17 @@ public Course addCourseWithExercisesAndSubmissions(String courseShortName, Strin } var tutors = userRepo.getTutors(course).stream().sorted(Comparator.comparing(User::getId)).toList(); + course = courseRepo.save(course); + course.setExercises(new HashSet<>()); // avoid lazy init issue for (int i = 0; i < numberOfExercises; i++) { var currentUser = tutors.get(i % 4); if ((i % 3) == 0) { ModelingExercise modelingExercise = ModelingExerciseFactory.generateModelingExercise(releaseDate, dueDate, assessmentDueDate, DiagramType.ClassDiagram, course); modelingExercise.setTitle("Modeling" + i); + modelingExercise.setCourse(course); + modelingExercise = exerciseRepo.save(modelingExercise); course.addExercises(modelingExercise); - course = courseRepo.save(course); - exerciseRepo.save(modelingExercise); for (int j = 1; j <= numberOfSubmissionPerExercise; j++) { StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(modelingExercise, userPrefix + "student" + j); ModelingSubmission submission = ParticipationFactory.generateModelingSubmission(validModel, true); @@ -881,9 +888,9 @@ public Course addCourseWithExercisesAndSubmissions(String courseShortName, Strin else if ((i % 3) == 1) { TextExercise textExercise = TextExerciseFactory.generateTextExercise(releaseDate, dueDate, assessmentDueDate, course); textExercise.setTitle("Text" + i); + textExercise.setCourse(course); + textExercise = exerciseRepo.save(textExercise); course.addExercises(textExercise); - course = courseRepo.save(course); - exerciseRepo.save(textExercise); for (int j = 1; j <= numberOfSubmissionPerExercise; j++) { TextSubmission submission = ParticipationFactory.generateTextSubmission("submissionText", Language.ENGLISH, true); submission = textExerciseUtilService.saveTextSubmission(textExercise, submission, userPrefix + "student" + j); @@ -899,12 +906,16 @@ else if ((i % 3) == 1) { else { // i.e. (i % 3) == 2 FileUploadExercise fileUploadExercise = FileUploadExerciseFactory.generateFileUploadExercise(releaseDate, dueDate, assessmentDueDate, "png,pdf", course); fileUploadExercise.setTitle("FileUpload" + i); + fileUploadExercise.setCourse(course); + fileUploadExercise = exerciseRepo.save(fileUploadExercise); course.addExercises(fileUploadExercise); - course = courseRepo.save(course); - exerciseRepo.save(fileUploadExercise); for (int j = 1; j <= numberOfSubmissionPerExercise; j++) { - FileUploadSubmission submission = ParticipationFactory.generateFileUploadSubmissionWithFile(true, "path/to/file.pdf"); - fileUploadExerciseUtilService.saveFileUploadSubmission(fileUploadExercise, submission, userPrefix + "student" + j); + FileUploadSubmission submission = ParticipationFactory.generateFileUploadSubmissionWithFile(true, null); + var savedSubmission = fileUploadExerciseUtilService.saveFileUploadSubmission(fileUploadExercise, submission, userPrefix + "student" + j); + var filePath = FileUploadSubmission.buildFilePath(fileUploadExercise.getId(), savedSubmission.getId()).resolve("file.pdf"); + FileUtils.write(filePath.toFile(), "test content", Charset.defaultCharset()); + savedSubmission.setFilePath(FilePathService.publicPathForActualPath(filePath, submission.getId()).toString()); + fileUploadSubmissionRepo.save(savedSubmission); if (numberOfAssessments >= j) { Result result = participationUtilService.generateResultWithScore(submission, currentUser, 3.0); participationUtilService.saveResultInParticipation(submission, result); @@ -914,7 +925,6 @@ else if ((i % 3) == 1) { } } } - course = courseRepo.save(course); return course; } diff --git a/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java b/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java index ed346f73d0b7..8025d5502f88 100644 --- a/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/domain/ExerciseTest.java @@ -21,8 +21,8 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.service.ExerciseService; diff --git a/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java b/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java index c4fbc0d8fe21..924ce6cf2540 100644 --- a/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java +++ b/src/test/java/de/tum/in/www1/artemis/domain/ResultTest.java @@ -16,7 +16,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ResultRepository; class ResultTest extends AbstractSpringIntegrationIndependentTest { diff --git a/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java index 602c24a6c004..78ac26887ed5 100644 --- a/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/entitylistener/ResultListenerIntegrationTest.java @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.domain.scores.ParticipantScore; import de.tum.in.www1.artemis.domain.scores.StudentScore; import de.tum.in.www1.artemis.domain.scores.TeamScore; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.ParticipantScoreRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java index 2d1df6ea19fa..cd489a0d25e9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java @@ -64,10 +64,10 @@ import de.tum.in.www1.artemis.domain.quiz.QuizPool; import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExamLiveEventRepository; import de.tum.in.www1.artemis.repository.ExamRepository; @@ -80,11 +80,11 @@ import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; -import de.tum.in.www1.artemis.service.QuizPoolService; import de.tum.in.www1.artemis.service.dto.StudentDTO; import de.tum.in.www1.artemis.service.exam.ExamAccessService; import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.service.exam.ExamService; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.service.user.PasswordService; import de.tum.in.www1.artemis.user.UserFactory; @@ -770,7 +770,7 @@ void testGetExam_asInstructor() throws Exception { assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> examRepository.findByIdWithExerciseGroupsElseThrow(Long.MAX_VALUE)); - assertThat(examRepository.findAllExercisesByExamId(Long.MAX_VALUE)).isEmpty(); + assertThat(examRepository.findAllExercisesWithDetailsByExamId(Long.MAX_VALUE)).isEmpty(); request.get("/api/courses/" + course1.getId() + "/exams/" + exam1.getId(), HttpStatus.OK, Exam.class); diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java index 08f95492e220..f4bd8da323c5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamParticipationIntegrationTest.java @@ -60,10 +60,10 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.BonusRepository; import de.tum.in.www1.artemis.repository.CourseRepository; @@ -82,9 +82,9 @@ import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; -import de.tum.in.www1.artemis.service.QuizSubmissionService; import de.tum.in.www1.artemis.service.exam.ExamService; import de.tum.in.www1.artemis.service.exam.StudentExamService; +import de.tum.in.www1.artemis.service.quiz.QuizSubmissionService; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.team.TeamUtilService; import de.tum.in.www1.artemis.user.UserUtilService; @@ -247,7 +247,7 @@ void testRemovingAllStudents_AfterParticipatingInExam() throws Exception { List<StudentExam> studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); assertThat(studentExamsDB).hasSize(3); List<StudentParticipation> participationList = new ArrayList<>(); - Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(Exercise[]::new); + Exercise[] exercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()).toArray(Exercise[]::new); for (Exercise value : exercises) { participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); } @@ -268,7 +268,7 @@ void testRemovingAllStudents_AfterParticipatingInExam() throws Exception { assertThat(studentExamsDB).isEmpty(); // Fetch participations - exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(Exercise[]::new); + exercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()).toArray(Exercise[]::new); participationList = new ArrayList<>(); for (Exercise exercise : exercises) { participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); @@ -295,7 +295,7 @@ void testRemovingAllStudentsAndParticipations() throws Exception { List<StudentExam> studentExamsDB = request.getList("/api/courses/" + course1.getId() + "/exams/" + exam.getId() + "/student-exams", HttpStatus.OK, StudentExam.class); assertThat(studentExamsDB).hasSize(3); List<StudentParticipation> participationList = new ArrayList<>(); - Exercise[] exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(Exercise[]::new); + Exercise[] exercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()).toArray(Exercise[]::new); for (Exercise value : exercises) { participationList.addAll(studentParticipationRepository.findByExerciseId(value.getId())); } @@ -318,7 +318,7 @@ void testRemovingAllStudentsAndParticipations() throws Exception { assertThat(studentExamsDB).isEmpty(); // Fetch participations - exercises = examRepository.findAllExercisesByExamId(exam.getId()).toArray(Exercise[]::new); + exercises = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()).toArray(Exercise[]::new); participationList = new ArrayList<>(); for (Exercise exercise : exercises) { participationList.addAll(studentParticipationRepository.findByExerciseId(exercise.getId())); diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java index 451281c37ea7..0ee993d98e50 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamStartTest.java @@ -44,11 +44,11 @@ import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java index a935d30dd649..ba779b961ec4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamUserIntegrationTest.java @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExamUser; import de.tum.in.www1.artemis.domain.exam.StudentExam; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.repository.UserRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java index 2ec80f1ffcc6..cda5fd601d14 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamUtilService.java @@ -39,16 +39,16 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.domain.quiz.QuizPool; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseFactory; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.post.ConversationFactory; @@ -63,7 +63,7 @@ import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ConversationRepository; -import de.tum.in.www1.artemis.service.QuizPoolService; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.user.UserUtilService; /** diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationJenkinsGitlabTest.java index 66106a0858b3..2d2ba57cc4a8 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExerciseGroupIntegrationJenkinsGitlabTest.java @@ -28,9 +28,9 @@ import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java index 934ec6d79b73..b19d093a153f 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java @@ -36,9 +36,9 @@ import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.exam.StudentExam; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/QuizPoolIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/QuizPoolIntegrationTest.java index ec38cb5069a5..8de49015073e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/QuizPoolIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/QuizPoolIntegrationTest.java @@ -23,8 +23,8 @@ import de.tum.in.www1.artemis.domain.quiz.QuizPool; import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; import de.tum.in.www1.artemis.domain.quiz.ShortAnswerQuestion; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.service.QuizPoolService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.service.quiz.QuizPoolService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.RequestUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java index 3dc8017fbe93..417df25f47d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java @@ -106,8 +106,8 @@ import de.tum.in.www1.artemis.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.in.www1.artemis.domain.submissionpolicy.SubmissionPolicy; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.BonusRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java index 7925ca93777b..9ac0f7b8f685 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java @@ -27,9 +27,9 @@ import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java index 22bb3b15927f..7c5d00530307 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java @@ -36,9 +36,9 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.FeedbackRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseFactory.java index fd08b5133116..1d02e8449af7 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseFactory.java @@ -7,11 +7,13 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.FileUploadExercise; import de.tum.in.www1.artemis.domain.GradingCriterion; import de.tum.in.www1.artemis.domain.GradingInstruction; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; +import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; /** @@ -44,6 +46,9 @@ public static Exercise populateExercise(Exercise exercise, ZonedDateTime release exercise.setPresentationScoreEnabled(course.getPresentationScore() != 0); exercise.setCourse(course); exercise.setExerciseGroup(null); + if (!(exercise instanceof QuizExercise) && !(exercise instanceof FileUploadExercise)) { + exercise.setPlagiarismDetectionConfig(new PlagiarismDetectionConfig()); + } return exercise; } @@ -69,6 +74,9 @@ public static Exercise populateExerciseForExam(Exercise exercise, ExerciseGroup exercise.getCategories().add("Category"); exercise.setExerciseGroup(exerciseGroup); exercise.setCourse(null); + if (!(exercise instanceof QuizExercise) && !(exercise instanceof FileUploadExercise)) { + exercise.setPlagiarismDetectionConfig(new PlagiarismDetectionConfig()); + } if (!(exercise instanceof QuizExercise)) { exercise.setGradingInstructions("Grading instructions"); exercise.setGradingCriteria(Set.of(new GradingCriterion())); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java index da31d55d6351..37eaddcef96c 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseIntegrationTest.java @@ -46,9 +46,9 @@ import de.tum.in.www1.artemis.domain.quiz.QuizPointStatistic; import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExamRepository; @@ -165,7 +165,7 @@ void testGetStatsForExamExerciseAssessmentDashboard() throws Exception { Course course = examUtilService.createCourseWithExamAndExerciseGroupAndExercises(user); course = courseRepository.findByIdWithEagerExercisesElseThrow(course.getId()); var exam = examRepository.findByCourseId(course.getId()).get(0); - var textExercise = examRepository.findAllExercisesByExamId(exam.getId()).stream().filter(ex -> ex instanceof TextExercise).findFirst().orElseThrow(); + var textExercise = examRepository.findAllExercisesWithDetailsByExamId(exam.getId()).stream().filter(ex -> ex instanceof TextExercise).findFirst().orElseThrow(); StatsForDashboardDTO statsForDashboardDTO = request.get("/api/exercises/" + textExercise.getId() + "/stats-for-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class); assertThat(statsForDashboardDTO.getNumberOfSubmissions().inTime()).isZero(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java index 6c6f26b72d0a..de62774ce12f 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/ExerciseUtilService.java @@ -39,10 +39,10 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.post.ConversationFactory; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java index 5e3891f2480a..ce6d8ac1a3d5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.fileuploadexercise; +package de.tum.in.www1.artemis.exercise.fileupload; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.eq; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseFactory.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseFactory.java rename to src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseFactory.java index 7450294e718d..86fbb68ec808 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseFactory.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.fileuploadexercise; +package de.tum.in.www1.artemis.exercise.fileupload; import java.time.ZonedDateTime; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java index 2537662f201a..abe9ae454c59 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.fileuploadexercise; +package de.tum.in.www1.artemis.exercise.fileupload; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseUtilService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseUtilService.java index 64eddcb30fff..a4af853f97a4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseUtilService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.fileuploadexercise; +package de.tum.in.www1.artemis.exercise.fileupload; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadSubmissionIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadSubmissionIntegrationTest.java index e45c9c238250..861ca211a91b 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadSubmissionIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.fileuploadexercise; +package de.tum.in.www1.artemis.exercise.fileupload; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -40,7 +40,7 @@ import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.FileUploadSubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonConversionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonConversionIntegrationTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonConversionIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonConversionIntegrationTest.java index 426ddabfccbe..b1070c7893a3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonConversionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonConversionIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonDiagramResourceIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonDiagramResourceIntegrationTest.java index 1f707aa15233..6b0206001148 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ApollonDiagramResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ApollonDiagramResourceIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingAssessmentIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingAssessmentIntegrationTest.java index 92ecb66f6da4..97d367be9e91 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingAssessmentIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static de.tum.in.www1.artemis.util.TestResourceUtils.loadFileFromResources; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingComparisonTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingComparisonTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingComparisonTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingComparisonTest.java index 8ef123d6cbf6..25e191e37ae0 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingComparisonTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingComparisonTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseFactory.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseFactory.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseFactory.java index 45bd73bb2f15..4287343b27b9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseFactory.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import java.time.ZonedDateTime; import java.util.HashSet; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java index 2e0fc3b332c6..dfab03a33e9c 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; @@ -439,9 +439,8 @@ void importModelingExerciseFromCourseToCourse() throws Exception { var importedExercise = request.postWithResponseBody("/api/modeling-exercises/import/" + modelingExerciseToImport.getId(), modelingExerciseToImport, ModelingExercise.class, HttpStatus.CREATED); - assertThat(importedExercise).usingRecursiveComparison() - .ignoringFields("id", "course", "shortName", "releaseDate", "dueDate", "assessmentDueDate", "exampleSolutionPublicationDate", "channelNameTransient") - .isEqualTo(modelingExerciseToImport); + assertThat(importedExercise).usingRecursiveComparison().ignoringFields("id", "course", "shortName", "releaseDate", "dueDate", "assessmentDueDate", + "exampleSolutionPublicationDate", "channelNameTransient", "plagiarismDetectionConfig.id").isEqualTo(modelingExerciseToImport); Channel channelFromDB = channelRepository.findChannelByExerciseId(importedExercise.getId()); assertThat(channelFromDB).isNotNull(); assertThat(channelFromDB.getName()).isEqualTo(uniqueChannelName); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseUtilService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseUtilService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseUtilService.java index 0f2e44339237..31db8b325d89 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseUtilService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -219,7 +219,7 @@ public Course addCourseWithDifferentModelingExercises() { exerciseRepo.save(syntaxTreeExercise); exerciseRepo.save(flowchartExercise); exerciseRepo.save(finishedExercise); - Course storedCourse = courseRepo.findByIdWithExercisesAndLecturesElseThrow(course.getId()); + Course storedCourse = courseRepo.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(course.getId()); Set<Exercise> exercises = storedCourse.getExercises(); assertThat(exercises).as("eleven exercises got stored").hasSize(11); assertThat(exercises).as("Contains all exercises").containsExactlyInAnyOrder(course.getExercises().toArray(new Exercise[] {})); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingSubmissionIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingSubmissionIntegrationTest.java index 9fcc40622e9e..30eda851c49a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingSubmissionIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise; +package de.tum.in.www1.artemis.exercise.modeling; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -42,7 +42,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingSubmissionElement; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/FeedbackSelectorTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/FeedbackSelectorTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/FeedbackSelectorTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/FeedbackSelectorTest.java index 2a61905ef1cf..22c5437b285a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/FeedbackSelectorTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/FeedbackSelectorTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.controller; +package de.tum.in.www1.artemis.exercise.modeling.compass.controller; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/ModelClusterFactoryTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/ModelClusterFactoryTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/ModelClusterFactoryTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/ModelClusterFactoryTest.java index 7010ed0f0b97..26b47e3f81ed 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/ModelClusterFactoryTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/ModelClusterFactoryTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.controller; +package de.tum.in.www1.artemis.exercise.modeling.compass.controller; import static de.tum.in.www1.artemis.util.TestResourceUtils.loadFileFromResources; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/UMLModelParserTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/UMLModelParserTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/UMLModelParserTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/UMLModelParserTest.java index 926aecffc7da..4ff6a35c8b88 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/controller/UMLModelParserTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/controller/UMLModelParserTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.controller; +package de.tum.in.www1.artemis.exercise.modeling.compass.controller; import static com.google.gson.JsonParser.parseString; import static de.tum.in.www1.artemis.service.compass.umlmodel.activity.UMLActivityNode.UMLActivityNodeType.ACTIVITY_ACTION_NODE; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/AbstractUMLDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/AbstractUMLDiagramTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/AbstractUMLDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/AbstractUMLDiagramTest.java index 9f69214443e7..4ce80ae2151f 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/AbstractUMLDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/AbstractUMLDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/UMLDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/UMLDiagramTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/UMLDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/UMLDiagramTest.java index 42783f43df2b..6ee07d0ecd79 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/UMLDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/UMLDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagramTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagramTest.java index 3113167d02af..cbb4680c5207 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.activity; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.activity; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -14,7 +14,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLElement; import de.tum.in.www1.artemis.service.compass.umlmodel.activity.UMLActivity; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagrams.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagrams.java index 9196ce58e8d3..5ed95df0dc74 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.activity; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.activity; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityNodeTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityNodeTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityNodeTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityNodeTest.java index 7aed2e73d1e7..608875631034 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityNodeTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityNodeTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.activity; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.activity; import static de.tum.in.www1.artemis.service.compass.umlmodel.activity.UMLActivityNode.UMLActivityNodeType.ACTIVITY_ACTION_NODE; import static de.tum.in.www1.artemis.service.compass.umlmodel.activity.UMLActivityNode.UMLActivityNodeType.ACTIVITY_FINAL_NODE; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityTest.java similarity index 96% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityTest.java index fdb11acd182c..b96a8310161a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLActivityTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLActivityTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.activity; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.activity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLControlFlowTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLControlFlowTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLControlFlowTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLControlFlowTest.java index 508077fb6913..c9e31636307d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/UMLControlFlowTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/UMLControlFlowTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.activity; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.activity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagramTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagramTest.java index 1d76e38bed7b..c71ac89973ee 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.bpmn; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.bpmn; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLElement; import de.tum.in.www1.artemis.service.compass.umlmodel.bpmn.BPMNAnnotation; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagrams.java similarity index 91% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagrams.java index de4be599edf0..a5f73b5552e4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/BPMNDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/BPMNDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.bpmn; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.bpmn; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLAttributeTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLAttributeTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLAttributeTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLAttributeTest.java index 2e64d140a28b..e41606e5f4c3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLAttributeTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLAttributeTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagramTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagramTest.java index a2316f415d5c..e10d6ab969bc 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -12,7 +12,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLElement; import de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLAttribute; import de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLClass; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagrams.java similarity index 92% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagrams.java index 486ceec332c2..852012449fad 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassTest.java index 9c2c57bb4bb6..49436cdf88c0 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLClassTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLClassTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLClass.UMLClassType.ABSTRACT_CLASS; import static de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLClass.UMLClassType.CLASS; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLMethodTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLMethodTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLMethodTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLMethodTest.java index d710d7b2862e..61e8d33f3d7e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLMethodTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLMethodTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLPackageTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLPackageTest.java similarity index 95% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLPackageTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLPackageTest.java index d0dfb12070d5..46f2996b460d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLPackageTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLPackageTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLRelationshipTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLRelationshipTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLRelationshipTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLRelationshipTest.java index 32fabbb7a027..c5fed56fb2f9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/UMLRelationshipTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/UMLRelationshipTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.classdiagram; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.classdiagram; import static de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLRelationship.UMLRelationshipType.CLASS_BIDIRECTIONAL; import static de.tum.in.www1.artemis.service.compass.umlmodel.classdiagram.UMLRelationship.UMLRelationshipType.CLASS_UNIDIRECTIONAL; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagramTest.java similarity index 95% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagramTest.java index 0e4e21d74999..a5b0f97bd9c9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.communication; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.communication; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.communication.UMLCommunicationDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.communication.UMLCommunicationLink; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagrams.java similarity index 93% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagrams.java index 23533bab92c2..b4dbd80204fc 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/UMLCommunicationDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/UMLCommunicationDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.communication; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.communication; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagramTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagramTest.java index 7b43c7d178fc..647cf9570398 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.component; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.component; import static com.google.gson.JsonParser.parseString; import static de.tum.in.www1.artemis.service.compass.umlmodel.component.UMLComponentRelationship.UMLComponentRelationshipType.COMPONENT_DEPENDENCY; @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.component.UMLComponent; import de.tum.in.www1.artemis.service.compass.umlmodel.component.UMLComponentDiagram; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagrams.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagrams.java index bdb23f2ef1a2..8e29fd58e0bb 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/UMLComponentDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/UMLComponentDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.component; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.component; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java similarity index 96% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java index b329b8bc0d38..c91ef60a667a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.deployment; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.deployment; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.component.UMLComponent; import de.tum.in.www1.artemis.service.compass.umlmodel.deployment.UMLArtifact; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagrams.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagrams.java index 7c277ffaca37..7b4c8c66e1e6 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/UMLDeploymentDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/UMLDeploymentDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.deployment; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.deployment; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartTest.java similarity index 88% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartTest.java index 3ed027e95049..b55fedd9c731 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartTest.java @@ -1,12 +1,12 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart; import static com.google.gson.JsonParser.parseString; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1A; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1A_V3; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1B; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1B_V3; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_2; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_2_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1A; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1A_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1B; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_1B_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_2; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart.FlowchartUtil.FLOWCHART_MODEL_2_V3; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.type; @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.flowchart.Flowchart; import de.tum.in.www1.artemis.service.compass.umlmodel.flowchart.FlowchartDecision; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartUtil.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartUtil.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartUtil.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartUtil.java index 2b0b06fd6046..5f35eb4d9f89 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/FlowchartUtil.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/FlowchartUtil.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.flowchart; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.flowchart; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagramTest.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagramTest.java index c7c4ea65c881..9d920a91ceb2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.object; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.object; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.object.UMLObject; import de.tum.in.www1.artemis.service.compass.umlmodel.object.UMLObjectDiagram; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagrams.java similarity index 92% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagrams.java index 3aa2a11a85df..1696bcadb062 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/UMLObjectDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/UMLObjectDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.object; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.object; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNetTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNetTest.java similarity index 84% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNetTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNetTest.java index 4a2e22060a3d..d0dc0f31e331 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNetTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNetTest.java @@ -1,12 +1,12 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet; import static com.google.gson.JsonParser.parseString; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1A; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1A_V3; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1B; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1B_V3; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_2; -import static de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_2_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1A; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1A_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1B; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_1B_V3; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_2; +import static de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet.PetriNets.PETRI_NET_MODEL_2_V3; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.type; @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.parsers.UMLModelParser; import de.tum.in.www1.artemis.service.compass.umlmodel.petrinet.PetriNet; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNets.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNets.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNets.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNets.java index ee2bb1b751e2..a762c34254d5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/PetriNets.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/PetriNets.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.petrinet; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.petrinet; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTreeTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTreeTest.java similarity index 96% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTreeTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTreeTest.java index 3021f5b9a33b..d793a551188a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTreeTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTreeTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.syntaxtree; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.syntaxtree; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.parsers.UMLModelParser; import de.tum.in.www1.artemis.service.compass.umlmodel.syntaxtree.SyntaxTree; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTrees.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTrees.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTrees.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTrees.java index 0b89431b1746..4709f41b12c3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/SyntaxTrees.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/SyntaxTrees.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.syntaxtree; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.syntaxtree; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java similarity index 95% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java index 250a60821766..76dec8987c89 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagramTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.usecase; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.usecase; import static com.google.gson.JsonParser.parseString; import static org.assertj.core.api.Assertions.assertThat; @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.AbstractUMLDiagramTest; +import de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.AbstractUMLDiagramTest; import de.tum.in.www1.artemis.service.compass.umlmodel.UMLDiagram; import de.tum.in.www1.artemis.service.compass.umlmodel.parsers.UMLModelParser; import de.tum.in.www1.artemis.service.compass.umlmodel.usecase.UMLActor; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagrams.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagrams.java similarity index 93% rename from src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagrams.java rename to src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagrams.java index 0e3a48cdec9b..f70f751859a2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/UMLUseCaseDiagrams.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/UMLUseCaseDiagrams.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.modelingexercise.compass.umlmodel.usecase; +package de.tum.in.www1.artemis.exercise.modeling.compass.umlmodel.usecase; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/AuxiliaryRepositoryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/AuxiliaryRepositoryServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/AuxiliaryRepositoryServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/AuxiliaryRepositoryServiceTest.java index b23c175bd59e..43b3d2f18790 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/AuxiliaryRepositoryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/AuxiliaryRepositoryServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ConsistencyCheckGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ConsistencyCheckGitlabJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java index 3b302ad9009b..6808cddc5fd0 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ConsistencyCheckGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ContinuousIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ContinuousIntegrationTestService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java index 53871af79e28..3a5e1c3f523a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ContinuousIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java index b4aa30893046..a659517a221d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/CourseGitlabJenkinsIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verifyNoInteractions; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/GitServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/GitServiceTest.java index 4136bb9d2dfe..ffd756415cc9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/GitServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitlabServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/GitlabServiceTest.java similarity index 94% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitlabServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/GitlabServiceTest.java index a622f2afe407..e228dfe3c973 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/GitlabServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/GitlabServiceTest.java @@ -1,8 +1,8 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST_WITHOUT_COMMIT; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST_WRONG_COMMIT_ORDER; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST_WITHOUT_COMMIT; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST_WRONG_COMMIT_ORDER; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/MockDelegate.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/MockDelegate.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/MockDelegate.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/MockDelegate.java index 913cd5950111..ab8e7e4591ee 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/MockDelegate.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/MockDelegate.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import java.io.IOException; import java.net.MalformedURLException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/PlantUmlIntegrationTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/PlantUmlIntegrationTest.java index c2cbec03a72f..9da875df4ba2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/PlantUmlIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/PlantUmlIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingAssessmentIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingAssessmentIntegrationTest.java index 8d32d987ad88..ee9da5c3d295 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingAssessmentIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doNothing; @@ -1026,7 +1026,7 @@ void overrideProgrammingAssessmentAfterComplaint() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void unlockFeedbackRequestAfterAssessment() throws Exception { - programmingExercise.setAllowManualFeedbackRequests(true); + programmingExercise.setAllowFeedbackRequests(true); programmingExercise.setDueDate(ZonedDateTime.now().plusDays(1)); exerciseRepository.save(programmingExercise); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBuildPlanTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseBuildPlanTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBuildPlanTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseBuildPlanTest.java index f5154ae7a5b5..51889258dc6a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseBuildPlanTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseBuildPlanTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseFactory.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseFactory.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseFactory.java index 5df2b34ba4ee..654b12ec1f69 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseFactory.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.exercise.ExerciseFactory.populateExerciseForExam; import static java.time.ZonedDateTime.now; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitIntegrationTest.java index a3b829a81a17..67f09657452b 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java similarity index 95% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java index 0ea9855ead0a..ea0dd500fed3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.C; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.EMPTY; @@ -7,8 +7,8 @@ import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.KOTLIN; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.PYTHON; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.SWIFT; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService.studentLogin; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService.studentLogin; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; @@ -578,6 +578,28 @@ void testExportExamSolutionRepository_shouldReturnFileOrForbidden() throws Excep verify(fileService, times(3)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testExportStudentRepository_asStudent_authorized() throws Exception { + programmingExerciseTestService.exportStudentRepository(true); + // Two invocations: one for the repository directory; one for the output. + verify(fileService, times(2)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student2", roles = "USER") + void testExportStudentRepository_asStudent_unauthorized() throws Exception { + // The repository does not belong to this student. + programmingExerciseTestService.exportStudentRepository(false); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testExportStudentRepository_asTutor() throws Exception { + programmingExerciseTestService.exportStudentRepository(true); + verify(fileService, times(2)).scheduleDirectoryPathForRecursiveDeletion(any(Path.class), eq(5L)); + } + // TODO: add startProgrammingExerciseStudentSubmissionFailedWithBuildlog & copyRepository_testConflictError @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java index b6a19f147fb2..d794579e870e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseGradingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseGradingServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.config.Constants.TEST_CASES_DUPLICATE_NOTIFICATION; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationJenkinsGitlabTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java index 9168cf1c4fd6..5dbbc2e11984 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.BuildPlanType.SOLUTION; import static de.tum.in.www1.artemis.domain.enumeration.BuildPlanType.TEMPLATE; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationTestService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationTestService.java index 63db8225ab3e..d48047487304 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseIntegrationTestService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.BuildPlanType.SOLUTION; import static de.tum.in.www1.artemis.domain.enumeration.BuildPlanType.TEMPLATE; @@ -90,7 +90,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.GradingCriterionUtil; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.plagiarism.PlagiarismUtilService; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index 12069798331e..d65634dfce6f 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.config.Constants.LOCALCI_RESULTS_DIRECTORY; import static de.tum.in.www1.artemis.config.Constants.LOCALCI_WORKING_DIRECTORY; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseParticipationIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseParticipationIntegrationTest.java index 80dee4e2ffdf..c286e8d89446 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseParticipationIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseRepositoryServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseRepositoryServiceTest.java index 3a02a5818170..964d72f52e23 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseRepositoryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseRepositoryServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java index 61cf01b0399c..191d585766a1 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java @@ -1,6 +1,6 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory.DEFAULT_BRANCH; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory.DEFAULT_BRANCH; import static org.mockito.Mockito.doReturn; import java.time.ZonedDateTime; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultTestService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultTestService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultTestService.java index a56e03e830c3..1f860df7e058 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseResultTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseResultTestService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.config.Constants.NEW_RESULT_TOPIC; import static java.util.Comparator.comparing; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseScheduleServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseScheduleServiceTest.java index abefa45f699b..e01b4ac78bb9 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseScheduleServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.after; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceIntegrationTest.java index fa22debbe3dd..bed40a6f56ea 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceTest.java index f24dc8ec5d00..0218f7a9f5a1 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTemplateIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTemplateIntegrationTest.java index f380fcf7b591..121488751dc2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTemplateIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTemplateIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.util.TestConstants.COMMIT_HASH_OBJECT_ID; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTest.java index d970ec2b1540..e1b19e1eea18 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestCaseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestCaseServiceTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestCaseServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestCaseServiceTest.java index df2cefb9a0d5..bc0aad594c33 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestCaseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestCaseServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestService.java index 98405a64992e..25b94421d20a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseTestService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.INDIVIDUAL; import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.TEAM; @@ -1544,6 +1544,25 @@ private String exportStudentRequestedRepository(HttpStatus expectedStatus, boole return request.get(url, expectedStatus, String.class); } + /** + * Attempts to export a student repository and verifies that the file is (or is not) returned. + * + * @param authorized Whether to expect that the user is authorized. + */ + void exportStudentRepository(boolean authorized) throws Exception { + HttpStatus expectedStatus = authorized ? HttpStatus.OK : HttpStatus.FORBIDDEN; + generateProgrammingExerciseForExport(); + var participation = createStudentParticipationWithSubmission(INDIVIDUAL); + var url = "/api/programming-exercises/" + exercise.getId() + "/export-student-repository/" + participation.getId(); + String zip = request.get(url, expectedStatus, String.class); + if (expectedStatus.is2xxSuccessful()) { + assertThat(zip).isNotNull(); + } + else { + assertThat(zip).isNull(); + } + } + // Test /** @@ -1838,7 +1857,7 @@ private void testExportCourseWithFaultyParticipationCannotGetOrCheckoutRepositor // Mock error when exporting a participation doThrow(exceptionToThrow).when(gitService).getOrCheckoutRepository(eq(participation.getVcsRepositoryUri()), any(Path.class), anyBoolean()); - course = courseRepository.findByIdWithExercisesAndLecturesElseThrow(course.getId()); + course = courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(course.getId()); List<String> errors = new ArrayList<>(); var optionalExportedCourse = courseExamExportService.exportCourse(course, courseArchivesDirPath, errors); assertThat(optionalExportedCourse).isPresent(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseUtilService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseUtilService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseUtilService.java index 211deddf1117..b2bd88d0f5ee 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseUtilService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -371,7 +371,7 @@ public Course addCourseWithOneProgrammingExercise(boolean enableStaticCodeAnalys var course = CourseFactory.generateCourse(null, pastTimestamp, futureFutureTimestamp, new HashSet<>(), "tumuser", "tutor", "editor", "instructor"); course = courseRepo.save(course); addProgrammingExerciseToCourse(course, enableStaticCodeAnalysis, enableTestwiseCoverageAnalysis, programmingLanguage, title, shortName, null); - return courseRepo.findByIdWithExercisesAndLecturesElseThrow(course.getId()); + return courseRepo.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(course.getId()); } /** @@ -484,7 +484,7 @@ public Course addCourseWithNamedProgrammingExercise(String programmingExerciseTi programmingExercise = addSolutionParticipationForProgrammingExercise(programmingExercise); addTemplateParticipationForProgrammingExercise(programmingExercise); - return courseRepo.findByIdWithExercisesAndLecturesElseThrow(course.getId()); + return courseRepo.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(course.getId()); } /** @@ -558,7 +558,7 @@ public Course addCourseWithOneProgrammingExerciseAndTestCases() { Course course = addCourseWithOneProgrammingExercise(); ProgrammingExercise programmingExercise = exerciseUtilService.findProgrammingExerciseWithTitle(course.getExercises(), "Programming"); addTestCasesToProgrammingExercise(programmingExercise); - return courseRepo.findByIdWithExercisesAndLecturesElseThrow(course.getId()); + return courseRepo.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(course.getId()); } /** diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java index 12e7031bc6b4..a89cd7f38615 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java @@ -1,7 +1,7 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.JAVA; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingSubmissionConstants.GITLAB_PUSH_EVENT_REQUEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultIntegrationTestService.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultIntegrationTestService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultIntegrationTestService.java index 61e8c01f438a..22ca6070899b 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionAndResultIntegrationTestService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.JAVA; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionConstants.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionConstants.java similarity index 95% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionConstants.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionConstants.java index 5283720c08b1..92ea645b30c2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionConstants.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionConstants.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionIntegrationTest.java index 21f58221d3c9..60a4e8ea9a2a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingSubmissionIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.config.Constants.NEW_SUBMISSION_TOPIC; import static de.tum.in.www1.artemis.config.Constants.SETUP_COMMIT_MESSAGE; @@ -53,7 +53,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryIntegrationTest.java index 7f963f7d2f46..7c7bcf3264d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.util.RequestUtilService.parameters; import static org.assertj.core.api.Assertions.assertThat; @@ -74,7 +74,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java similarity index 98% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java index f8a18543df44..d2433a8efba6 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/StaticCodeAnalysisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/StaticCodeAnalysisIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/StaticCodeAnalysisIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/StaticCodeAnalysisIntegrationTest.java index bba5b9214f0f..473989879cb8 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/StaticCodeAnalysisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/StaticCodeAnalysisIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/SubmissionPolicyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/SubmissionPolicyIntegrationTest.java similarity index 92% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/SubmissionPolicyIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/SubmissionPolicyIntegrationTest.java index b0e71e371207..2d5e7dfb2c28 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/SubmissionPolicyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/SubmissionPolicyIntegrationTest.java @@ -1,7 +1,7 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static de.tum.in.www1.artemis.domain.Feedback.SUBMISSION_POLICY_FEEDBACK_IDENTIFIER; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseResultTestService.convertBuildResultToJsonObject; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseResultTestService.convertBuildResultToJsonObject; import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; @@ -10,7 +10,6 @@ import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -35,6 +34,7 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.service.connectors.ci.notification.dto.CommitDTO; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseGradingService; import de.tum.in.www1.artemis.user.UserUtilService; @@ -401,8 +401,6 @@ private enum EnforcePolicyTestType { POLICY_NULL, POLICY_ACTIVE, POLICY_INACTIVE } - // TODO enable this test (Issue - https://github.com/ls1intum/Artemis/issues/8296) - @Disabled @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(EnforcePolicyTestType.class) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") @@ -433,8 +431,6 @@ void test_enforceLockRepositoryPolicyOnStudentParticipation(EnforcePolicyTestTyp } } - // TODO enable this test (Issue - https://github.com/ls1intum/Artemis/issues/8296) - @Disabled @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(EnforcePolicyTestType.class) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") @@ -452,10 +448,15 @@ void test_enforceSubmissionPenaltyPolicyOnStudentParticipation(EnforcePolicyTest if (type == EnforcePolicyTestType.POLICY_ACTIVE) { mockGitlabRequests(participation); } - final var resultRequestBody = convertBuildResultToJsonObject(resultNotification); + var resultRequestBody = convertBuildResultToJsonObject(resultNotification); var result = gradingService.processNewProgrammingExerciseResult(participation, resultRequestBody); assertThat(result).isNotNull(); assertThat(result.getScore()).isEqualTo(25); + + // resultNotification with changed commit hash + var updatedResultNotification = ProgrammingExerciseFactory.generateTestResultDTO(null, repositoryName, null, programmingExercise.getProgrammingLanguage(), false, + List.of("test1", "test2", "test3"), Collections.emptyList(), null, List.of(new CommitDTO("commit1", "slug", defaultBranch)), null); + resultRequestBody = convertBuildResultToJsonObject(updatedResultNotification); result = gradingService.processNewProgrammingExerciseResult(participation, resultRequestBody); assertThat(result).isNotNull(); if (type == EnforcePolicyTestType.POLICY_ACTIVE) { @@ -468,6 +469,25 @@ void test_enforceSubmissionPenaltyPolicyOnStudentParticipation(EnforcePolicyTest } } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void test_getSameScoreForSameCommitHash() { + ProgrammingExerciseStudentParticipation participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, + TEST_PREFIX + "student1"); + String repositoryName = programmingExercise.getProjectKey().toLowerCase() + "-" + TEST_PREFIX + "student1"; + var resultNotification1 = ProgrammingExerciseFactory.generateTestResultDTO(null, repositoryName, null, programmingExercise.getProgrammingLanguage(), false, + List.of("test1"), List.of("test2", "test3"), null, List.of(new CommitDTO("commit1", "slug", defaultBranch)), null); + var resultNotification2 = ProgrammingExerciseFactory.generateTestResultDTO(null, repositoryName, null, programmingExercise.getProgrammingLanguage(), false, + List.of("test1"), List.of("test2", "test3"), null, List.of(new CommitDTO("commit1", "slug", defaultBranch)), null); + var resultRequestBody = convertBuildResultToJsonObject(resultNotification1); + var result1 = gradingService.processNewProgrammingExerciseResult(participation, resultRequestBody); + resultRequestBody = convertBuildResultToJsonObject(resultNotification2); + var result2 = gradingService.processNewProgrammingExerciseResult(participation, resultRequestBody); + assertThat(result1).isNotNull(); + assertThat(result2).isNotNull(); + assertThat(result1.getScore()).isEqualTo(result2.getScore()); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void test_getParticipationSubmissionCount() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/TestRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/TestRepositoryResourceIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/TestRepositoryResourceIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/programming/TestRepositoryResourceIntegrationTest.java index 853091234df1..fff900e6715c 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/TestRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/TestRepositoryResourceIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.programmingexercise; +package de.tum.in.www1.artemis.exercise.programming; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizComparisonTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizComparisonTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizComparisonTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizComparisonTest.java index 3b8480fe5943..e603a9153aff 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizComparisonTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizComparisonTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.quizexercise; +package de.tum.in.www1.artemis.exercise.quiz; import static de.tum.in.www1.artemis.service.exam.StudentExamService.isContentEqualTo; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseFactory.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java rename to src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseFactory.java index 553473aaf471..2ec53833b945 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseFactory.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.quizexercise; +package de.tum.in.www1.artemis.exercise.quiz; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseIntegrationTest.java index 44d33d96d1a1..89d218ca7595 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.quizexercise; +package de.tum.in.www1.artemis.exercise.quiz; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.byLessThan; @@ -81,7 +81,7 @@ import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.service.ExerciseService; -import de.tum.in.www1.artemis.service.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.ExerciseIntegrationTestService; import de.tum.in.www1.artemis.util.PageableSearchUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseUtilService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseUtilService.java index 506d7179259e..f2972113abb5 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizExerciseUtilService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.quizexercise; +package de.tum.in.www1.artemis.exercise.quiz; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizSubmissionIntegrationTest.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizSubmissionIntegrationTest.java index c89ecd8b6e4b..709b4305a1c2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/quizexercise/QuizSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/quiz/QuizSubmissionIntegrationTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.quizexercise; +package de.tum.in.www1.artemis.exercise.quiz; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -69,9 +69,9 @@ import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; -import de.tum.in.www1.artemis.service.QuizBatchService; -import de.tum.in.www1.artemis.service.QuizExerciseService; -import de.tum.in.www1.artemis.service.QuizStatisticService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizStatisticService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextComparisonTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextComparisonTest.java similarity index 97% rename from src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextComparisonTest.java rename to src/test/java/de/tum/in/www1/artemis/exercise/text/TextComparisonTest.java index 24d904ee8842..c7f3894c69f3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextComparisonTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextComparisonTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.textexercise; +package de.tum.in.www1.artemis.exercise.text; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseFactory.java b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseFactory.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseFactory.java rename to src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseFactory.java index 825ec65c3699..564eaa018674 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseFactory.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.textexercise; +package de.tum.in.www1.artemis.exercise.text; import java.time.ZonedDateTime; import java.util.ArrayList; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseUtilService.java similarity index 99% rename from src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java rename to src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseUtilService.java index d11131824043..fa4a63ff1269 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/textexercise/TextExerciseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/text/TextExerciseUtilService.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.exercise.textexercise; +package de.tum.in.www1.artemis.exercise.text; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java index 0f3822466f44..0d137a9452ca 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintIntegrationTest.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.domain.hestia.CodeHint; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java index 671c15231456..59ff30f35948 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/CodeHintServiceTest.java @@ -25,7 +25,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTestCaseType; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java index 17e35253f47c..f24de0ca7044 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintIntegrationTest.java @@ -30,7 +30,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java index e78f312b0baa..9115fa968e0e 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ExerciseHintServiceTest.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java index 62311d6bda57..3c3a049aa759 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/HestiaDatabaseTest.java @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java index 91077093d50d..81097a639095 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java @@ -13,7 +13,7 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffReport; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.localvcci.AbstractLocalCILocalVCIntegrationTest; import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseGitDiffReportService; import de.tum.in.www1.artemis.user.UserUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java index d4dea97da35c..927d060c7feb 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseGitDiffReportServiceTest.java @@ -17,7 +17,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseGitDiffReport; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.localvcci.AbstractLocalCILocalVCIntegrationTest; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseGitDiffReportRepository; import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseGitDiffReportService; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java index b8a2864e6d76..fb8c43e162b7 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java @@ -19,7 +19,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java index 8bcf1915a5f6..7cd595254f45 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskIntegrationTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java index 12dd76f4305f..8fed8584a4d3 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/ProgrammingExerciseTaskServiceTest.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.domain.hestia.CodeHint; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java index 3e329138451e..f807304864f3 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/StructuralTestCaseServiceTest.java @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTestCaseType; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.localvcci.AbstractLocalCILocalVCIntegrationTest; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.service.hestia.structural.StructuralSolutionEntryGenerationException; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java index c21034d3322e..e410652e0fcc 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageIntegrationTest.java @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.hestia.CoverageReport; import de.tum.in.www1.artemis.domain.hestia.TestwiseCoverageReportEntry; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionTestRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java index 6ec177a9b5cd..9a2ecb5e9b9c 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/TestwiseCoverageReportServiceTest.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.hestia.TestwiseCoverageReportEntry; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.localvcci.AbstractLocalCILocalVCIntegrationTest; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java index c6a50db26d3f..ff0521bbd561 100644 --- a/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/hestia/behavioral/BehavioralTestCaseServiceTest.java @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTestCaseType; import de.tum.in.www1.artemis.domain.hestia.TestwiseCoverageReportEntry; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.localvcci.AbstractLocalCILocalVCIntegrationTest; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java index 0f88481652a1..c96140daaf1c 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java @@ -6,6 +6,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.Set; +import java.util.TreeSet; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.mockito.ArgumentMatcher; @@ -21,7 +24,7 @@ import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettings; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; @@ -87,6 +90,7 @@ protected void activateIrisGlobally() { private void activateSubSettings(IrisSubSettings settings) { settings.setEnabled(true); settings.setPreferredModel(null); + settings.setAllowedModels(new TreeSet<>(Set.of("dummy"))); } protected void activateIrisFor(Course course) { diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisChatMessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisChatMessageIntegrationTest.java index 25564d7f5c13..268541ef4e54 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisChatMessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisChatMessageIntegrationTest.java @@ -1,23 +1,31 @@ package de.tum.in.www1.artemis.iris; +import static de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO.DONE; +import static de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO.IN_PROGRESS; +import static de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO.NOT_STARTED; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; @@ -29,9 +37,12 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.status.PyrisStageStateDTO; +import de.tum.in.www1.artemis.service.connectors.pyris.dto.tutorChat.PyrisTutorChatStatusUpdateDTO; import de.tum.in.www1.artemis.service.iris.IrisMessageService; import de.tum.in.www1.artemis.service.iris.session.IrisChatSessionService; -import de.tum.in.www1.artemis.service.iris.websocket.IrisChatWebsocketService; +import de.tum.in.www1.artemis.service.iris.websocket.IrisWebsocketDTO; import de.tum.in.www1.artemis.util.IrisUtilTestService; import de.tum.in.www1.artemis.util.LocalRepository; @@ -61,6 +72,8 @@ class IrisChatMessageIntegrationTest extends AbstractIrisIntegrationTest { private LocalRepository repository; + private AtomicBoolean pipelineDone; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 0, 0, 0); @@ -71,6 +84,7 @@ void initTestCase() { activateIrisFor(course); activateIrisFor(exercise); repository = new LocalRepository("main"); + pipelineDone = new AtomicBoolean(false); } @Test @@ -80,19 +94,22 @@ void sendOneMessage() throws Exception { var messageToSend = createDefaultMockMessage(irisSession); messageToSend.setMessageDifferentiator(1453); - irisRequestMockProvider.mockMessageV2Response(Map.of("response", "Hello World")); setupExercise(); - var irisMessage = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); - assertThat(irisMessage.getSender()).isEqualTo(IrisMessageSender.USER); - assertThat(irisMessage.getHelpful()).isNull(); - assertThat(irisMessage.getMessageDifferentiator()).isEqualTo(1453); - assertThat(irisMessage.getContent()).hasSize(3); - assertThat(irisMessage.getContent().stream().map(IrisMessageContent::getContentAsString).toList()) - .isEqualTo(messageToSend.getContent().stream().map(IrisMessageContent::getContentAsString).toList()); - await().untilAsserted(() -> assertThat(irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages()).hasSize(2).contains(irisMessage)); + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", dto.initialStages())); + + pipelineDone.set(true); + }); - verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend.getContent()), messageDTO("Hello World")); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, HttpStatus.CREATED); + + await().until(pipelineDone::get); + + verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend.getContent()), statusDTO(IN_PROGRESS, NOT_STARTED), statusDTO(DONE, IN_PROGRESS), + messageDTO("Hello World")); } @Test @@ -101,7 +118,7 @@ void sendOneMessageToWrongSession() throws Exception { irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student2")); IrisMessage messageToSend = createDefaultMockMessage(irisSession); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.FORBIDDEN); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, HttpStatus.FORBIDDEN); } @Test @@ -109,7 +126,7 @@ void sendOneMessageToWrongSession() throws Exception { void sendMessageWithoutContent() throws Exception { var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); var messageToSend = irisSession.newMessage(); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.BAD_REQUEST); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, HttpStatus.BAD_REQUEST); } @Test @@ -120,28 +137,31 @@ void sendTwoMessages() throws Exception { setupExercise(); - var irisMessage1 = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); - assertThat(irisMessage1.getSender()).isEqualTo(IrisMessageSender.USER); - assertThat(irisMessage1.getHelpful()).isNull(); - assertThat(irisMessage1.getContent()).hasSize(3); - // Compare contents of messages by only comparing the string content - assertThat(irisMessage1.getContent().stream().map(IrisMessageContent::getContentAsString).toList()) - .isEqualTo(messageToSend1.getContent().stream().map(IrisMessageContent::getContentAsString).toList()); - var irisSessionFromDb = irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()); - assertThat(irisSessionFromDb.getMessages()).hasSize(1).isEqualTo(List.of(irisMessage1)); + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World 1", dto.initialStages())); + + pipelineDone.set(true); + }); + + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World 2", dto.initialStages())); + + pipelineDone.set(true); + }); + + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, HttpStatus.CREATED); IrisMessage messageToSend2 = createDefaultMockMessage(irisSession); - var irisMessage2 = request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend2, IrisMessage.class, HttpStatus.CREATED); - assertThat(irisMessage2.getSender()).isEqualTo(IrisMessageSender.USER); - assertThat(irisMessage2.getHelpful()).isNull(); - assertThat(irisMessage2.getContent()).hasSize(3); - // Compare contents of messages by only comparing the string content - assertThat(irisMessage2.getContent().stream().map(IrisMessageContent::getContentAsString).toList()) - .isEqualTo(messageToSend2.getContent().stream().map(IrisMessageContent::getContentAsString).toList()); - irisSessionFromDb = irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()); - assertThat(irisSessionFromDb.getMessages()).hasSize(2).isEqualTo(List.of(irisMessage1, irisMessage2)); - - verify(websocketMessagingService, timeout(3000).times(4)).sendMessageToUser(eq(TEST_PREFIX + "student1"), any(), any()); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend2, HttpStatus.CREATED); + + verify(websocketMessagingService, times(8)).sendMessageToUser(eq(TEST_PREFIX + "student1"), eq("/topic/iris/sessions/" + irisSession.getId()), any()); + + var irisSessionFromDb = irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()); + assertThat(irisSessionFromDb.getMessages()).hasSize(4); } @Test @@ -219,45 +239,24 @@ void rateMessageWrongSession() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void sendOneMessageBadRequest() throws Exception { - var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - IrisMessage messageToSend = createDefaultMockMessage(irisSession); - - irisRequestMockProvider.mockMessageV1Error(500); - setupExercise(); - - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); - - verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend.getContent()), errorDTO()); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void sendOneMessageEmptyBody() throws Exception { + void resendMessage() throws Exception { var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - IrisMessage messageToSend = createDefaultMockMessage(irisSession); + var messageToSend = createDefaultMockMessage(irisSession); - irisRequestMockProvider.mockEmptyResponse(); setupExercise(); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, IrisMessage.class, HttpStatus.CREATED); - - verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend.getContent()), errorDTO()); - } + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void resendMessage() throws Exception { - var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - var messageToSend = createDefaultMockMessage(irisSession); + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", dto.initialStages())); - irisRequestMockProvider.mockMessageV2Response(Map.of("response", "Hello World")); - setupExercise(); + pipelineDone.set(true); + }); var irisMessage = irisMessageService.saveMessage(messageToSend, irisSession, IrisMessageSender.USER); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, IrisMessage.class, HttpStatus.OK); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, HttpStatus.OK); await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); - verifyWebsocketActivityWasExactly(irisSession, messageDTO("Hello World")); + verifyWebsocketActivityWasExactly(irisSession, statusDTO(IN_PROGRESS, NOT_STARTED), statusDTO(DONE, IN_PROGRESS), messageDTO("Hello World")); } // User needs to be Admin to change settings @@ -268,31 +267,43 @@ void sendMessageRateLimitReached() throws Exception { var messageToSend1 = createDefaultMockMessage(irisSession); var messageToSend2 = createDefaultMockMessage(irisSession); - irisRequestMockProvider.mockMessageV2Response(Map.of("response", "Hello World")); setupExercise(); + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", dto.initialStages())); + + pipelineDone.set(true); + }); + var globalSettings = irisSettingsService.getGlobalSettings(); globalSettings.getIrisChatSettings().setRateLimit(1); globalSettings.getIrisChatSettings().setRateLimitTimeframeHours(10); irisSettingsService.saveIrisSettings(globalSettings); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); - await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend2, IrisMessage.class, HttpStatus.TOO_MANY_REQUESTS); - var irisMessage = irisMessageService.saveMessage(messageToSend2, irisSession, IrisMessageSender.USER); - request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, IrisMessage.class, - HttpStatus.TOO_MANY_REQUESTS); - - verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend1.getContent()), messageDTO("Hello World")); - - // Reset to not interfere with other tests - globalSettings.getIrisChatSettings().setRateLimit(null); - globalSettings.getIrisChatSettings().setRateLimitTimeframeHours(null); - irisSettingsService.saveIrisSettings(globalSettings); + try { + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, HttpStatus.CREATED); + await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend2, HttpStatus.TOO_MANY_REQUESTS); + var irisMessage = irisMessageService.saveMessage(messageToSend2, irisSession, IrisMessageSender.USER); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages/" + irisMessage.getId() + "/resend", null, HttpStatus.TOO_MANY_REQUESTS); + + verifyWebsocketActivityWasExactly(irisSession, messageDTO(messageToSend1.getContent()), statusDTO(IN_PROGRESS, NOT_STARTED), statusDTO(DONE, IN_PROGRESS), + messageDTO("Hello World")); + } + finally { + // Reset to not interfere with other tests + globalSettings.getIrisChatSettings().setRateLimit(null); + globalSettings.getIrisChatSettings().setRateLimitTimeframeHours(null); + irisSettingsService.saveIrisSettings(globalSettings); + } } private void setupExercise() throws Exception { var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); + savedExercise = irisUtilTestService.setupSolution(savedExercise, repository); + savedExercise = irisUtilTestService.setupTest(savedExercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); } @@ -324,13 +335,13 @@ private ArgumentMatcher<Object> messageDTO(List<IrisMessageContent> content) { @Override public boolean matches(Object argument) { - if (!(argument instanceof IrisChatWebsocketService.IrisWebsocketDTO websocketDTO)) { + if (!(argument instanceof IrisWebsocketDTO websocketDTO)) { return false; } - if (websocketDTO.getType() != IrisChatWebsocketService.IrisWebsocketDTO.IrisWebsocketMessageType.MESSAGE) { + if (websocketDTO.type() != IrisWebsocketDTO.IrisWebsocketMessageType.MESSAGE) { return false; } - return Objects.equals(websocketDTO.getMessage().getContent().stream().map(IrisMessageContent::getContentAsString).toList(), + return Objects.equals(websocketDTO.message().getContent().stream().map(IrisMessageContent::getContentAsString).toList(), content.stream().map(IrisMessageContent::getContentAsString).toList()); } @@ -341,15 +352,42 @@ public String toString() { }; } + private ArgumentMatcher<Object> statusDTO(PyrisStageStateDTO... stageStates) { + return new ArgumentMatcher<>() { + + @Override + public boolean matches(Object argument) { + if (!(argument instanceof IrisWebsocketDTO websocketDTO)) { + return false; + } + if (websocketDTO.type() != IrisWebsocketDTO.IrisWebsocketMessageType.STATUS) { + return false; + } + if (websocketDTO.stages() == null) { + return stageStates == null; + } + if (websocketDTO.stages().size() != stageStates.length) { + return false; + } + return websocketDTO.stages().stream().map(PyrisStageDTO::state).toList().equals(List.of(stageStates)); + } + + @Override + public String toString() { + return "IrisChatWebsocketService.IrisWebsocketDTO with type STATUS and stage states " + Arrays.toString(stageStates); + } + }; + } + private ArgumentMatcher<Object> errorDTO() { return new ArgumentMatcher<>() { @Override public boolean matches(Object argument) { - if (!(argument instanceof IrisChatWebsocketService.IrisWebsocketDTO websocketDTO)) { + if (!(argument instanceof IrisWebsocketDTO websocketDTO)) { return false; } - return websocketDTO.getType() == IrisChatWebsocketService.IrisWebsocketDTO.IrisWebsocketMessageType.ERROR; + return websocketDTO.type() == IrisWebsocketDTO.IrisWebsocketMessageType.ERROR; } @Override @@ -359,4 +397,9 @@ public String toString() { }; } + private void sendStatus(String jobId, String result, List<PyrisStageDTO> stages) throws Exception { + var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of("Authorization", List.of("Bearer " + jobId)))); + request.postWithoutResponseBody("/api/public/pyris/pipelines/tutor-chat/runs/" + jobId + "/status", new PyrisTutorChatStatusUpdateDTO(result, stages), HttpStatus.OK, + headers); + } } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisChatWebsocketTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisChatWebsocketTest.java index 1e372a2baebc..673ae3dbd3a6 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisChatWebsocketTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisChatWebsocketTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.BeforeEach; @@ -16,8 +17,10 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.iris.message.IrisTextMessageContent; import de.tum.in.www1.artemis.service.WebsocketMessagingService; +import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.session.IrisChatSessionService; import de.tum.in.www1.artemis.service.iris.websocket.IrisChatWebsocketService; +import de.tum.in.www1.artemis.service.iris.websocket.IrisWebsocketDTO; @ActiveProfiles("iris") class IrisChatWebsocketTest extends AbstractIrisIntegrationTest { @@ -50,9 +53,9 @@ void sendMessage() { var message = irisSession.newMessage(); message.addContent(createMockContent(), createMockContent()); message.setMessageDifferentiator(101010); - irisChatWebsocketService.sendMessage(message); + irisChatWebsocketService.sendMessage(message, List.of()); verify(websocketMessagingService, times(1)).sendMessageToUser(eq(TEST_PREFIX + "student1"), eq("/topic/iris/sessions/" + irisSession.getId()), - eq(new IrisChatWebsocketService.IrisWebsocketDTO(message, null))); + eq(new IrisWebsocketDTO(message, null, new IrisRateLimitService.IrisRateLimitInformation(0, -1, 0), List.of()))); } private IrisTextMessageContent createMockContent() { diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisCompetencyGenerationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisCompetencyGenerationIntegrationTest.java index 1c3df98fff9e..f2704bad0a39 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisCompetencyGenerationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisCompetencyGenerationIntegrationTest.java @@ -4,8 +4,10 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -20,11 +22,13 @@ class IrisCompetencyGenerationIntegrationTest extends AbstractIrisIntegrationTes private static final String TEST_PREFIX = "iriscompetencyintegration"; - private Course course; - @Autowired private CourseUtilService courseUtilService; + private Course course; + + private AtomicBoolean pipelineDone; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); @@ -32,9 +36,11 @@ void initTestCase() { course = courseUtilService.createCourse(); activateIrisGlobally(); activateIrisFor(course); + pipelineDone = new AtomicBoolean(false); } @Test + @Disabled // TODO: Enable this test again! @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void generateCompetencies_asEditor_shouldSucceed() throws Exception { final String courseDescription = "Any description"; @@ -48,7 +54,11 @@ void generateCompetencies_asEditor_shouldSucceed() throws Exception { var competencyMap3 = Map.of("malformed", "any content"); var responseMap = Map.of("competencies", List.of(competencyMap1, competencyMap2, competencyMap3)); - irisRequestMockProvider.mockMessageV2Response(responseMap); + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + pipelineDone.set(true); + }); List<Competency> competencies = request.postListWithResponseBody("/api/courses/" + course.getId() + "/competencies/generate-from-description", courseDescription, Competency.class, HttpStatus.OK); diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisConnectorServiceTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisConnectorServiceTest.java deleted file mode 100644 index 57de7c31c34d..000000000000 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisConnectorServiceTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.tum.in.www1.artemis.iris; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Collections; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; - -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorException; -import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; -import de.tum.in.www1.artemis.service.iris.exception.IrisForbiddenException; -import de.tum.in.www1.artemis.service.iris.exception.IrisInternalPyrisErrorException; -import de.tum.in.www1.artemis.service.iris.exception.IrisInvalidTemplateException; -import de.tum.in.www1.artemis.service.iris.exception.IrisModelNotAvailableException; - -class IrisConnectorServiceTest extends AbstractIrisIntegrationTest { - - @Autowired - private IrisConnectorService irisConnectorService; - - private static Stream<Arguments> irisExceptions() { - // @formatter:off - return Stream.of( - Arguments.of(400, IrisInvalidTemplateException.class), - Arguments.of(401, IrisForbiddenException.class), - Arguments.of(403, IrisForbiddenException.class), - Arguments.of(404, IrisModelNotAvailableException.class), - Arguments.of(500, IrisInternalPyrisErrorException.class), - Arguments.of(418, IrisInternalPyrisErrorException.class) // Test default case - ); - // @formatter:on - } - - @ParameterizedTest - @MethodSource("irisExceptions") - void testExceptionV2(int httpStatus, Class<?> exceptionClass) throws Exception { - irisRequestMockProvider.mockMessageV2Error(httpStatus); - - irisConnectorService.sendRequestV2("Dummy", "TEST_MODEL", Collections.emptyMap()).handle((response, throwable) -> { - assertThat(throwable.getCause()).isNotNull().isInstanceOf(exceptionClass); - return null; - }).get(); - } - - @Test - void testOfferedModels() throws Exception { - irisRequestMockProvider.mockModelsResponse(); - - var offeredModels = irisConnectorService.getOfferedModels(); - assertThat(offeredModels).hasSize(1); - assertThat(offeredModels.get(0).id()).isEqualTo("TEST_MODEL"); - } - - @Test - void testOfferedModelsError() { - irisRequestMockProvider.mockModelsError(); - - assertThatThrownBy(() -> irisConnectorService.getOfferedModels()).isInstanceOf(IrisConnectorException.class); - } -} diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java index 78025a098717..13043d7df758 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java @@ -2,9 +2,10 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -26,6 +27,8 @@ class IrisHestiaIntegrationTest extends AbstractIrisIntegrationTest { private CodeHint codeHint; + private AtomicBoolean pipelineDone; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 1); @@ -35,14 +38,20 @@ void initTestCase() { activateIrisGlobally(); activateIrisFor(course); activateIrisFor(exercise); + pipelineDone = new AtomicBoolean(false); } @Test + @Disabled // TODO: Enable this test again! @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateSolutionEntriesOnSaving() throws Exception { addCodeHints(); - irisRequestMockProvider.mockMessageV2Response(Map.of("shortDescription", "Hello World Description", "longDescription", "Hello World Content")); + irisRequestMockProvider.mockRunResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + + pipelineDone.set(true); + }); var updatedCodeHint = request.postWithResponseBody("/api/programming-exercises/" + exercise.getId() + "/code-hints/" + codeHint.getId() + "/generate-description", null, CodeHint.class, HttpStatus.OK); diff --git a/src/test/java/de/tum/in/www1/artemis/iris/PyrisConnectorServiceTest.java b/src/test/java/de/tum/in/www1/artemis/iris/PyrisConnectorServiceTest.java new file mode 100644 index 000000000000..fbda9985cbbb --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/iris/PyrisConnectorServiceTest.java @@ -0,0 +1,99 @@ +package de.tum.in.www1.artemis.iris; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.participation.ParticipationUtilService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorException; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisConnectorService; +import de.tum.in.www1.artemis.service.connectors.pyris.PyrisPipelineService; +import de.tum.in.www1.artemis.service.iris.exception.IrisForbiddenException; +import de.tum.in.www1.artemis.service.iris.exception.IrisInternalPyrisErrorException; +import de.tum.in.www1.artemis.service.iris.session.IrisChatSessionService; +import de.tum.in.www1.artemis.util.IrisUtilTestService; +import de.tum.in.www1.artemis.util.LocalRepository; + +class PyrisConnectorServiceTest extends AbstractIrisIntegrationTest { + + private static final String TEST_PREFIX = "pyrisconnectorservice"; + + @Autowired + private PyrisConnectorService pyrisConnectorService; + + @Autowired + private PyrisPipelineService pyrisPipelineService; + + @Autowired + private IrisUtilTestService irisUtilTestService; + + @Autowired + private ParticipationUtilService participationUtilService; + + @Autowired + private IrisChatSessionService irisChatSessionService; + + private static Stream<Arguments> irisExceptions() { + // @formatter:off + return Stream.of( + Arguments.of(400, IrisInternalPyrisErrorException.class), + Arguments.of(401, IrisForbiddenException.class), + Arguments.of(403, IrisForbiddenException.class), + Arguments.of(404, IrisInternalPyrisErrorException.class), // TODO: Change with more specific exception + Arguments.of(500, IrisInternalPyrisErrorException.class), + Arguments.of(418, IrisInternalPyrisErrorException.class) // Test default case + ); + // @formatter:on + } + + @ParameterizedTest + @MethodSource("irisExceptions") + void testExceptionV2(int httpStatus, Class<?> exceptionClass) throws Exception { + userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); + + var course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); + var exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + activateIrisGlobally(); + activateIrisFor(course); + activateIrisFor(exercise); + var repository = new LocalRepository("main"); + + exercise = irisUtilTestService.setupTemplate(exercise, repository); + exercise = irisUtilTestService.setupSolution(exercise, repository); + exercise = irisUtilTestService.setupTest(exercise, repository); + var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(exercise, TEST_PREFIX + "student1"); + irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); + + var irisSession = irisChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + irisRequestMockProvider.mockRunError(httpStatus); + + ProgrammingExercise finalExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exercise.getId()); + assertThatThrownBy(() -> pyrisPipelineService.executeTutorChatPipeline("default", Optional.empty(), finalExercise, irisSession)).isInstanceOf(exceptionClass); + } + + @Test + void testOfferedModels() throws Exception { + irisRequestMockProvider.mockModelsResponse(); + + var offeredModels = pyrisConnectorService.getOfferedModels(); + assertThat(offeredModels).hasSize(1); + assertThat(offeredModels.get(0).id()).isEqualTo("TEST_MODEL"); + } + + @Test + void testOfferedModelsError() { + irisRequestMockProvider.mockModelsError(); + + assertThatThrownBy(() -> pyrisConnectorService.getOfferedModels()).isInstanceOf(PyrisConnectorException.class); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java index e41cfc09f02c..f6f77d971eaa 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.iris.AbstractIrisIntegrationTest; import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; import de.tum.in.www1.artemis.repository.iris.IrisSubSettingsRepository; -import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.iris.dto.IrisCombinedSettingsDTO; class IrisSettingsIntegrationTest extends AbstractIrisIntegrationTest { @@ -87,7 +87,9 @@ void getCourseSettingsAsUser() throws Exception { request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.FORBIDDEN, IrisSettings.class); var loadedSettings = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, true)); + assertThat(loadedSettings).isNotNull().usingRecursiveComparison() + .ignoringCollectionOrderInFields("irisChatSettings.allowedModels", "irisCompetencyGenerationSettings.allowedModels", "irisHestiaSettings.allowedModels") + .ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, true)); } @Test @@ -218,7 +220,9 @@ void getProgrammingExerciseSettingsAsUser() throws Exception { request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.FORBIDDEN, IrisSettings.class); var loadedSettings = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(programmingExercise, true)); + assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id") + .ignoringCollectionOrderInFields("irisChatSettings.allowedModels", "irisCompetencyGenerationSettings.allowedModels", "irisHestiaSettings.allowedModels") + .isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(programmingExercise, true)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java index a723826488ba..cb04b768ec5d 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentResourceIntegrationTest.java @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.AttachmentRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.LectureRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java index b183f88df81f..7dd0745c4748 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/CompetencyIntegrationTest.java @@ -46,7 +46,7 @@ import de.tum.in.www1.artemis.domain.lecture.TextUnit; import de.tum.in.www1.artemis.domain.participation.Participant; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; import de.tum.in.www1.artemis.repository.CompetencyRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java index f1c0a3aa4d63..122d9ae9fd52 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java @@ -77,7 +77,7 @@ class ExerciseUnitIntegrationTest extends AbstractSpringIntegrationIndependentTe void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, 2, 0, 1); List<Course> courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, 2); - this.course1 = this.courseRepository.findByIdWithExercisesAndLecturesElseThrow(courses.get(0).getId()); + this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.get(0).getId()); this.lecture1 = this.course1.getLectures().stream().findFirst().orElseThrow(); this.textExercise = textExerciseRepository.findByCourseIdWithCategories(course1.getId()).stream().findFirst().orElseThrow(); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java index 894c511236d0..f37bf902e1ff 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java @@ -88,7 +88,7 @@ void initTestCase() throws Exception { int numberOfTutors = 2; userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); List<Course> courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, numberOfTutors); - this.course1 = this.courseRepository.findByIdWithExercisesAndLecturesElseThrow(courses.get(0).getId()); + this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.get(0).getId()); var lecture = this.course1.getLectures().stream().findFirst().orElseThrow(); lecture.setTitle("Lecture " + new Random().nextInt()); // needed for search by title Channel channel = new Channel(); diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java index 056ff99cfb19..3facb541173b 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureUnitIntegrationTest.java @@ -78,7 +78,7 @@ class LectureUnitIntegrationTest extends AbstractSpringIntegrationIndependentTes void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); List<Course> courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, 1); - Course course1 = this.courseRepository.findByIdWithExercisesAndLecturesElseThrow(courses.get(0).getId()); + Course course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.get(0).getId()); this.lecture1 = course1.getLectures().stream().findFirst().orElseThrow(); // Add users that are not in the course diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java index 08fad9ec68f0..768f110c4a87 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/AbstractLocalCILocalVCIntegrationTest.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIDockerServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIDockerServiceTest.java index 837772beb3b8..ba1b1fcb8795 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIDockerServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIDockerServiceTest.java @@ -12,13 +12,20 @@ import java.util.List; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import com.github.dockerjava.api.command.InfoCmd; import com.github.dockerjava.api.command.InspectImageCmd; import com.github.dockerjava.api.command.ListContainersCmd; +import com.github.dockerjava.api.command.StopContainerCmd; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.Info; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; @@ -32,6 +39,7 @@ import de.tum.in.www1.artemis.service.connectors.localci.dto.BuildConfig; import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildJobQueueItem; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class LocalCIDockerServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { @Autowired @@ -41,6 +49,7 @@ class LocalCIDockerServiceTest extends AbstractSpringIntegrationLocalCILocalVCTe private BuildJobRepository buildJobRepository; @Autowired + @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @AfterEach @@ -49,6 +58,7 @@ void tearDown() { } @Test + @Order(2) void testDeleteOldDockerImages() { // Save build job with outdated image to database ZonedDateTime buildStartDate = ZonedDateTime.now().minusDays(3); @@ -70,6 +80,7 @@ void testDeleteOldDockerImages() { } @Test + @Order(1) void testDeleteOldDockerImages_NoOutdatedImages() { // Save build job to database ZonedDateTime buildStartDate = ZonedDateTime.now(); @@ -109,6 +120,28 @@ void testPullDockerImage() { verify(dockerClient, times(1)).pullImageCmd("test-image-name"); } + @Test + @Order(3) + void testCheckUsableDiskSpaceThenCleanUp() { + // Mock dockerClient.infoCmd().exec() + InfoCmd infoCmd = mock(InfoCmd.class); + Info info = mock(Info.class); + doReturn(infoCmd).when(dockerClient).infoCmd(); + doReturn(info).when(infoCmd).exec(); + doReturn("/").when(info).getDockerRootDir(); + + ZonedDateTime buildStartDate = ZonedDateTime.now(); + + IMap<String, ZonedDateTime> dockerImageCleanupInfo = hazelcastInstance.getMap("dockerImageCleanupInfo"); + + dockerImageCleanupInfo.put("test-image-name", buildStartDate); + + localCIDockerService.checkUsableDiskSpaceThenCleanUp(); + + // Verify that removeImageCmd() was called. + verify(dockerClient, times(2)).removeImageCmd("test-image-name"); + } + @Test void testRemoveStrandedContainers() { @@ -127,7 +160,7 @@ void testRemoveStrandedContainers() { localCIDockerService.cleanUpContainers(); // Verify that removeContainerCmd() was called - verify(dockerClient, times(1)).removeContainerCmd(anyString()); + verify(dockerClient, times(1)).stopContainerCmd(anyString()); // Mock container creation time to be younger than 5 minutes doReturn(Instant.now().getEpochSecond()).when(mockContainer).getCreated(); @@ -135,6 +168,19 @@ void testRemoveStrandedContainers() { localCIDockerService.cleanUpContainers(); // Verify that removeContainerCmd() was not called a second time - verify(dockerClient, times(1)).removeContainerCmd(anyString()); + verify(dockerClient, times(1)).stopContainerCmd(anyString()); + + // Mock container creation time to be older than 5 minutes + doReturn(Instant.now().getEpochSecond() - (6 * 60)).when(mockContainer).getCreated(); + + // Mock exception when stopping container + StopContainerCmd stopContainerCmd = mock(StopContainerCmd.class); + doReturn(stopContainerCmd).when(dockerClient).stopContainerCmd(anyString()); + doThrow(new RuntimeException("Container stopping failed")).when(stopContainerCmd).exec(); + + localCIDockerService.cleanUpContainers(); + + // Verify that killContainerCmd() was called + verify(dockerClient, times(1)).killContainerCmd(anyString()); } } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java index 1e856ecbd34d..4446b98c3a5e 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResourceIntegrationTest.java @@ -61,7 +61,7 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTe protected IQueue<LocalCIBuildJobQueueItem> queuedJobs; - protected IMap<Long, LocalCIBuildJobQueueItem> processingJobs; + protected IMap<String, LocalCIBuildJobQueueItem> processingJobs; protected IMap<String, LocalCIBuildAgentInformation> buildAgentInformation; @@ -86,7 +86,7 @@ void createJobs() { job1 = new LocalCIBuildJobQueueItem("1", "job1", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); job2 = new LocalCIBuildJobQueueItem("2", "job2", "address1", 2, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - agent1 = new LocalCIBuildAgentInformation(memberAddress, 1, 0, null, false, new ArrayList<>(List.of())); + agent1 = new LocalCIBuildAgentInformation(memberAddress, 1, 0, new ArrayList<>(List.of(job1)), false, new ArrayList<>(List.of(job2))); LocalCIBuildJobQueueItem finishedJobQueueItem1 = new LocalCIBuildJobQueueItem("3", "job3", "address1", 3, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); LocalCIBuildJobQueueItem finishedJobQueueItem2 = new LocalCIBuildJobQueueItem("4", "job4", "address1", 4, course.getId() + 1, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, @@ -104,8 +104,8 @@ void createJobs() { processingJobs = hazelcastInstance.getMap("processingJobs"); buildAgentInformation = hazelcastInstance.getMap("buildAgentInformation"); - processingJobs.put(1L, job1); - processingJobs.put(2L, job2); + processingJobs.put(job1.id(), job1); + processingJobs.put(job2.id(), job2); buildAgentInformation.put(memberAddress, agent1); } @@ -181,11 +181,25 @@ void testGetBuildAgents_returnsAgents() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") - void testCancelBuildJob() throws Exception { - LocalCIBuildJobQueueItem buildJob = processingJobs.get(1L); + void testGetBuildAgentDetails_returnsAgent() throws Exception { + var retrievedAgent = request.get("/api/admin/build-agent?agentName=" + agent1.name(), HttpStatus.OK, LocalCIBuildAgentInformation.class); + assertThat(retrievedAgent.name()).isEqualTo(agent1.name()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") + void testCancelProcessingBuildJob() throws Exception { + LocalCIBuildJobQueueItem buildJob = processingJobs.get(job1.id()); request.delete("/api/admin/cancel-job/" + buildJob.id(), HttpStatus.NO_CONTENT); } + @Test + @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") + void testCancelQueuedBuildJob() throws Exception { + queuedJobs.put(job1); + request.delete("/api/admin/cancel-job/" + job1.id(), HttpStatus.NO_CONTENT); + } + @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testCancelAllQueuedBuildJobs() throws Exception { @@ -201,13 +215,15 @@ void testCancelAllRunningBuildJobs() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCancelBuildJobForCourse() throws Exception { - LocalCIBuildJobQueueItem buildJob = processingJobs.get(1L); + LocalCIBuildJobQueueItem buildJob = processingJobs.get(job1.id()); request.delete("/api/courses/" + course.getId() + "/cancel-job/" + buildJob.id(), HttpStatus.NO_CONTENT); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCancelAllQueuedBuildJobsForCourse() throws Exception { + queuedJobs.put(job1); + queuedJobs.put(job2); request.delete("/api/courses/" + course.getId() + "/cancel-all-queued-jobs", HttpStatus.NO_CONTENT); } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResultServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResultServiceTest.java index 629b913c6da6..060014d4c0aa 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResultServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIResultServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import de.tum.in.www1.artemis.exception.LocalCIException; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; import de.tum.in.www1.artemis.service.connectors.localci.LocalCIResultService; class LocalCIResultServiceTest extends AbstractLocalCILocalVCIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java index fd6d02d5c261..201083921fd7 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.service.connectors.BuildScriptProviderService; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusTemplateService; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCITestConfiguration.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCITestConfiguration.java index 568d96b8304e..2b5f32a9786b 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCITestConfiguration.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCITestConfiguration.java @@ -25,12 +25,14 @@ import com.github.dockerjava.api.command.ExecStartCmd; import com.github.dockerjava.api.command.InspectImageCmd; import com.github.dockerjava.api.command.InspectImageResponse; +import com.github.dockerjava.api.command.KillContainerCmd; import com.github.dockerjava.api.command.ListContainersCmd; import com.github.dockerjava.api.command.ListImagesCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; import com.github.dockerjava.api.command.RemoveImageCmd; import com.github.dockerjava.api.command.StartContainerCmd; +import com.github.dockerjava.api.command.StopContainerCmd; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.Image; @@ -137,7 +139,7 @@ public DockerClient dockerClient() throws InterruptedException { doReturn(new String[] { "test-image-name" }).when(image).getRepoTags(); doReturn(List.of(image)).when(listImagesCmd).exec(); - // Mock removeContainer() method. + // Mock removeImageCmd method. RemoveImageCmd removeImageCmd = mock(RemoveImageCmd.class); doReturn(removeImageCmd).when(dockerClient).removeImageCmd(anyString()); doNothing().when(removeImageCmd).exec(); @@ -147,6 +149,15 @@ public DockerClient dockerClient() throws InterruptedException { doReturn(removeContainerCmd).when(dockerClient).removeContainerCmd(anyString()); doReturn(removeContainerCmd).when(removeContainerCmd).withForce(true); + // Mock stopContainerCmd + StopContainerCmd stopContainerCmd = mock(StopContainerCmd.class); + doReturn(stopContainerCmd).when(dockerClient).stopContainerCmd(anyString()); + doReturn(stopContainerCmd).when(stopContainerCmd).withTimeout(any()); + + // Mock killContainerCmd + KillContainerCmd killContainerCmd = mock(KillContainerCmd.class); + doReturn(killContainerCmd).when(dockerClient).killContainerCmd(anyString()); + return dockerClient; } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java index c1635055fd74..b730f2936833 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java @@ -16,7 +16,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.participation.TemplateProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUri; import de.tum.in.www1.artemis.util.LocalRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCServiceTest.java index f48f8a96ac04..b037de655d28 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCServiceTest.java @@ -14,7 +14,7 @@ import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; class LocalVCServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java b/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java index a1e06ea1bee6..b9956458b631 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/SharedQueueManagementService.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; @@ -25,6 +26,7 @@ class SharedQueueManagementServiceTest extends AbstractSpringIntegrationLocalCIL private BuildJobRepository buildJobRepository; @Autowired + @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @BeforeEach diff --git a/src/test/java/de/tum/in/www1/artemis/metis/ChannelIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/ChannelIntegrationTest.java index baa99dde8709..6d74f1b6048a 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/ChannelIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/ChannelIntegrationTest.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.post.ConversationUtilService; import de.tum.in.www1.artemis.repository.LectureRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/metis/ConversationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/metis/ConversationIntegrationTest.java index c881526bd6dc..d2058a683ffd 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/ConversationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/ConversationIntegrationTest.java @@ -27,7 +27,7 @@ import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.post.ConversationUtilService; import de.tum.in.www1.artemis.service.dto.ResponsibleUserDTO; @@ -211,7 +211,10 @@ void getConversationsOfUser_onlyCourseWideChannelsIfMessagingDisabled() throws E setCourseInformationSharingConfiguration(CourseInformationSharingConfiguration.COMMUNICATION_ONLY); List<ConversationDTO> channels = request.getList("/api/courses/" + exampleCourseId + "/conversations", HttpStatus.OK, ConversationDTO.class); - channels.forEach(conv -> assertThat(conv instanceof ChannelDTO ch && ch.getIsCourseWide())); + assertThat(channels).allSatisfy(ch -> { + assertThat(ch).isInstanceOf(ChannelDTO.class); + assertThat(((ChannelDTO) ch).getIsCourseWide()).isTrue(); + }); // cleanup conversationMessageRepository.deleteById(post.getId()); diff --git a/src/test/java/de/tum/in/www1/artemis/metis/PostingServiceUnitTest.java b/src/test/java/de/tum/in/www1/artemis/metis/PostingServiceUnitTest.java index 539a706b1fd2..3f650e76ee09 100644 --- a/src/test/java/de/tum/in/www1/artemis/metis/PostingServiceUnitTest.java +++ b/src/test/java/de/tum/in/www1/artemis/metis/PostingServiceUnitTest.java @@ -13,6 +13,7 @@ import java.lang.reflect.Method; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -40,14 +41,23 @@ class PostingServiceUnitTest { private Method parseUserMentions; + private AutoCloseable closeable; + @BeforeEach void initTestCase() throws NoSuchMethodException { - MockitoAnnotations.openMocks(this); + closeable = MockitoAnnotations.openMocks(this); parseUserMentions = PostingService.class.getDeclaredMethod("parseUserMentions", Course.class, String.class); parseUserMentions.setAccessible(true); } + @AfterEach + void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + } + @Test void testParseUserMentionsEmptyContent() throws InvocationTargetException, IllegalAccessException { Course course = new Course(); diff --git a/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java index fe60b182d5c6..b9edfa501ba1 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/GroupNotificationServiceTest.java @@ -62,8 +62,8 @@ import de.tum.in.www1.artemis.domain.notification.Notification; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.NotificationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java index 89edd51555f9..df542a5fa2fa 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationResourceIntegrationTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.notification.Notification; import de.tum.in.www1.artemis.domain.notification.NotificationConstants; import de.tum.in.www1.artemis.domain.notification.SingleUserNotification; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.NotificationRepository; import de.tum.in.www1.artemis.repository.NotificationSettingRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java index a1b688ac565e..acc58c6b7964 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/NotificationScheduleServiceTest.java @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.NotificationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java index 7c49839e6671..58e97088c0da 100644 --- a/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/notification/SingleUserNotificationServiceTest.java @@ -94,8 +94,8 @@ import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroup; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.NotificationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationFactory.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationFactory.java index 851fa90c3f1c..4bc9b6c74989 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationFactory.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationFactory.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.participation; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory.DEFAULT_BRANCH; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory.DEFAULT_BRANCH; import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java index 01432d81a88a..34952f69234d 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java @@ -1,11 +1,14 @@ package de.tum.in.www1.artemis.participation; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import java.net.URI; @@ -20,12 +23,16 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; @@ -33,7 +40,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.in.www1.artemis.AbstractSpringIntegrationJenkinsGitlabTest; +import de.tum.in.www1.artemis.AbstractAthenaTest; import de.tum.in.www1.artemis.assessment.GradingScaleUtilService; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.Course; @@ -52,6 +59,7 @@ import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.QuizMode; +import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.Participation; @@ -64,15 +72,17 @@ import de.tum.in.www1.artemis.domain.quiz.ShortAnswerSpot; import de.tum.in.www1.artemis.domain.quiz.ShortAnswerSubmittedAnswer; import de.tum.in.www1.artemis.domain.quiz.ShortAnswerSubmittedText; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exam.ExamFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; @@ -80,14 +90,14 @@ import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.service.GradingScaleService; import de.tum.in.www1.artemis.service.ParticipationService; -import de.tum.in.www1.artemis.service.QuizBatchService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggleService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; import de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.LocalRepository; -class ParticipationIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ParticipationIntegrationTest extends AbstractAthenaTest { private static final String TEST_PREFIX = "participationintegration"; @@ -151,6 +161,12 @@ class ParticipationIntegrationTest extends AbstractSpringIntegrationJenkinsGitla @Autowired private QuizExerciseUtilService quizExerciseUtilService; + @Autowired + private ExamRepository examRepository; + + @Captor + private ArgumentCaptor<Result> resultCaptor; + @Value("${artemis.version-control.default-branch:main}") private String defaultBranch; @@ -163,7 +179,7 @@ class ParticipationIntegrationTest extends AbstractSpringIntegrationJenkinsGitla private ProgrammingExercise programmingExercise; @BeforeEach - void initTestCase() throws Exception { + void initTestData() throws Exception { userUtilService.addUsers(TEST_PREFIX, 4, 1, 1, 1); // Add users that are not in the course/exercise @@ -197,6 +213,8 @@ void initTestCase() throws Exception { @AfterEach void tearDown() throws Exception { + Mockito.reset(programmingMessagingService); + featureToggleService.enableFeature(Feature.ProgrammingExercises); programmingExerciseTestService.tearDown(); } @@ -508,6 +526,7 @@ void deleteParticipation_notFound() throws Exception { } @Test + @Disabled @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void requestFeedbackScoreNotFull() throws Exception { var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, @@ -558,7 +577,17 @@ void requestFeedbackAlreadySent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void requestFeedbackSuccess() throws Exception { + void requestFeedbackSuccess_withAthenaSuccess() throws Exception { + + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + this.courseRepo.save(course); + + this.programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + this.exerciseRepo.save(programmingExercise); + + athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("programming"); + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); @@ -570,19 +599,72 @@ void requestFeedbackSuccess() throws Exception { gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); - var result = ParticipationFactory.generateResult(true, 100).participation(participation); - result.setCompletionDate(ZonedDateTime.now()); - resultRepository.save(result); + Result result1 = participationUtilService.createSubmissionAndResult(participation, 100, false); + Result result2 = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); + result2.setAssessmentType(AssessmentType.AUTOMATIC); + result2.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result2); - doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(programmingExercise, participation); + doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(eq(programmingExercise), any()); + doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); - var response = request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, - HttpStatus.OK); + request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); - assertThat(response.getResults()).allMatch(result1 -> result.getAssessmentType() == AssessmentType.SEMI_AUTOMATIC); - assertThat(response.getIndividualDueDate()).isNotNull().isBefore(ZonedDateTime.now()); + verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); + + Result invokedResult = resultCaptor.getAllValues().get(0); + assertThat(invokedResult).isNotNull(); + assertThat(invokedResult.getId()).isNotNull(); + assertThat(invokedResult.isSuccessful()).isTrue(); + assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.getFeedbacks()).hasSize(1); + + localRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void requestFeedbackSuccess_withAthenaFailure() throws Exception { + + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + this.courseRepo.save(course); + + this.programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + this.exerciseRepo.save(programmingExercise); + this.athenaRequestMockProvider.mockGetFeedbackSuggestionsWithFailure("programming"); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + Result result1 = participationUtilService.createSubmissionAndResult(participation, 100, false); + Result result2 = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); + result2.setAssessmentType(AssessmentType.AUTOMATIC); + result2.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result2); + + doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(any(), any()); + doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); + + request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); + + verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); + + Result invokedResult = resultCaptor.getAllValues().get(0); + assertThat(invokedResult).isNotNull(); + assertThat(invokedResult.getId()).isNotNull(); + assertThat(invokedResult.isSuccessful()).isFalse(); + assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.getFeedbacks()).hasSize(0); - verify(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(programmingExercise, participation); localRepo.resetLocalRepo(); } @@ -1378,6 +1460,135 @@ void testCheckQuizParticipation(QuizMode quizMode) throws Exception { assertThat(actualSubmittedAnswerText.isIsCorrect()).isFalse(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void whenRequestFeedbackForExam_thenFail() throws Exception { + + Exam examWithExerciseGroups = ExamFactory.generateExamWithExerciseGroup(course, false); + examRepository.save(examWithExerciseGroups); + var exerciseGroup1 = examWithExerciseGroups.getExerciseGroups().get(0); + programmingExercise = exerciseRepo.save(ProgrammingExerciseFactory.generateProgrammingExerciseForExam(exerciseGroup1)); + course.addExercises(programmingExercise); + course = courseRepo.save(course); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + var result = ParticipationFactory.generateResult(true, 100).participation(participation); + result.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result); + + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + + localRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void whenFeedbackRequestedAndDeadlinePassed_thenFail() throws Exception { + + programmingExercise.setDueDate(ZonedDateTime.now().minusDays(100)); + programmingExercise = exerciseRepo.save(programmingExercise); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + var result = ParticipationFactory.generateResult(true, 100).participation(participation); + result.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result); + + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + + localRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void whenFeedbackRequestedAndRateLimitExceeded_thenFail() throws Exception { + + programmingExercise.setDueDate(ZonedDateTime.now().plusDays(100)); + programmingExercise = exerciseRepo.save(programmingExercise); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + var result = ParticipationFactory.generateResult(true, 100).participation(participation); + result.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result); + + // generate 5 athena results + for (int i = 0; i < 5; i++) { + var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); + athenaResult.setCompletionDate(ZonedDateTime.now()); + athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + resultRepository.save(athenaResult); + } + + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + + localRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void whenFeedbackRequestedAndRateLimitStillUnknownDueRequestsInProgress_thenFail() throws Exception { + + programmingExercise.setDueDate(ZonedDateTime.now().plusDays(100)); + programmingExercise = exerciseRepo.save(programmingExercise); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + var result = ParticipationFactory.generateResult(false, 100).participation(participation); + result.setCompletionDate(ZonedDateTime.now()); + resultRepository.save(result); + + // generate 5 athena results + for (int i = 0; i < 5; i++) { + var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); + athenaResult.setCompletionDate(ZonedDateTime.now()); + athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + athenaResult.setSuccessful(null); + resultRepository.save(athenaResult); + } + + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + + localRepo.resetLocalRepo(); + } + @Nested @Isolated class ParticipationIntegrationIsolatedTest { diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java index 5f715f91bcd9..a6ebcd78e885 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationSubmissionIntegrationTest.java @@ -16,7 +16,7 @@ import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.user.UserUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java index 491468d2de71..41e663b6e40f 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionExportIntegrationTest.java @@ -31,9 +31,9 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.rest.dto.SubmissionExportOptionsDTO; diff --git a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java index 8a9ede80fa3d..14796c45f3fd 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/SubmissionIntegrationTest.java @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java index 3731f231d83a..8bb2473a2161 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismCaseIntegrationTest.java @@ -31,7 +31,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.metis.PostRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java index f41b7f2051e4..1744bd6f1df4 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismIntegrationTest.java @@ -24,7 +24,7 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismSubmission; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.TextExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismUtilService.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismUtilService.java index a9e1adbdf70e..2752b16e60e2 100644 --- a/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismUtilService.java @@ -20,8 +20,8 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/post/ConversationUtilService.java b/src/test/java/de/tum/in/www1/artemis/post/ConversationUtilService.java index 3a827093d33b..ade4a3b36140 100644 --- a/src/test/java/de/tum/in/www1/artemis/post/ConversationUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/post/ConversationUtilService.java @@ -33,7 +33,7 @@ import de.tum.in.www1.artemis.domain.metis.conversation.OneToOneChat; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismCase; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.lecture.LectureFactory; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetrieverTest.java b/src/test/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetrieverTest.java index 270187a16182..49eb3576fd4e 100644 --- a/src/test/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetrieverTest.java +++ b/src/test/java/de/tum/in/www1/artemis/security/lti/Lti13TokenRetrieverTest.java @@ -36,6 +36,8 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; @@ -156,13 +158,13 @@ void getTokenNoReturn() throws NoSuchAlgorithmException { } @Test - void getToken() throws NoSuchAlgorithmException { + void getToken() throws NoSuchAlgorithmException, JsonProcessingException { JWK jwk = generateKey(); when(oAuth2JWKSService.getJWK(any())).thenReturn(jwk); Map<String, String> map = new HashMap<>(); map.put("access_token", "result"); - ResponseEntity<String> responseEntity = ResponseEntity.of(Optional.of(map.toString())); + ResponseEntity<String> responseEntity = ResponseEntity.of(Optional.of(new ObjectMapper().writeValueAsString(map))); when(restTemplate.exchange(any(), eq(String.class))).thenReturn(responseEntity); String token = lti13TokenRetriever.getToken(clientRegistration, Scopes.AGS_SCORE); diff --git a/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java index a9a5d0b53ea0..8f98bc6079ae 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/AssessmentServiceTest.java @@ -35,9 +35,9 @@ import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.GradingCriterionUtil; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java index b36fe5c9aa8c..91c6fb84bf0f 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ComplaintResponseServiceTest.java @@ -21,7 +21,7 @@ import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.ComplaintType; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ComplaintRepository; import de.tum.in.www1.artemis.repository.ResultRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ConsistencyCheckTestService.java b/src/test/java/de/tum/in/www1/artemis/service/ConsistencyCheckTestService.java index 9d26b0792515..b2b704f44839 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ConsistencyCheckTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ConsistencyCheckTestService.java @@ -14,8 +14,8 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.MockDelegate; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.dto.ConsistencyErrorDTO; diff --git a/src/test/java/de/tum/in/www1/artemis/service/CourseServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/CourseServiceTest.java index 4fac86070ca2..8e245d3b4181 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/CourseServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/CourseServiceTest.java @@ -26,7 +26,7 @@ import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java index d405904bdefd..f11896357a19 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/DataExportCreationServiceTest.java @@ -56,9 +56,9 @@ import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismVerdict; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.post.ConversationUtilService; import de.tum.in.www1.artemis.repository.DataExportRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java index 602954fc18e4..6bb7c6d5409d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/EmailSummaryServiceTest.java @@ -28,7 +28,7 @@ import de.tum.in.www1.artemis.domain.NotificationSetting; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.NotificationSettingRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java index b69e7a70df2f..5f924ae0637e 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ExerciseDateServiceTest.java @@ -20,8 +20,8 @@ import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java index 1d670c1959c7..0385dbba2ba9 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/GitlabCIServiceTest.java @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exception.GitLabCIException; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.BuildLogStatisticsEntryRepository; import de.tum.in.www1.artemis.repository.BuildPlanRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java index 3acfc8fa0a34..aa108d23de5f 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/JenkinsServiceTest.java @@ -37,8 +37,8 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.exception.JenkinsException; -import de.tum.in.www1.artemis.exercise.programmingexercise.ContinuousIntegrationTestService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ContinuousIntegrationTestService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.BuildPlanRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java index 78992894bcb1..c9385f311fcb 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LearningPathServiceTest.java @@ -35,7 +35,7 @@ import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java index c7ebe13c567f..c32062c8d925 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/LectureImportServiceTest.java @@ -52,7 +52,7 @@ class LectureImportServiceTest extends AbstractSpringIntegrationIndependentTest void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 0, 0, 0, 1); List<Course> courses = lectureUtilService.createCoursesWithExercisesAndLecturesAndLectureUnits(TEST_PREFIX, false, true, 0); - Course course1 = this.courseRepository.findByIdWithExercisesAndLecturesElseThrow(courses.get(0).getId()); + Course course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.get(0).getId()); long lecture1Id = course1.getLectures().stream().findFirst().orElseThrow().getId(); this.lecture1 = this.lectureRepository.findByIdWithAttachmentsAndPostsAndLectureUnitsAndCompetenciesAndCompletionsElseThrow(lecture1Id); this.course2 = courseUtilService.createCourse(); diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java index 47eead66938f..2a2e064108d4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationAuthorizationCheckServiceTest.java @@ -17,7 +17,7 @@ import de.tum.in.www1.artemis.domain.participation.ParticipationInterface; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java index 61d5b54735f5..df0007e3268c 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationLifecycleServiceTest.java @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ParticipationLifecycle; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.security.SecurityUtils; import de.tum.in.www1.artemis.user.UserUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 377fe392fdcc..34af5ff620f5 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -29,8 +29,8 @@ import de.tum.in.www1.artemis.domain.participation.Participant; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.BuildLogEntryRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java index 6d5c1ccf7b80..57ccb7daead3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationTeamWebsocketServiceTest.java @@ -28,8 +28,8 @@ import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.dto.SubmissionPatch; diff --git a/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java index aa9e0bf69e85..cdac6403e9d8 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/PresentationPointsCalculationServiceTest.java @@ -16,7 +16,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java index c3422da7266b..b1c5ccea8333 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java @@ -1,6 +1,6 @@ package de.tum.in.www1.artemis.service; -import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseResultTestService.convertBuildResultToJsonObject; +import static de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseResultTestService.convertBuildResultToJsonObject; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.catchThrowableOfType; @@ -24,8 +24,8 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.submissionpolicy.LockRepositoryPolicy; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java index a595692b57b2..11b2c91b4f92 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ResultServiceTest.java @@ -23,7 +23,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java index 26bcdbc49de7..1b5d7826084d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/SubmissionServiceTest.java @@ -38,7 +38,7 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ComplaintRepository; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java index caac79334b91..45669457bb30 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/TitleCacheEvictionServiceTest.java @@ -14,9 +14,9 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.modelingexercise.ModelingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.modeling.ModelingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.lecture.LectureUtilService; import de.tum.in.www1.artemis.organization.OrganizationUtilService; import de.tum.in.www1.artemis.repository.ApollonDiagramRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java index 2ae3af05a5ed..08b2a739e15d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java @@ -30,8 +30,8 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.GradingCriterionUtil; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.GradingCriterionRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.TextBlockRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java index 25b19e11f3a2..ec73b73ba5c0 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java @@ -20,8 +20,8 @@ import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exception.NetworkingException; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.service.dto.athena.ProgrammingFeedbackDTO; import de.tum.in.www1.artemis.service.dto.athena.TextFeedbackDTO; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; @@ -69,7 +69,7 @@ void testFeedbackSuggestionsText() throws NetworkingException { athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("text", jsonPath("$.exercise.id").value(textExercise.getId()), jsonPath("$.exercise.title").value(textExercise.getTitle()), jsonPath("$.submission.id").value(textSubmission.getId()), jsonPath("$.submission.text").value(textSubmission.getText())); - List<TextFeedbackDTO> suggestions = athenaFeedbackSuggestionsService.getTextFeedbackSuggestions(textExercise, textSubmission); + List<TextFeedbackDTO> suggestions = athenaFeedbackSuggestionsService.getTextFeedbackSuggestions(textExercise, textSubmission, true); assertThat(suggestions.get(0).title()).isEqualTo("Not so good"); assertThat(suggestions.get(0).indexStart()).isEqualTo(3); athenaRequestMockProvider.verify(); @@ -82,7 +82,7 @@ void testFeedbackSuggestionsProgramming() throws NetworkingException { jsonPath("$.exercise.title").value(programmingExercise.getTitle()), jsonPath("$.submission.id").value(programmingSubmission.getId()), jsonPath("$.submission.repositoryUri") .value("https://artemislocal.ase.in.tum.de/api/public/athena/programming-exercises/" + programmingExercise.getId() + "/submissions/3/repository")); - List<ProgrammingFeedbackDTO> suggestions = athenaFeedbackSuggestionsService.getProgrammingFeedbackSuggestions(programmingExercise, programmingSubmission); + List<ProgrammingFeedbackDTO> suggestions = athenaFeedbackSuggestionsService.getProgrammingFeedbackSuggestions(programmingExercise, programmingSubmission, true); assertThat(suggestions.get(0).title()).isEqualTo("Not so good"); assertThat(suggestions.get(0).lineStart()).isEqualTo(3); athenaRequestMockProvider.verify(); @@ -93,6 +93,6 @@ void testFeedbackSuggestionsIdConflict() { athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("text"); var otherExercise = new TextExercise(); textSubmission.setParticipation(new StudentParticipation().exercise(otherExercise)); // Add submission to wrong exercise - assertThatExceptionOfType(ConflictException.class).isThrownBy(() -> athenaFeedbackSuggestionsService.getTextFeedbackSuggestions(textExercise, textSubmission)); + assertThatExceptionOfType(ConflictException.class).isThrownBy(() -> athenaFeedbackSuggestionsService.getTextFeedbackSuggestions(textExercise, textSubmission, true)); } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java index 8340d77dc7d5..2484c46adbfa 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.LocalRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java index e80cf4408580..99853700d473 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java @@ -21,8 +21,8 @@ import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.TextSubmission; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java index b786268c64db..16fe2babdfb4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java @@ -17,8 +17,8 @@ import de.tum.in.www1.artemis.domain.TextSubmission; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; import de.tum.in.www1.artemis.domain.enumeration.Language; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingServiceTest.java index afffccd67126..f29e605c573d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/lti/LtiDeepLinkingServiceTest.java @@ -7,6 +7,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -66,7 +71,7 @@ void tearDown() throws Exception { } @Test - void testPerformDeepLinking() { + void testPerformDeepLinking() throws MalformedURLException { createMockOidcIdToken(); when(tokenRetriever.createDeepLinkingJWT(anyString(), anyMap())).thenReturn("test_jwt"); @@ -78,7 +83,7 @@ void testPerformDeepLinking() { } @Test - void testEmptyJwtBuildLtiDeepLinkResponse() { + void testEmptyJwtBuildLtiDeepLinkResponse() throws MalformedURLException { createMockOidcIdToken(); when(tokenRetriever.createDeepLinkingJWT(anyString(), anyMap())).thenReturn(null); @@ -90,7 +95,7 @@ void testEmptyJwtBuildLtiDeepLinkResponse() { } @Test - void testEmptyReturnUrlBuildLtiDeepLinkResponse() throws JsonProcessingException { + void testEmptyReturnUrlBuildLtiDeepLinkResponse() throws JsonProcessingException, MalformedURLException { createMockOidcIdToken(); when(tokenRetriever.createDeepLinkingJWT(anyString(), anyMap())).thenReturn("test_jwt"); ObjectMapper mapper = new ObjectMapper(); @@ -129,7 +134,7 @@ void testEmptyReturnUrlBuildLtiDeepLinkResponse() throws JsonProcessingException } @Test - void testEmptyDeploymentIdBuildLtiDeepLinkResponse() { + void testEmptyDeploymentIdBuildLtiDeepLinkResponse() throws MalformedURLException { createMockOidcIdToken(); when(tokenRetriever.createDeepLinkingJWT(anyString(), anyMap())).thenReturn("test_jwt"); when(oidcIdToken.getClaim(de.tum.in.www1.artemis.domain.lti.Claims.LTI_DEPLOYMENT_ID)).thenReturn(null); @@ -140,17 +145,22 @@ void testEmptyDeploymentIdBuildLtiDeepLinkResponse() { .withMessage("Missing claim: " + Claims.LTI_DEPLOYMENT_ID); } - private void createMockOidcIdToken() { + private void createMockOidcIdToken() throws MalformedURLException { Map<String, Object> mockSettings = new TreeMap<>(); mockSettings.put("deep_link_return_url", "test_return_url"); when(oidcIdToken.getClaim(Claims.DEEP_LINKING_SETTINGS)).thenReturn(mockSettings); - when(oidcIdToken.getClaim("iss")).thenReturn("http://artemis.com"); when(oidcIdToken.getClaim("aud")).thenReturn("http://moodle.com"); when(oidcIdToken.getClaim("exp")).thenReturn("12345"); when(oidcIdToken.getClaim("iat")).thenReturn("test"); when(oidcIdToken.getClaim("nonce")).thenReturn("1234-34535-abcbcbd"); + when(oidcIdToken.getIssuer()).thenReturn(new URL("http://artemis.com")); + when(oidcIdToken.getAudience()).thenReturn(Arrays.asList("http://moodle.com")); + when(oidcIdToken.getExpiresAt()).thenReturn(Instant.now().plus(2, ChronoUnit.HOURS)); + when(oidcIdToken.getIssuedAt()).thenReturn(Instant.now()); + when(oidcIdToken.getNonce()).thenReturn("1234-34535-abcbcbd"); when(oidcIdToken.getClaim(Claims.LTI_DEPLOYMENT_ID)).thenReturn("1"); + when(oidcIdToken.getClaimAsString(Claims.LTI_DEPLOYMENT_ID)).thenReturn("1"); } private Exercise createMockExercise(long exerciseId, long courseId) { diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java index 7934225296b9..212040aca2ea 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamQuizServiceTest.java @@ -28,14 +28,14 @@ import de.tum.in.www1.artemis.domain.quiz.QuizQuestion; import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.ExerciseGroupRepository; import de.tum.in.www1.artemis.repository.QuizExerciseRepository; import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; -import de.tum.in.www1.artemis.service.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; class ExamQuizServiceTest extends AbstractSpringIntegrationIndependentTest { diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java index f2217fb1b452..7953e1727146 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionServiceTest.java @@ -21,8 +21,8 @@ import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/service/export/CourseExamExportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/export/CourseExamExportServiceTest.java new file mode 100644 index 000000000000..4388c4eafead --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/service/export/CourseExamExportServiceTest.java @@ -0,0 +1,91 @@ +package de.tum.in.www1.artemis.service.export; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.user.UserUtilService; + +class CourseExamExportServiceTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "exam_export"; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private ExamRepository examRepository; + + @Autowired + private CourseExamExportService courseExamExportService; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private ExerciseRepository exerciseRepository; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + void setup() { + // setup users + userUtilService.addUsers(TEST_PREFIX, 2, 3, 0, 1); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportCourseExams() throws IOException { + var course = courseUtilService.createCourseWithExamExercisesAndSubmissions(TEST_PREFIX); + var exam = examRepository.findByCourseId(course.getId()).stream().findFirst().orElseThrow(); + List<String> exportErrors = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> courseExamExportService.exportExam(exam, Path.of("tmp/export"), exportErrors)); + + assertThat(exportErrors).isEmpty(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testExportCourse() throws IOException { + // Add tutor for complaint response + User tutor = userUtilService.createAndSaveUser(TEST_PREFIX + "tutor5"); + tutor.setGroups(Set.of("tutor")); + userRepository.save(tutor); + + var course = courseUtilService.createCourseWithExamExercisesAndSubmissions(TEST_PREFIX); + var courseWithExercises = courseUtilService.addCourseWithExercisesAndSubmissions(TEST_PREFIX, "", 3, 2, 1, 1, true, 1, ""); + var exercises = courseWithExercises.getExercises(); + exercises.forEach(exercise -> { + exercise.setCourse(course); + }); + exerciseRepository.saveAll(exercises); + course.setExercises(courseWithExercises.getExercises()); + courseRepository.save(course); + + List<String> exportErrors = new ArrayList<>(); + assertThatNoException().isThrownBy(() -> courseExamExportService.exportCourse(course, Path.of("tmp/export"), exportErrors)); + + assertThat(exportErrors).isEmpty(); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/service/notifications/GeneralInstantNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/notifications/GeneralInstantNotificationServiceTest.java index 13c229e0e7a1..275c2076d630 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/notifications/GeneralInstantNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/notifications/GeneralInstantNotificationServiceTest.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -44,9 +45,11 @@ class GeneralInstantNotificationServiceTest { private Notification notification; + private AutoCloseable closeable; + @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); + closeable = MockitoAnnotations.openMocks(this); student1 = new User(); student1.setId(1L); student1.setLogin("1"); @@ -63,6 +66,13 @@ void setUp() { when(notificationSettingsService.checkNotificationTypeForEmailSupport(any(NotificationType.class))).thenCallRealMethod(); } + @AfterEach + void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + } + /** * Very basic test that checks if emails and pushes are send for one user */ diff --git a/src/test/java/de/tum/in/www1/artemis/service/notifications/MailServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/notifications/MailServiceTest.java index a8afcece758e..1af71cfe2c9a 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/notifications/MailServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/notifications/MailServiceTest.java @@ -1,23 +1,36 @@ package de.tum.in.www1.artemis.service.notifications; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.ZonedDateTime; +import java.util.Set; + import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.context.MessageSource; +import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.test.util.ReflectionTestUtils; import org.thymeleaf.spring6.SpringTemplateEngine; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.exception.ArtemisMailException; +import de.tum.in.www1.artemis.domain.enumeration.GroupNotificationType; +import de.tum.in.www1.artemis.domain.metis.Post; +import de.tum.in.www1.artemis.domain.metis.conversation.Channel; +import de.tum.in.www1.artemis.domain.notification.GroupNotification; +import de.tum.in.www1.artemis.domain.notification.NotificationConstants; import de.tum.in.www1.artemis.service.TimeService; import tech.jhipster.config.JHipsterProperties; @@ -53,6 +66,8 @@ class MailServiceTest { private User student1; + private User student2; + private String subject; private String content; @@ -61,11 +76,17 @@ class MailServiceTest { * Prepares the needed values and objects for testing */ @BeforeEach - void setUp() { + void setUp() throws MalformedURLException, URISyntaxException { student1 = new User(); + student1.setLogin("student1"); student1.setId(555L); - String EMAIL_ADDRESS_A = "benige8246@omibrown.com"; - student1.setEmail(EMAIL_ADDRESS_A); + student1.setEmail("benige8246@omibrown.com"); + student1.setLangKey("de"); + + student2 = new User(); + student2.setLogin("student2"); + student2.setId(556L); + student2.setEmail("bege123@abc.com"); subject = "subject"; content = "content"; @@ -82,7 +103,14 @@ void setUp() { jHipsterProperties = mock(JHipsterProperties.class); when(jHipsterProperties.getMail()).thenReturn(mail); + messageSource = mock(MessageSource.class); + when(messageSource.getMessage(any(String.class), any(), any())).thenReturn("test"); + + templateEngine = mock(SpringTemplateEngine.class); + when(templateEngine.process(any(String.class), any())).thenReturn("test"); + mailService = new MailService(jHipsterProperties, javaMailSender, messageSource, templateEngine, timeService); + ReflectionTestUtils.setField(mailService, "artemisServerUrl", new URI("http://localhost:8080").toURL()); } /** @@ -95,11 +123,34 @@ void testSendEmail() { } /** - * When the javaMailSender returns an exception, that exception should be caught and an ArtemisMailException should be thrown instead. + * When the javaMailSender returns an exception, that exception should be caught and should not be thrown instead. + */ + @Test + void testNoMailSendExceptionThrown() { + doThrow(new MailSendException("Some error occurred during mail send")).when(javaMailSender).send(any(MimeMessage.class)); + assertThatNoException().isThrownBy(() -> mailService.sendEmail(student1, subject, content, false, true)); + } + + /** + * When the javaMailSender returns an exception, that exception should be caught and should not be thrown instead. */ @Test - void testThrowException() { - doThrow(new org.springframework.mail.MailSendException("Some error occurred")).when(javaMailSender).send(any(MimeMessage.class)); - assertThatExceptionOfType(ArtemisMailException.class).isThrownBy(() -> mailService.sendEmail(student1, subject, content, false, true)); + void testNoExceptionThrown() { + doThrow(new RuntimeException("Some random error occurred")).when(javaMailSender).send(any(MimeMessage.class)); + var notification = new GroupNotification(null, NotificationConstants.NEW_ANNOUNCEMENT_POST_TITLE, NotificationConstants.NEW_ANNOUNCEMENT_POST_TEXT, false, new String[0], + student1, GroupNotificationType.STUDENT); + Post post = new Post(); + post.setAuthor(student1); + post.setCreationDate(ZonedDateTime.now()); + post.setVisibleForStudents(true); + post.setContent("hi test"); + post.setTitle("announcement"); + + Course course = new Course(); + course.setId(141L); + Channel channel = new Channel(); + channel.setCourse(course); + post.setConversation(channel); + assertThatNoException().isThrownBy(() -> mailService.sendNotification(notification, Set.of(student1, student2), post)); } } diff --git a/src/test/java/de/tum/in/www1/artemis/service/notifications/push_notifications/AppleFirebasePushNotificationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/notifications/push_notifications/AppleFirebasePushNotificationServiceTest.java index b0da4e33550a..c52848ac92ae 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/notifications/push_notifications/AppleFirebasePushNotificationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/notifications/push_notifications/AppleFirebasePushNotificationServiceTest.java @@ -14,6 +14,7 @@ import java.util.HexFormat; import java.util.Optional; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -51,9 +52,11 @@ class AppleFirebasePushNotificationServiceTest { private User student; + private AutoCloseable closeable; + @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); + closeable = MockitoAnnotations.openMocks(this); student = new User(); student.setId(1L); @@ -79,6 +82,13 @@ void setUp() { ReflectionTestUtils.setField(firebasePushNotificationService, "relayServerBaseUrl", Optional.of("test")); } + @AfterEach + void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + } + @Test void sendNotificationRequestsToEndpoint_shouldSendNotifications() throws InterruptedException { // Given diff --git a/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java index cf8ea78b1758..99ea095a0d5c 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationServiceTest.java @@ -22,8 +22,8 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.domain.enumeration.StaticCodeAnalysisTool; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTestCaseType; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseFactory; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseFactory; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.service.dto.AbstractBuildResultNotificationDTO; diff --git a/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java b/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java index 4789b09607f9..eb8f9f093ea5 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/scheduled/cache/quiz/QuizCacheTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -32,13 +33,13 @@ import de.tum.in.www1.artemis.domain.quiz.QuizBatch; import de.tum.in.www1.artemis.domain.quiz.QuizExercise; import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseFactory; -import de.tum.in.www1.artemis.exercise.quizexercise.QuizExerciseUtilService; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseFactory; +import de.tum.in.www1.artemis.exercise.quiz.QuizExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.QuizExerciseRepository; import de.tum.in.www1.artemis.repository.QuizSubmissionRepository; -import de.tum.in.www1.artemis.service.QuizBatchService; -import de.tum.in.www1.artemis.service.QuizExerciseService; +import de.tum.in.www1.artemis.service.quiz.QuizBatchService; +import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.web.websocket.QuizSubmissionWebsocketService; @@ -48,6 +49,7 @@ class QuizCacheTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "quizcachetest"; @Autowired + @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @Autowired diff --git a/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java index 7631d2590f38..91c3ef97b27e 100644 --- a/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/team/TeamIntegrationTest.java @@ -31,8 +31,8 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java index febe53d52064..844243c3e935 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/AssessmentEventIntegrationTest.java @@ -18,8 +18,8 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.analytics.TextAssessmentEvent; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.TextAssessmentEventRepository; import de.tum.in.www1.artemis.repository.TextSubmissionRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java index 9ab55deb6d0a..f181f0d916c9 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java @@ -62,10 +62,10 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseFactory; -import de.tum.in.www1.artemis.exercise.fileuploadexercise.FileUploadExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseFactory; +import de.tum.in.www1.artemis.exercise.fileupload.FileUploadExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ComplaintRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java index 84bfbb32b387..26e089a5c4c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java @@ -55,8 +55,8 @@ import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.exercise.GradingCriterionUtil; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.plagiarism.PlagiarismUtilService; diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java index f6e767339eb1..5ac0bd41e7b5 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextSubmissionIntegrationTest.java @@ -38,8 +38,8 @@ import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingSubmissionElement; import de.tum.in.www1.artemis.domain.plagiarism.text.TextSubmissionElement; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupFreePeriodIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupFreePeriodIntegrationTest.java index 5cffcb7407c3..ba646afd1f29 100644 --- a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupFreePeriodIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupFreePeriodIntegrationTest.java @@ -270,7 +270,7 @@ void create_overlapsWithSessionAlreadyCancelledDueToFreePeriod_shouldNotUpdateTu List<TutorialGroupSession> sessions = this.getTutorialGroupSessionsAscending(exampleTutorialGroupId); TutorialGroupSession firstMondayOfAugustSession = sessions.get(0); assertIndividualSessionIsCancelledOnDate(firstMondayOfAugustSession, FIRST_AUGUST_MONDAY_00_00, exampleTutorialGroupId, null); - assert (firstMondayOfAugustSession.getTutorialGroupFreePeriod().getId()).equals(createdPeriod.getId()); + assertThat(firstMondayOfAugustSession.getTutorialGroupFreePeriod().getId()).isEqualTo(createdPeriod.getId()); assertTutorialGroupFreePeriodCreatedCorrectlyFromDTO(firstMondayOfAugustSession.getTutorialGroupFreePeriod(), dto); // cleanup @@ -301,7 +301,7 @@ void delete_cancelledSessionStillOverlapsWithAnotherFreePeriod_shouldNotActivate // then firstMondayOfAugustSession = tutorialGroupSessionRepository.findByIdElseThrow(firstMondayOfAugustSession.getId()); assertIndividualSessionIsCancelledOnDate(firstMondayOfAugustSession, FIRST_AUGUST_MONDAY_00_00, exampleTutorialGroupId, null); - assert (firstMondayOfAugustSession.getTutorialGroupFreePeriod().getId()).equals(createdPeriod2.getId()); + assertThat(firstMondayOfAugustSession.getTutorialGroupFreePeriod().getId()).isEqualTo(createdPeriod2.getId()); // cleanup tutorialGroupSessionRepository.deleteById(firstMondayOfAugustSession.getId()); diff --git a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupSessionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupSessionIntegrationTest.java index cc02aaa5ec99..c5014427fbb7 100644 --- a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupSessionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupSessionIntegrationTest.java @@ -180,7 +180,7 @@ void updateAttendanceCount_asTutor_shouldUpdateAttendanceCount() throws Exceptio // when request.patchWithResponseBody(getSessionsPathOfTutorialGroup(exampleTutorialGroupId, session.getId()) + "/attendance-count", null, TutorialGroupSession.class, - HttpStatus.OK).getId(); + HttpStatus.OK); updatedSession = tutorialGroupSessionRepository.findByIdElseThrow(updatedSessionId); assertThat(updatedSession.getAttendanceCount()).isNull(); diff --git a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupsConfigurationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupsConfigurationIntegrationTest.java index 72c0ea7b08ee..b540916b3844 100644 --- a/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupsConfigurationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/tutorialgroups/TutorialGroupsConfigurationIntegrationTest.java @@ -20,7 +20,7 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.TutorialGroupSessionStatus; import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroupsConfiguration; -import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; +import de.tum.in.www1.artemis.exercise.text.TextExerciseFactory; import de.tum.in.www1.artemis.user.UserFactory; class TutorialGroupsConfigurationIntegrationTest extends AbstractTutorialGroupIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java b/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java index 5ac53027a4da..685895edb315 100644 --- a/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/user/UserTestService.java @@ -26,8 +26,8 @@ import de.tum.in.www1.artemis.domain.Authority; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.exercise.programmingexercise.MockDelegate; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.MockDelegate; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.AuthorityRepository; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.UserRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/util/HestiaUtilTestService.java b/src/test/java/de/tum/in/www1/artemis/util/HestiaUtilTestService.java index 28406a5d955a..e8f40ae6e45f 100644 --- a/src/test/java/de/tum/in/www1/artemis/util/HestiaUtilTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/util/HestiaUtilTestService.java @@ -22,7 +22,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.Repository; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; diff --git a/src/test/java/de/tum/in/www1/artemis/util/IrisUtilTestService.java b/src/test/java/de/tum/in/www1/artemis/util/IrisUtilTestService.java index 43b6452dee41..eb49aab3c586 100644 --- a/src/test/java/de/tum/in/www1/artemis/util/IrisUtilTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/util/IrisUtilTestService.java @@ -10,7 +10,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; -import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.programming.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionTestRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; diff --git a/src/test/javascript/spec/component/admin/lti-configuration.component.spec.ts b/src/test/javascript/spec/component/admin/lti-configuration.component.spec.ts index 4f4668c70318..b54d9a90ac6a 100644 --- a/src/test/javascript/spec/component/admin/lti-configuration.component.spec.ts +++ b/src/test/javascript/spec/component/admin/lti-configuration.component.spec.ts @@ -3,7 +3,6 @@ import { LtiConfigurationService } from 'app/admin/lti-configuration/lti-configu import { Router } from '@angular/router'; import { SortService } from 'app/shared/service/sort.service'; import { LtiConfigurationComponent } from 'app/admin/lti-configuration/lti-configuration.component'; -import { MockLtiConfigurationService } from '../../helpers/mocks/service/mock-lti-configuration-service'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; @@ -17,18 +16,45 @@ import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-rout import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { ActivatedRoute } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { MockAlertService } from '../../helpers/mocks/service/mock-alert.service'; describe('LtiConfigurationComponent', () => { let component: LtiConfigurationComponent; let fixture: ComponentFixture<LtiConfigurationComponent>; let mockRouter: any; + let mockActivatedRoute: any; let mockSortService: any; - let ltiConfigurationService: LtiConfigurationService; + let mockLtiConfigurationService: any; + let mockAlertService: AlertService; beforeEach(async () => { mockRouter = { navigate: jest.fn() }; mockSortService = { sortByProperty: jest.fn() }; - + mockActivatedRoute = { + data: of({ defaultSort: 'id,desc' }), + queryParamMap: of( + new Map([ + ['page', '1'], + ['sort', 'id,asc'], + ]), + ), + }; + mockLtiConfigurationService = { + query: jest.fn().mockReturnValue( + of( + new HttpResponse({ + body: [{ id: 1, registrationId: 'platform-1' }], + headers: new HttpHeaders({ 'X-Total-Count': '1' }), + }), + ), + ), + deleteLtiPlatform: jest.fn().mockReturnValue(of({})), + }; await TestBed.configureTestingModule({ imports: [NgbNavModule, FontAwesomeModule], declarations: [ @@ -42,18 +68,21 @@ describe('LtiConfigurationComponent', () => { MockRouterLinkDirective, ], providers: [ - { provide: LtiConfigurationService, useClass: MockLtiConfigurationService }, { provide: Router, useValue: mockRouter }, { provide: SortService, useValue: mockSortService }, { provide: TranslateService, useClass: MockTranslateService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: LtiConfigurationService, useValue: mockLtiConfigurationService }, + { provide: AlertService, useClass: MockAlertService }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(LtiConfigurationComponent); component = fixture.componentInstance; - ltiConfigurationService = TestBed.inject(LtiConfigurationService); fixture.detectChanges(); component.predicate = 'id'; + mockAlertService = fixture.debugElement.injector.get(AlertService); }); it('should create', () => { @@ -62,7 +91,11 @@ describe('LtiConfigurationComponent', () => { it('should initialize and load LTI platforms', () => { expect(component.platforms).toBeDefined(); - expect(component.platforms).toHaveLength(2); + expect(component.page).toBe(1); + expect(component.predicate).toBe('id'); + expect(component.ascending).toBeTrue(); + expect(mockLtiConfigurationService.query).toHaveBeenCalled(); + expect(component.platforms).toHaveLength(1); }); it('should generate URLs correctly', () => { @@ -111,11 +144,26 @@ describe('LtiConfigurationComponent', () => { it('should delete an LTI platform and navigate', () => { const platformIdToDelete = 1; - const deleteSpy = jest.spyOn(ltiConfigurationService, 'deleteLtiPlatform'); - component.deleteLtiPlatform(platformIdToDelete); - - expect(deleteSpy).toHaveBeenCalledWith(platformIdToDelete); + expect(mockLtiConfigurationService.deleteLtiPlatform).toHaveBeenCalledWith(platformIdToDelete); expect(mockRouter.navigate).toHaveBeenCalledWith(['admin', 'lti-configuration']); }); + + it('should handle navigation on transition', () => { + component.transition(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/admin/lti-configuration'], { + queryParams: { + page: component.page, + sort: 'id,asc', + }, + }); + }); + + it('should handle errors on deleting LTI platform', () => { + const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error', error: { message: 'Error occurred' } }); + const errorSpy = jest.spyOn(mockAlertService, 'error'); + mockLtiConfigurationService.deleteLtiPlatform.mockReturnValue(throwError(() => errorResponse)); + component.deleteLtiPlatform(123); + expect(errorSpy).toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/component/assessment-dashboard/exercise-assessment-dashboard.component.spec.ts b/src/test/javascript/spec/component/assessment-dashboard/exercise-assessment-dashboard.component.spec.ts index 6c16a0d608b5..f8bdd14321c4 100644 --- a/src/test/javascript/spec/component/assessment-dashboard/exercise-assessment-dashboard.component.spec.ts +++ b/src/test/javascript/spec/component/assessment-dashboard/exercise-assessment-dashboard.component.spec.ts @@ -593,7 +593,7 @@ describe('ExerciseAssessmentDashboardComponent', () => { function initComponent() { comp.exercise = { - allowManualFeedbackRequests: false, + allowFeedbackRequests: false, type: fakeExerciseType, numberOfAssessmentsOfCorrectionRounds: [], studentAssignedTeamIdComputed: false, diff --git a/src/test/javascript/spec/component/course/course-overview.service.spec.ts b/src/test/javascript/spec/component/course/course-overview.service.spec.ts index 06b79be50a53..389c3af10bab 100644 --- a/src/test/javascript/spec/component/course/course-overview.service.spec.ts +++ b/src/test/javascript/spec/component/course/course-overview.service.spec.ts @@ -111,17 +111,18 @@ describe('CourseOverviewService', () => { it('should group lectures by start date and map to sidebar card elements', () => { const sortedLectures = [futureLecture, pastLecture, currentLecture]; - jest.spyOn(service, 'getCorrespondingGroupByDate'); + jest.spyOn(service, 'getCorrespondingLectureGroupByDate'); jest.spyOn(service, 'mapLectureToSidebarCardElement'); const groupedLectures = service.groupLecturesByStartDate(sortedLectures); expect(groupedLectures['current'].entityData).toHaveLength(1); - expect(groupedLectures['past'].entityData).toHaveLength(2); + expect(groupedLectures['past'].entityData).toHaveLength(1); + expect(groupedLectures['future'].entityData).toHaveLength(1); expect(service.mapLectureToSidebarCardElement).toHaveBeenCalledTimes(3); - expect(groupedLectures['current'].entityData[0].title).toBe('Advanced Topics in Computer Science'); + expect(groupedLectures['future'].entityData[0].title).toBe('Advanced Topics in Computer Science'); expect(groupedLectures['past'].entityData[0].title).toBe('Introduction to Computer Science'); - expect(groupedLectures['past'].entityData[1].title).toBe('Algorithms'); + expect(groupedLectures['current'].entityData[0].title).toBe('Algorithms'); }); it('should return undefined if lectures array is undefined', () => { @@ -156,7 +157,7 @@ describe('CourseOverviewService', () => { it('should group exercises by start date and map to sidebar card elements', () => { const sortedExercises = [futureExercise, pastExercise, currentExercise]; - jest.spyOn(service, 'getCorrespondingGroupByDate'); + jest.spyOn(service, 'getCorrespondingExerciseGroupByDate'); jest.spyOn(service, 'mapExerciseToSidebarCardElement'); const groupedExercises = service.groupExercisesByDueDate(sortedExercises); @@ -178,7 +179,7 @@ describe('CourseOverviewService', () => { ]; const sortedExercises = service.sortExercises(pastExercises); - jest.spyOn(service, 'getCorrespondingGroupByDate'); + jest.spyOn(service, 'getCorrespondingExerciseGroupByDate'); jest.spyOn(service, 'mapExerciseToSidebarCardElement'); const groupedExercises = service.groupExercisesByDueDate(sortedExercises); diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index b919ecdd60c7..be63776c0a42 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -400,6 +400,19 @@ describe('Course Management Update Component', () => { comp.updateCourseInformationSharingMessagingCodeOfConduct('# Code of Conduct'); expect(comp.courseForm.controls['courseInformationSharingMessagingCodeOfConduct'].value).toBe('# Code of Conduct'); }); + + it('should update course information sharing code of conduct when communication is enabled and messaging disabled', () => { + comp.communicationEnabled = true; + comp.messagingEnabled = false; + comp.course = new Course(); + comp.courseForm = new FormGroup({ + courseInformationSharingMessagingCodeOfConduct: new FormControl(), + }); + comp.updateCourseInformationSharingMessagingCodeOfConduct('# Code of Conduct'); + expect(comp.courseForm.controls['courseInformationSharingMessagingCodeOfConduct'].value).toBe('# Code of Conduct'); + // Verify the form control is editable + expect(comp.courseForm.controls['courseInformationSharingMessagingCodeOfConduct'].enabled).toBeTrue(); + }); }); describe('changeComplaintsEnabled', () => { diff --git a/src/test/javascript/spec/component/course/edit-course-lti-configuration.component.spec.ts b/src/test/javascript/spec/component/course/edit-course-lti-configuration.component.spec.ts index f344d9046bc0..1247723d25fa 100644 --- a/src/test/javascript/spec/component/course/edit-course-lti-configuration.component.spec.ts +++ b/src/test/javascript/spec/component/course/edit-course-lti-configuration.component.spec.ts @@ -1,4 +1,4 @@ -import { HttpResponse } from '@angular/common/http'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; @@ -20,11 +20,13 @@ import { ArtemisTestModule } from '../../test.module'; import { regexValidator } from 'app/shared/form/shortname-validator.directive'; import { LOGIN_PATTERN } from 'app/shared/constants/input.constants'; import { MockHasAnyAuthorityDirective } from '../../helpers/mocks/directive/mock-has-any-authority.directive'; +import { LtiConfigurationService } from 'app/admin/lti-configuration/lti-configuration.service'; describe('Edit Course LTI Configuration Component', () => { let comp: EditCourseLtiConfigurationComponent; let fixture: ComponentFixture<EditCourseLtiConfigurationComponent>; let courseService: CourseManagementService; + let ltiConfigService: { query: any }; const router = new MockRouter(); @@ -41,6 +43,17 @@ describe('Edit Course LTI Configuration Component', () => { } as Course; beforeEach(() => { + ltiConfigService = { + query: jest.fn().mockReturnValue( + of( + new HttpResponse({ + body: [], + headers: new HttpHeaders({ 'X-Total-Count': '0' }), + }), + ), + ), + }; + TestBed.configureTestingModule({ imports: [ArtemisTestModule, NgbNavModule, MockModule(ReactiveFormsModule)], declarations: [ @@ -63,6 +76,7 @@ describe('Edit Course LTI Configuration Component', () => { }, {}, ), + { provide: LtiConfigurationService, useValue: ltiConfigService }, ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/exercises/shared/result.spec.ts b/src/test/javascript/spec/component/exercises/shared/result.spec.ts index df50f02be73a..65d663a7dd12 100644 --- a/src/test/javascript/spec/component/exercises/shared/result.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/result.spec.ts @@ -81,6 +81,7 @@ describe('ResultComponent', () => { const participation1 = cloneDeep(programmingParticipation); participation1.results = [result1, result2]; component.participation = participation1; + component.showUngradedResults = true; fixture.detectChanges(); @@ -99,6 +100,7 @@ describe('ResultComponent', () => { const participation1 = cloneDeep(modelingParticipation); participation1.results = [result1, result2]; component.participation = participation1; + component.showUngradedResults = true; fixture.detectChanges(); diff --git a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel-title.component.spec.ts b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel-title.component.spec.ts new file mode 100644 index 000000000000..3dc633eb4b50 --- /dev/null +++ b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel-title.component.spec.ts @@ -0,0 +1,55 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { GitDiffFilePanelTitleComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component'; + +describe('GitDiffFilePanelTitleComponent', () => { + let comp: GitDiffFilePanelTitleComponent; + let fixture: ComponentFixture<GitDiffFilePanelTitleComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [], + providers: [], + }).compileComponents(); + fixture = TestBed.createComponent(GitDiffFilePanelTitleComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([ + { + filePath: 'some-unchanged-file.java', + previousFilePath: 'some-unchanged-file.java', + status: 'unchanged', + title: 'some-unchanged-file.java', + }, + { + filePath: undefined, + previousFilePath: 'some-deleted-file.java', + status: 'deleted', + title: 'some-deleted-file.java', + }, + { + filePath: 'some-created-file.java', + previousFilePath: undefined, + status: 'created', + title: 'some-created-file.java', + }, + { + filePath: 'some-renamed-file.java', + previousFilePath: 'some-file.java', + status: 'renamed', + title: 'some-file.java → some-renamed-file.java', + }, + ])('should correctly set title and status', ({ filePath, previousFilePath, status, title }) => { + comp.previousFilePath = previousFilePath; + comp.filePath = filePath; + fixture.detectChanges(); + expect(comp.title).toBe(title); + expect(comp.fileStatus).toBe(status); + }); +}); diff --git a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel.component.spec.ts b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel.component.spec.ts index a1668c85b8da..d4938caea953 100644 --- a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel.component.spec.ts +++ b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file-panel.component.spec.ts @@ -8,6 +8,7 @@ import { GitDiffFilePanelComponent } from 'app/exercises/programming/hestia/git- import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; import { NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem } from '@ng-bootstrap/ng-bootstrap'; import { GitDiffFileComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file.component'; +import { GitDiffFilePanelTitleComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component'; describe('ProgrammingExerciseGitDiffFilePanel Component', () => { let comp: GitDiffFilePanelComponent; @@ -19,6 +20,7 @@ describe('ProgrammingExerciseGitDiffFilePanel Component', () => { declarations: [ GitDiffFilePanelComponent, MockPipe(ArtemisTranslatePipe), + MockComponent(GitDiffFilePanelTitleComponent), MockComponent(GitDiffLineStatComponent), MockComponent(GitDiffFileComponent), MockDirective(DeleteButtonDirective), diff --git a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file.component.spec.ts b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file.component.spec.ts index a668b21d73ad..dbef6bd7eb72 100644 --- a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file.component.spec.ts +++ b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-file.component.spec.ts @@ -1,143 +1,30 @@ import { ArtemisTestModule } from '../../../test.module'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AceEditorComponent } from 'app/shared/markdown-editor/ace-editor/ace-editor.component'; import { GitDiffFileComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file.component'; -import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; -import * as ace from 'brace'; -import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-editor.module'; -import { CodeEditorAceComponent } from 'app/exercises/programming/shared/code-editor/ace/code-editor-ace.component'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; -function createDiffEntry( - id: number, - previousFilePath: string | undefined, - filePath: string | undefined, - previousStartLine: number | undefined, - startLine: number | undefined, - previousLineCount: number | undefined, - lineCount: number | undefined, -): ProgrammingExerciseGitDiffEntry { - const diffEntry = new ProgrammingExerciseGitDiffEntry(); - diffEntry.id = id; - diffEntry.previousFilePath = previousFilePath; - diffEntry.filePath = filePath; - diffEntry.previousStartLine = previousStartLine; - diffEntry.startLine = startLine; - diffEntry.previousLineCount = previousLineCount; - diffEntry.lineCount = lineCount; - return diffEntry; -} - -function createFileContent(lineCount: number): string { - return new Array(lineCount) - .fill('') - .map((_, index) => `Line ${index + 1}`) - .join('\n'); -} - -function insertEmptyLines(fileContent: string, startLine: number, lineCount: number): string { - const lines = fileContent.split('\n'); - const insertIndex = startLine - 1; - const insertLines = new Array(lineCount).fill(''); - return [...lines.slice(0, insertIndex), ...insertLines, ...lines.slice(insertIndex)].join('\n'); +function getDiffEntryWithPaths(previousFilePath?: string, filePath?: string) { + return { + previousFilePath, + filePath, + }; } -describe('ProgrammingExerciseGitDiffEntry Component', () => { - ace.acequire('ace/ext/modelist'); +describe('GitDiffFileComponent', () => { let comp: GitDiffFileComponent; let fixture: ComponentFixture<GitDiffFileComponent>; - const singleAddition = { - name: 'single-addition', - diffEntries: [createDiffEntry(1, 'file1', 'file1', undefined, 4, undefined, 4)], - templateFileContent: createFileContent(10), - solutionFileContent: createFileContent(14), - expectedTemplateFileContent: insertEmptyLines(createFileContent(10), 4, 4), - expectedSolutionFileContent: createFileContent(14), - actualStartLineWithoutOffset: 4, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['placeholder', 'placeholder', 'placeholder', 'placeholder'], - coloringSolution: ['addition', 'addition', 'addition', 'addition'], - gutterWidthTemplate: 1, - gutterWidthSolution: 2, - }; - const singleDeletion = { - name: 'single-deletion', - diffEntries: [createDiffEntry(1, 'file1', 'file1', 4, undefined, 4, undefined)], - templateFileContent: createFileContent(14), - solutionFileContent: createFileContent(10), - expectedTemplateFileContent: createFileContent(14), - expectedSolutionFileContent: insertEmptyLines(createFileContent(10), 4, 4), - actualStartLineWithoutOffset: 4, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['deletion', 'deletion', 'deletion', 'deletion'], - coloringSolution: ['placeholder', 'placeholder', 'placeholder', 'placeholder'], - gutterWidthTemplate: 2, - gutterWidthSolution: 1, - }; - const singleChangeMoreAdded = { - name: 'single-change-more-added', - diffEntries: [createDiffEntry(1, 'file1', 'file1', 4, 4, 2, 4)], - templateFileContent: createFileContent(10), - solutionFileContent: createFileContent(12), - expectedTemplateFileContent: insertEmptyLines(createFileContent(10), 6, 2), - expectedSolutionFileContent: createFileContent(12), - actualStartLineWithoutOffset: 4, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['deletion', 'deletion', 'placeholder', 'placeholder'], - coloringSolution: ['addition', 'addition', 'addition', 'addition'], - gutterWidthTemplate: 1, - gutterWidthSolution: 2, - }; - const singleChangeMoreDeleted = { - name: 'single-change-more-deleted', - diffEntries: [createDiffEntry(1, 'file1', 'file1', 4, 4, 4, 2)], - templateFileContent: createFileContent(12), - solutionFileContent: createFileContent(10), - expectedTemplateFileContent: createFileContent(12), - expectedSolutionFileContent: insertEmptyLines(createFileContent(10), 6, 2), - actualStartLineWithoutOffset: 4, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['deletion', 'deletion', 'deletion', 'deletion'], - coloringSolution: ['addition', 'addition', 'placeholder', 'placeholder'], - gutterWidthTemplate: 2, - gutterWidthSolution: 1, - }; - const singleChangeEqual = { - name: 'single-change-equal', - diffEntries: [createDiffEntry(1, 'file1', 'file1', 4, 4, 4, 4)], - templateFileContent: createFileContent(10) + '\n', - solutionFileContent: createFileContent(10) + '\n', - expectedTemplateFileContent: createFileContent(10), - expectedSolutionFileContent: createFileContent(10), - actualStartLineWithoutOffset: 4, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['deletion', 'deletion', 'deletion', 'deletion'], - coloringSolution: ['addition', 'addition', 'addition', 'addition'], - gutterWidthTemplate: 2, - gutterWidthSolution: 2, - }; - const multipleChanges = { - name: 'multiple-changes', - diffEntries: [createDiffEntry(1, 'file1', 'file1', 1, 1, 1, 2), createDiffEntry(2, 'file1', 'file1', 3, 4, 2, 1), createDiffEntry(3, 'file1', 'file1', 6, 6, 1, 1)], - templateFileContent: 'Line 1 (Changed A)\nLine 3\nLine 4 (Changed A)\nLine 5 (Removed)\nLine 6\nLine 7 (Changed A)\nLine 8\nLine 9\n', - solutionFileContent: 'Line 1 (Changed B)\nLine 2 (Added)\nLine 3\nLine 4 (Changed B)\nLine 6\nLine 7 (Changed B)\nLine 8\nLine 9\n', - expectedTemplateFileContent: 'Line 1 (Changed A)\n\nLine 3\nLine 4 (Changed A)\nLine 5 (Removed)\nLine 6\nLine 7 (Changed A)\nLine 8\nLine 9', - expectedSolutionFileContent: 'Line 1 (Changed B)\nLine 2 (Added)\nLine 3\nLine 4 (Changed B)\n\nLine 6\nLine 7 (Changed B)\nLine 8\nLine 9', - actualStartLineWithoutOffset: 1, - actualEndLineWithoutOffset: 8, - coloringTemplate: ['deletion', 'placeholder', undefined, 'deletion', 'deletion', undefined, 'deletion'], - coloringSolution: ['addition', 'addition', undefined, 'addition', 'placeholder', undefined, 'addition'], - gutterWidthTemplate: 1, - gutterWidthSolution: 1, - }; - - const allCases = [singleAddition, singleDeletion, singleChangeMoreAdded, singleChangeMoreDeleted, singleChangeEqual, multipleChanges]; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule, AceEditorModule], - declarations: [GitDiffFileComponent, AceEditorComponent, CodeEditorAceComponent], + imports: [ArtemisTestModule, MonacoEditorModule], + declarations: [GitDiffFileComponent], providers: [], }).compileComponents(); + // Required because Monaco uses the ResizeObserver for the diff editor. + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); fixture = TestBed.createComponent(GitDiffFileComponent); comp = fixture.componentInstance; }); @@ -146,85 +33,28 @@ describe('ProgrammingExerciseGitDiffEntry Component', () => { jest.restoreAllMocks(); }); - it.each(allCases)('Check that the editor lines were created correctly for: $name', (testData) => { - const diffEntriesCopy = testData.diffEntries.map((entry) => ({ ...entry })); - comp.diffEntries = testData.diffEntries; - comp.templateFileContent = testData.templateFileContent; - comp.solutionFileContent = testData.solutionFileContent; - comp.ngOnInit(); - - // Check that the original diff entries were not modified - expect(testData.diffEntries).toEqual(diffEntriesCopy); - // Check that the empty lines were inserted correctly - expect(comp.templateLines).toHaveLength(comp.solutionLines.length); - expect(comp.templateLines.map((line) => line ?? '').join('\n')).toEqual(testData.expectedTemplateFileContent); - expect(comp.solutionLines.map((line) => line ?? '').join('\n')).toEqual(testData.expectedSolutionFileContent); - - // Check that the actual start and end lines were calculated correctly - expect(comp.actualStartLine).toEqual(Math.max(0, testData.actualStartLineWithoutOffset - 1 - 3)); - expect(comp.actualEndLine).toEqual(Math.min(comp.templateLines.length, testData.actualEndLineWithoutOffset - 1 + 3)); - - // Check that the editor lines were set correctly - expect(comp.editorPrevious.getEditor().getSession().getValue().split('\n')).toHaveLength(comp.actualEndLine - comp.actualStartLine); - expect(comp.editorPrevious.getEditor().getSession().getValue().split('\n')).toEqual( - comp.templateLines.slice(comp.actualStartLine, comp.actualEndLine).map((line) => line ?? ''), - ); - expect(comp.editorNow.getEditor().getSession().getValue().split('\n')).toHaveLength(comp.actualEndLine - comp.actualStartLine); - expect(comp.editorNow.getEditor().getSession().getValue().split('\n')).toEqual( - comp.solutionLines.slice(comp.actualStartLine, comp.actualEndLine).map((line) => line ?? ''), - ); - - // Check that the editor lines were colored correctly - const contextLines = new Array(testData.actualStartLineWithoutOffset - comp.actualStartLine - 1).fill(undefined); - const expectedDecorationsTemplate = [ - ...contextLines, - ...testData.coloringTemplate.map((type) => { - if (type === 'addition') { - return ' added-line-gutter'; - } else if (type === 'deletion') { - return ' removed-line-gutter'; - } else if (type === 'placeholder') { - return ' placeholder-line-gutter'; - } - }), - ]; - const expectedDecorationsSolution = [ - ...contextLines, - ...testData.coloringSolution.map((type) => { - if (type === 'addition') { - return ' added-line-gutter'; - } else if (type === 'deletion') { - return ' removed-line-gutter'; - } else if (type === 'placeholder') { - return ' placeholder-line-gutter'; - } - }), - ]; - expect( - comp.editorPrevious - .getEditor() - .getSession() - .$decorations.map((s: string) => s.replace(' removed-line-gutter removed-line-gutter', ' removed-line-gutter')) - .map((s: string) => s.replace(' placeholder-line-gutter placeholder-line-gutter', ' placeholder-line-gutter')), - ).toEqual(expectedDecorationsTemplate); - expect( - comp.editorNow - .getEditor() - .getSession() - .$decorations.map((s: string) => s.replace(' added-line-gutter added-line-gutter', ' added-line-gutter')) - .map((s: string) => s.replace(' placeholder-line-gutter placeholder-line-gutter', ' placeholder-line-gutter')), - ).toEqual(expectedDecorationsSolution); + it.each([ + getDiffEntryWithPaths('same file', 'same file'), + getDiffEntryWithPaths('old file', 'renamed file'), + getDiffEntryWithPaths('deleted file', undefined), + getDiffEntryWithPaths(undefined, 'created file'), + ])('should infer file paths from the diff entries', (entry) => { + comp.diffEntries = [entry]; + fixture.detectChanges(); + expect(comp.modifiedFilePath).toBe(entry.filePath); + expect(comp.originalFilePath).toBe(entry.previousFilePath); + }); - // Check that the gutter renderer works correctly - const gutterRendererPrevious = comp.editorPrevious.getEditor().getSession().gutterRenderer; - const gutterRendererNow = comp.editorNow.getEditor().getSession().gutterRenderer; - let currentTemplateLine = comp.actualStartLine + 1; - let currentSolutionLine = comp.actualStartLine + 1; - for (let i = comp.actualStartLine; i < comp.actualEndLine; i++) { - expect(gutterRendererPrevious.getText(0, i - comp.actualStartLine)).toBe(comp.templateLines[i] ? currentTemplateLine++ : ''); - expect(gutterRendererNow.getText(0, i - comp.actualStartLine)).toBe(comp.solutionLines[i] ? currentSolutionLine++ : ''); - } - expect(gutterRendererPrevious.getWidth(null, comp.actualEndLine, { characterWidth: 1 })).toBe(testData.gutterWidthTemplate); - expect(gutterRendererNow.getWidth(null, comp.actualEndLine, { characterWidth: 1 })).toBe(testData.gutterWidthSolution); + it('should initialize the content of the diff editor', () => { + const fileName = 'some-changed-file.java'; + const originalContent = 'some file content'; + const modifiedContent = 'some changed file content'; + const setFileContentsStub = jest.spyOn(comp.monacoDiffEditor, 'setFileContents').mockImplementation(); + const diffEntry = getDiffEntryWithPaths(fileName, fileName); + comp.originalFileContent = originalContent; + comp.modifiedFileContent = modifiedContent; + comp.diffEntries = [diffEntry]; + fixture.detectChanges(); + expect(setFileContentsStub).toHaveBeenCalledExactlyOnceWith(originalContent, fileName, modifiedContent, fileName); }); }); diff --git a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-report.component.spec.ts b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-report.component.spec.ts index d489c7240205..41c61bb03b8b 100644 --- a/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-report.component.spec.ts +++ b/src/test/javascript/spec/component/hestia/git-diff-report/git-diff-report.component.spec.ts @@ -6,6 +6,9 @@ import { GitDiffLineStatComponent } from 'app/exercises/programming/hestia/git-d import { GitDiffReportComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-report.component'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; +import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { GitDiffFilePanelComponent } from 'app/exercises/programming/hestia/git-diff-report/git-diff-file-panel.component'; +import { ButtonComponent } from 'app/shared/components/button.component'; describe('ProgrammingExerciseGitDiffReport Component', () => { let comp: GitDiffReportComponent; @@ -13,8 +16,14 @@ describe('ProgrammingExerciseGitDiffReport Component', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule], - declarations: [GitDiffReportComponent, MockPipe(ArtemisTranslatePipe), MockComponent(GitDiffLineStatComponent)], + imports: [ArtemisTestModule, NgbTooltipMocksModule], + declarations: [ + GitDiffReportComponent, + MockComponent(ButtonComponent), + MockPipe(ArtemisTranslatePipe), + MockComponent(GitDiffFilePanelComponent), + MockComponent(GitDiffLineStatComponent), + ], providers: [], }).compileComponents(); fixture = TestBed.createComponent(GitDiffReportComponent); @@ -140,4 +149,37 @@ describe('ProgrammingExerciseGitDiffReport Component', () => { expect(comp.addedLineCount).toBe(0); expect(comp.removedLineCount).toBe(1); }); + + it('should record for each path whether the diff is ready', () => { + const filePath1 = 'src/a.java'; + const filePath2 = 'src/b.java'; + const entries: ProgrammingExerciseGitDiffEntry[] = [ + { filePath: 'src/a.java', previousStartLine: 3 }, + { filePath: 'src/b.java', startLine: 1 }, + { filePath: 'src/a.java', startLine: 2 }, + { filePath: 'src/a.java', previousStartLine: 4 }, + { filePath: 'src/b.java', startLine: 2 }, + { filePath: 'src/a.java', startLine: 1 }, + ]; + comp.solutionFileContentByPath = new Map<string, string>(); + comp.solutionFileContentByPath.set(filePath1, 'some file content'); + comp.solutionFileContentByPath.set(filePath2, 'some other file content'); + comp.templateFileContentByPath = comp.solutionFileContentByPath; + comp.report = { entries } as ProgrammingExerciseGitDiffReport; + fixture.detectChanges(); + // Initialization + expect(comp.allDiffsReady).toBeFalse(); + expect(comp.diffsReadyByPath[filePath1]).toBeFalse(); + expect(comp.diffsReadyByPath[filePath2]).toBeFalse(); + // First file ready + comp.onDiffReady(filePath1, true); + expect(comp.allDiffsReady).toBeFalse(); + expect(comp.diffsReadyByPath[filePath1]).toBeTrue(); + expect(comp.diffsReadyByPath[filePath2]).toBeFalse(); + // Second file ready + comp.onDiffReady(filePath2, true); + expect(comp.allDiffsReady).toBeTrue(); + expect(comp.diffsReadyByPath[filePath1]).toBeTrue(); + expect(comp.diffsReadyByPath[filePath2]).toBeTrue(); + }); }); diff --git a/src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts deleted file mode 100644 index e738583f2b4f..000000000000 --- a/src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { ArtemisTestModule } from '../../../test.module'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisChatSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { MockDirective } from 'ng-mocks'; -import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; -import { SimpleChange, SimpleChanges } from '@angular/core'; - -function baseSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; - const irisSubSettings = new IrisChatSubSettings(); - irisSubSettings.id = 2; - irisSubSettings.template = mockTemplate; - irisSubSettings.enabled = true; - return irisSubSettings; -} - -describe('IrisChatSubSettingsUpdateComponent Component', () => { - let comp: IrisChatSubSettingsUpdateComponent; - let fixture: ComponentFixture<IrisChatSubSettingsUpdateComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], - declarations: [IrisChatSubSettingsUpdateComponent], - }).compileComponents(); - fixture = TestBed.createComponent(IrisChatSubSettingsUpdateComponent); - comp = fixture.componentInstance; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('template is not optional', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeFalsy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional', () => { - comp.subSettings = baseSettings(); - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional and changes from defined to undefined', () => { - comp.subSettings = baseSettings(); - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - comp.onInheritTemplateChanged(); - fixture.detectChanges(); - expect(comp.subSettings.template).toBeUndefined(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional and changes from undefined to defined', () => { - const subSettings = baseSettings(); - subSettings.template = undefined; - comp.subSettings = subSettings; - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - comp.onInheritTemplateChanged(); - fixture.detectChanges(); - expect(comp.subSettings.template).toBeDefined(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template changes', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - comp.templateContent = 'Hello World 2'; - comp.onTemplateChanged(); - - expect(comp.subSettings.template?.content).toBe('Hello World 2'); - }); - - it('template created', () => { - comp.subSettings = baseSettings(); - comp.subSettings.template = undefined; - fixture.detectChanges(); - comp.templateContent = 'Hello World 2'; - comp.onTemplateChanged(); - - expect(comp.subSettings.template!.content).toBe('Hello World 2'); - }); - - it('sub settings changes', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - const newSubSettings = baseSettings(); - newSubSettings.template!.content = 'Hello World 2'; - - const changes: SimpleChanges = { - subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), - }; - comp.subSettings = newSubSettings; - comp.ngOnChanges(changes); - - expect(comp.templateContent).toBe('Hello World 2'); - }); -}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-competency-generation-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-competency-generation-sub-settings-update.component.spec.ts deleted file mode 100644 index 92c4be98f6bf..000000000000 --- a/src/test/javascript/spec/component/iris/settings/iris-competency-generation-sub-settings-update.component.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ArtemisTestModule } from '../../../test.module'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { MockDirective } from 'ng-mocks'; -import { SimpleChange, SimpleChanges } from '@angular/core'; -import { IrisCompetencyGenerationSubSettings, IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisCompetencyGenerationSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component'; -import { expectElementToBeDisabled, expectElementToBeEnabled } from '../../../helpers/utils/general.utils'; - -function baseSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; - const irisSubSettings = new IrisCompetencyGenerationSubSettings(); - irisSubSettings.id = 2; - irisSubSettings.template = mockTemplate; - irisSubSettings.enabled = true; - return irisSubSettings; -} - -/** - * gets the full id of an element (as they have the settings type as suffix) - * @param baseId - */ -function getId(baseId: string) { - return baseId + IrisSubSettingsType.COMPETENCY_GENERATION; -} - -describe('IrisCompetencyGenerationSubSettingsUpdateComponent', () => { - let component: IrisCompetencyGenerationSubSettingsUpdateComponent; - let fixture: ComponentFixture<IrisCompetencyGenerationSubSettingsUpdateComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], - }).compileComponents(); - fixture = TestBed.createComponent(IrisCompetencyGenerationSubSettingsUpdateComponent); - component = fixture.componentInstance; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should initialize', () => { - fixture.detectChanges(); - expect(component).toBeDefined(); - }); - - it('should show inherit template switch', () => { - component.subSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector(getId('#inheritTemplate'))).toBeNull(); - expect(fixture.debugElement.nativeElement.querySelector(getId('#template-editor'))).not.toBeNull(); - - component.parentSubSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector(getId('#inheritTemplate'))).not.toBeNull(); - expect(fixture.debugElement.nativeElement.querySelector(getId('#template-editor'))).not.toBeNull(); - }); - - it('should disabled template editing when it is inherited', () => { - component.subSettings = baseSettings(); - component.parentSubSettings = baseSettings(); - const initialTemplateContent = component.subSettings.template?.content; - fixture.detectChanges(); - - //switch inherit template on - fixture.debugElement.nativeElement.querySelector(getId('#inheritTemplate')).click(); - fixture.detectChanges(); - - expectElementToBeDisabled(fixture.debugElement.nativeElement.querySelector(getId('#template-editor'))); - expect(component.subSettings.template).toBeUndefined(); - - fixture.debugElement.nativeElement.querySelector(getId('#inheritTemplate')).click(); - fixture.detectChanges(); - - expectElementToBeEnabled(fixture.debugElement.nativeElement.querySelector(getId('#template-editor'))); - expect(component.subSettings.template?.content).toEqual(initialTemplateContent); - }); - - it('should register template changes', () => { - component.subSettings = baseSettings(); - fixture.detectChanges(); - component.templateContent = 'Hello World'; - component.onTemplateChanged(); - - expect(component.subSettings.template?.content).toBe('Hello World'); - }); - - it('should create template', () => { - component.subSettings = baseSettings(); - component.subSettings.template = undefined; - fixture.detectChanges(); - component.templateContent = 'Hello World'; - component.onTemplateChanged(); - - expect(component.subSettings.template!.content).toBe('Hello World'); - }); - - it('should register sub setting changes', () => { - component.subSettings = baseSettings(); - fixture.detectChanges(); - const newSubSettings = baseSettings(); - newSubSettings.template!.content = 'Hello World 2'; - - const changes: SimpleChanges = { - subSettings: new SimpleChange(component.subSettings, newSubSettings, false), - }; - component.subSettings = newSubSettings; - component.ngOnChanges(changes); - - expect(component.templateContent).toBe('Hello World 2'); - }); -}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts index 5b760fe5682f..8b353327a1bc 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts @@ -5,11 +5,9 @@ import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.serv import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { BehaviorSubject, of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; -import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; -import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; -import { mockModels, mockSettings } from './mock-settings'; +import { mockSettings } from './mock-settings'; import { ActivatedRoute, Params } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { NgModel } from '@angular/forms'; @@ -17,7 +15,6 @@ import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course import { By } from '@angular/platform-browser'; import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { HttpResponse } from '@angular/common/http'; -import { IrisCompetencyGenerationSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component'; describe('IrisCourseSettingsUpdateComponent Component', () => { let comp: IrisCourseSettingsUpdateComponent; @@ -27,7 +24,7 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; let paramsSpy: jest.SpyInstance; let getSettingsSpy: jest.SpyInstance; - let getModelsSpy: jest.SpyInstance; + //let getModelsSpy: jest.SpyInstance; let getParentSettingsSpy: jest.SpyInstance; beforeEach(() => { @@ -37,9 +34,6 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { IrisCourseSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisChatSubSettingsUpdateComponent), - MockComponent(IrisHestiaSubSettingsUpdateComponent), - MockComponent(IrisCompetencyGenerationSubSettingsUpdateComponent), MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), @@ -56,7 +50,7 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); - getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); }); fixture = TestBed.createComponent(IrisCourseSettingsUpdateComponent); @@ -73,14 +67,11 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { expect(comp.courseId).toBe(1); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledWith(1); - expect(getModelsSpy).toHaveBeenCalledOnce(); + //expect(getModelsSpy).toHaveBeenCalledOnce(); expect(getParentSettingsSpy).toHaveBeenCalledOnce(); expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(3); - expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.query(By.directive(IrisCompetencyGenerationSubSettingsUpdateComponent))).toBeTruthy(); }); it('Can deactivate correctly', () => { diff --git a/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts index a4ff32a50654..d99ce839e8a5 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts @@ -5,11 +5,9 @@ import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.serv import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { BehaviorSubject, of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; -import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; -import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; -import { mockModels, mockSettings } from './mock-settings'; +import { mockSettings } from './mock-settings'; import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; import { ActivatedRoute, Params } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; @@ -17,7 +15,6 @@ import { NgModel } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { HttpResponse } from '@angular/common/http'; -import { IrisCompetencyGenerationSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component'; describe('IrisExerciseSettingsUpdateComponent Component', () => { let comp: IrisExerciseSettingsUpdateComponent; @@ -27,7 +24,7 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; let paramsSpy: jest.SpyInstance; let getSettingsSpy: jest.SpyInstance; - let getModelsSpy: jest.SpyInstance; + //let getModelsSpy: jest.SpyInstance; let getParentSettingsSpy: jest.SpyInstance; beforeEach(() => { @@ -37,9 +34,6 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { IrisExerciseSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisChatSubSettingsUpdateComponent), - MockComponent(IrisHestiaSubSettingsUpdateComponent), - MockComponent(IrisCompetencyGenerationSubSettingsUpdateComponent), MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), @@ -56,7 +50,7 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedProgrammingExerciseSettings').mockReturnValue(of(irisSettings)); - getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getCombinedCourseSettings').mockReturnValue(of(irisSettings)); }); fixture = TestBed.createComponent(IrisExerciseSettingsUpdateComponent); @@ -74,14 +68,11 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { expect(comp.exerciseId).toBe(2); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledWith(2); - expect(getModelsSpy).toHaveBeenCalledOnce(); + //expect(getModelsSpy).toHaveBeenCalledOnce(); expect(getParentSettingsSpy).toHaveBeenCalledWith(1); expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(1); - expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeFalsy(); - expect(fixture.debugElement.query(By.directive(IrisCompetencyGenerationSubSettingsUpdateComponent))).toBeFalsy(); }); it('Can deactivate correctly', () => { diff --git a/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts index 1e28649e3b4b..63d8f603c3d7 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts @@ -5,24 +5,21 @@ import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.serv import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; -import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; -import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; -import { mockModels, mockSettings } from './mock-settings'; +import { mockSettings } from './mock-settings'; import { NgModel } from '@angular/forms'; import { IrisGlobalSettingsUpdateComponent } from 'app/iris/settings/iris-global-settings-update/iris-global-settings-update.component'; import { By } from '@angular/platform-browser'; import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { HttpResponse } from '@angular/common/http'; -import { IrisCompetencyGenerationSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-competency-generation-sub-settings-update/iris-competency-generation-sub-settings-update.component'; describe('IrisGlobalSettingsUpdateComponent Component', () => { let comp: IrisGlobalSettingsUpdateComponent; let fixture: ComponentFixture<IrisGlobalSettingsUpdateComponent>; let irisSettingsService: IrisSettingsService; let getSettingsSpy: jest.SpyInstance; - let getModelsSpy: jest.SpyInstance; + //let getModelsSpy: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -31,9 +28,6 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { IrisGlobalSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisChatSubSettingsUpdateComponent), - MockComponent(IrisHestiaSubSettingsUpdateComponent), - MockComponent(IrisCompetencyGenerationSubSettingsUpdateComponent), MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), @@ -47,7 +41,7 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { // Setup const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); - getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); }); fixture = TestBed.createComponent(IrisGlobalSettingsUpdateComponent); comp = fixture.componentInstance; @@ -61,13 +55,10 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { fixture.detectChanges(); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledOnce(); - expect(getModelsSpy).toHaveBeenCalledOnce(); + //expect(getModelsSpy).toHaveBeenCalledOnce(); expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeTruthy(); expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(3); - expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.query(By.directive(IrisCompetencyGenerationSubSettingsUpdateComponent))).toBeTruthy(); }); it('Can deactivate correctly', () => { diff --git a/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts deleted file mode 100644 index 4941c6c92dbb..000000000000 --- a/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { ArtemisTestModule } from '../../../test.module'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { MockDirective } from 'ng-mocks'; -import { SimpleChange, SimpleChanges } from '@angular/core'; -import { IrisHestiaSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; - -function baseSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; - const irisSubSettings = new IrisHestiaSubSettings(); - irisSubSettings.id = 2; - irisSubSettings.template = mockTemplate; - irisSubSettings.enabled = true; - return irisSubSettings; -} - -describe('IrisHestiaSubSettingsUpdateComponent Component', () => { - let comp: IrisHestiaSubSettingsUpdateComponent; - let fixture: ComponentFixture<IrisHestiaSubSettingsUpdateComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], - declarations: [IrisHestiaSubSettingsUpdateComponent], - }).compileComponents(); - fixture = TestBed.createComponent(IrisHestiaSubSettingsUpdateComponent); - comp = fixture.componentInstance; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('template is not optional', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeFalsy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional', () => { - comp.subSettings = baseSettings(); - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional and changes from defined to undefined', () => { - comp.subSettings = baseSettings(); - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - comp.onInheritTemplateChanged(); - fixture.detectChanges(); - expect(comp.subSettings.template).toBeUndefined(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template is optional and changes from undefined to defined', () => { - const subSettings = baseSettings(); - subSettings.template = undefined; - comp.subSettings = subSettings; - comp.parentSubSettings = baseSettings(); - fixture.detectChanges(); - comp.onInheritTemplateChanged(); - fixture.detectChanges(); - expect(comp.subSettings.template).toBeDefined(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); - }); - - it('template changes', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - comp.templateContent = 'Hello World 2'; - comp.onTemplateChanged(); - - expect(comp.subSettings.template?.content).toBe('Hello World 2'); - }); - - it('template created', () => { - comp.subSettings = baseSettings(); - comp.subSettings.template = undefined; - fixture.detectChanges(); - comp.templateContent = 'Hello World 2'; - comp.onTemplateChanged(); - - expect(comp.subSettings.template!.content).toBe('Hello World 2'); - }); - - it('sub settings changes', () => { - comp.subSettings = baseSettings(); - fixture.detectChanges(); - const newSubSettings = baseSettings(); - newSubSettings.template!.content = 'Hello World 2'; - - const changes: SimpleChanges = { - subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), - }; - comp.subSettings = newSubSettings; - comp.ngOnChanges(changes); - - expect(comp.templateContent).toBe('Hello World 2'); - }); -}); diff --git a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts index 79d7e7165a40..764930b97f19 100644 --- a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit-form.component.spec.ts @@ -82,6 +82,7 @@ describe('AttachmentUnitFormComponent', () => { attachmentUnitFormComponent.releaseDateControl!.setValue(exampleReleaseDate); const exampleDescription = 'lorem ipsum'; attachmentUnitFormComponent.descriptionControl!.setValue(exampleDescription); + attachmentUnitFormComponent.versionControl!.enable(); const exampleVersion = 42; attachmentUnitFormComponent.versionControl!.setValue(exampleVersion); const exampleUpdateNotificationText = 'updated'; diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts new file mode 100644 index 000000000000..9b458fcfea34 --- /dev/null +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts @@ -0,0 +1,205 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; +import { of } from 'rxjs'; +import { BuildJob } from 'app/entities/build-job.model'; +import dayjs from 'dayjs/esm'; +import { ArtemisTestModule } from '../../../test.module'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { DataTableComponent } from 'app/shared/data-table/data-table.component'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { NgxDatatableModule } from '@flaviosantoro92/ngx-datatable'; +import { BuildAgent } from 'app/entities/build-agent.model'; +import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/repository-info.model'; +import { JobTimingInfo } from 'app/entities/job-timing-info.model'; +import { BuildConfig } from 'app/entities/build-config.model'; +import { BuildAgentDetailsComponent } from 'app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component'; +import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; +import { ActivatedRoute } from '@angular/router'; + +describe('BuildAgentDetailsComponent', () => { + let component: BuildAgentDetailsComponent; + let fixture: ComponentFixture<BuildAgentDetailsComponent>; + let activatedRoute: MockActivatedRoute; + + const mockWebsocketService = { + subscribe: jest.fn(), + unsubscribe: jest.fn(), + receive: jest.fn().mockReturnValue(of([])), + }; + + const mockBuildAgentsService = { + getBuildAgentDetails: jest.fn().mockReturnValue(of([])), + }; + + const repositoryInfo: RepositoryInfo = { + repositoryName: 'repo2', + repositoryType: 'USER', + triggeredByPushTo: TriggeredByPushTo.USER, + assignmentRepositoryUri: 'https://some.uri', + testRepositoryUri: 'https://some.uri', + solutionRepositoryUri: 'https://some.uri', + auxiliaryRepositoryUris: [], + auxiliaryRepositoryCheckoutDirectories: [], + }; + + const jobTimingInfo1: JobTimingInfo = { + submissionDate: dayjs('2023-01-01'), + buildStartDate: dayjs('2023-01-01'), + buildCompletionDate: dayjs('2023-01-02'), + buildDuration: undefined, + }; + + const buildConfig: BuildConfig = { + dockerImage: 'someImage', + commitHashToBuild: 'abc124', + branch: 'main', + programmingLanguage: 'Java', + projectType: 'Maven', + scaEnabled: false, + sequentialTestRunsEnabled: false, + testwiseCoverageEnabled: false, + resultPaths: [], + }; + + const mockRunningJobs1: BuildJob[] = [ + { + id: '2', + name: 'Build Job 2', + buildAgentAddress: 'agent2', + participationId: 102, + courseId: 10, + exerciseId: 100, + retryCount: 0, + priority: 3, + repositoryInfo: repositoryInfo, + jobTimingInfo: jobTimingInfo1, + buildConfig: buildConfig, + }, + { + id: '4', + name: 'Build Job 4', + buildAgentAddress: 'agent4', + participationId: 104, + courseId: 10, + exerciseId: 100, + retryCount: 0, + priority: 2, + repositoryInfo: repositoryInfo, + jobTimingInfo: jobTimingInfo1, + buildConfig: buildConfig, + }, + ]; + + const mockRecentBuildJobs1: BuildJob[] = [ + { + id: '1', + name: 'Build Job 1', + buildAgentAddress: 'agent1', + participationId: 101, + courseId: 10, + exerciseId: 100, + retryCount: 0, + priority: 4, + repositoryInfo: repositoryInfo, + jobTimingInfo: jobTimingInfo1, + buildConfig: buildConfig, + }, + ]; + + const mockBuildAgent: BuildAgent = { + id: 1, + name: 'buildagent1', + maxNumberOfConcurrentBuildJobs: 2, + numberOfCurrentBuildJobs: 2, + runningBuildJobs: mockRunningJobs1, + recentBuildJobs: mockRecentBuildJobs1, + status: true, + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, NgxDatatableModule], + declarations: [BuildAgentDetailsComponent, MockPipe(ArtemisTranslatePipe), MockComponent(DataTableComponent)], + providers: [ + { provide: JhiWebsocketService, useValue: mockWebsocketService }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute({ key: 'ABC123' }) }, + { provide: BuildAgentsService, useValue: mockBuildAgentsService }, + { provide: DataTableComponent, useClass: DataTableComponent }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BuildAgentDetailsComponent); + component = fixture.componentInstance; + activatedRoute = fixture.debugElement.injector.get(ActivatedRoute) as MockActivatedRoute; + activatedRoute.setParameters({ agentName: mockBuildAgent.name }); + })); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should load build agents on initialization', () => { + mockBuildAgentsService.getBuildAgentDetails.mockReturnValue(of(mockBuildAgent)); + mockWebsocketService.receive.mockReturnValue(of(mockBuildAgent)); + + component.ngOnInit(); + + expect(mockBuildAgentsService.getBuildAgentDetails).toHaveBeenCalled(); + expect(component.buildAgent).toEqual(mockBuildAgent); + }); + + it('should initialize websocket subscription on initialization', () => { + mockWebsocketService.receive.mockReturnValue(of(mockBuildAgent)); + + component.ngOnInit(); + + expect(component.buildAgent).toEqual(mockBuildAgent); + expect(mockWebsocketService.subscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + expect(mockWebsocketService.receive).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + }); + + it('should unsubscribe from the websocket channel on destruction', () => { + mockWebsocketService.receive.mockReturnValue(of(mockBuildAgent)); + + component.ngOnInit(); + + component.ngOnDestroy(); + + expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + }); + + it('should set recent build jobs duration', () => { + mockBuildAgentsService.getBuildAgentDetails.mockReturnValue(of(mockBuildAgent)); + mockWebsocketService.receive.mockReturnValue(of(mockBuildAgent)); + + component.ngOnInit(); + + for (const recentBuildJob of component.buildAgent.recentBuildJobs || []) { + const { jobTimingInfo } = recentBuildJob; + const { buildCompletionDate, buildStartDate, buildDuration } = jobTimingInfo || {}; + if (buildDuration && jobTimingInfo) { + expect(buildDuration).toEqual(buildCompletionDate!.diff(buildStartDate!, 'milliseconds') / 1000); + } + } + }); + + it('should cancel a build job', () => { + const buildJob = mockRunningJobs1[0]; + const spy = jest.spyOn(component, 'cancelBuildJob'); + + component.ngOnInit(); + component.cancelBuildJob(buildJob.id!); + + expect(spy).toHaveBeenCalledExactlyOnceWith(buildJob.id!); + }); + + it('should cancel all build jobs of a build agent', () => { + const spy = jest.spyOn(component, 'cancelAllBuildJobs'); + + component.ngOnInit(); + component.cancelAllBuildJobs(); + + expect(spy).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts similarity index 66% rename from src/test/javascript/spec/component/localci/build-agents/build-agents.component.spec.ts rename to src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index 59574d66ba3c..2ffc69f9b823 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BuildAgentsComponent } from 'app/localci/build-agents/build-agents.component'; +import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { of } from 'rxjs'; @@ -15,9 +15,9 @@ import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/repository-info. import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/build-config.model'; -describe('BuildAgentsComponent', () => { - let component: BuildAgentsComponent; - let fixture: ComponentFixture<BuildAgentsComponent>; +describe('BuildAgentSummaryComponent', () => { + let component: BuildAgentSummaryComponent; + let fixture: ComponentFixture<BuildAgentSummaryComponent>; const mockWebsocketService = { subscribe: jest.fn(), @@ -26,7 +26,7 @@ describe('BuildAgentsComponent', () => { }; const mockBuildAgentsService = { - getBuildAgents: jest.fn().mockReturnValue(of([])), + getBuildAgentSummary: jest.fn().mockReturnValue(of([])), }; const repositoryInfo: RepositoryInfo = { @@ -47,13 +47,6 @@ describe('BuildAgentsComponent', () => { buildDuration: undefined, }; - const jobTimingInfo2: JobTimingInfo = { - submissionDate: dayjs('2023-01-03'), - buildStartDate: dayjs('2023-01-03'), - buildCompletionDate: dayjs('2023-01-07'), - buildDuration: undefined, - }; - const buildConfig: BuildConfig = { dockerImage: 'someImage', commitHashToBuild: 'abc124', @@ -124,64 +117,6 @@ describe('BuildAgentsComponent', () => { }, ]; - const mockRecentBuildJobs1: BuildJob[] = [ - { - id: '1', - name: 'Build Job 1', - buildAgentAddress: 'agent1', - participationId: 101, - courseId: 10, - exerciseId: 100, - retryCount: 0, - priority: 4, - repositoryInfo: repositoryInfo, - jobTimingInfo: jobTimingInfo1, - buildConfig: buildConfig, - }, - { - id: '2', - name: 'Build Job 2', - buildAgentAddress: 'agent2', - participationId: 102, - courseId: 10, - exerciseId: 100, - retryCount: 0, - priority: 3, - repositoryInfo: repositoryInfo, - jobTimingInfo: jobTimingInfo2, - buildConfig: buildConfig, - }, - ]; - - const mockRecentBuildJobs2: BuildJob[] = [ - { - id: '3', - name: 'Build Job 3', - buildAgentAddress: 'agent3', - participationId: 103, - courseId: 10, - exerciseId: 100, - retryCount: 0, - priority: 5, - repositoryInfo: repositoryInfo, - jobTimingInfo: jobTimingInfo1, - buildConfig: buildConfig, - }, - { - id: '4', - name: 'Build Job 4', - buildAgentAddress: 'agent4', - participationId: 104, - courseId: 10, - exerciseId: 100, - retryCount: 0, - priority: 2, - repositoryInfo: repositoryInfo, - jobTimingInfo: jobTimingInfo2, - buildConfig: buildConfig, - }, - ]; - const mockBuildAgents: BuildAgent[] = [ { id: 1, @@ -189,7 +124,6 @@ describe('BuildAgentsComponent', () => { maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs1, - recentBuildJobs: mockRecentBuildJobs1, status: true, }, { @@ -198,7 +132,6 @@ describe('BuildAgentsComponent', () => { maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs2, - recentBuildJobs: mockRecentBuildJobs2, status: true, }, ]; @@ -206,7 +139,7 @@ describe('BuildAgentsComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, NgxDatatableModule], - declarations: [BuildAgentsComponent, MockPipe(ArtemisTranslatePipe), MockComponent(DataTableComponent)], + declarations: [BuildAgentSummaryComponent, MockPipe(ArtemisTranslatePipe), MockComponent(DataTableComponent)], providers: [ { provide: JhiWebsocketService, useValue: mockWebsocketService }, { provide: BuildAgentsService, useValue: mockBuildAgentsService }, @@ -214,7 +147,7 @@ describe('BuildAgentsComponent', () => { ], }).compileComponents(); - fixture = TestBed.createComponent(BuildAgentsComponent); + fixture = TestBed.createComponent(BuildAgentSummaryComponent); component = fixture.componentInstance; })); @@ -222,17 +155,13 @@ describe('BuildAgentsComponent', () => { jest.clearAllMocks(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should load build agents on initialization', () => { - mockBuildAgentsService.getBuildAgents.mockReturnValue(of(mockBuildAgents)); + mockBuildAgentsService.getBuildAgentSummary.mockReturnValue(of(mockBuildAgents)); mockWebsocketService.receive.mockReturnValue(of(mockBuildAgents)); component.ngOnInit(); - expect(mockBuildAgentsService.getBuildAgents).toHaveBeenCalled(); + expect(mockBuildAgentsService.getBuildAgentSummary).toHaveBeenCalled(); expect(component.buildAgents).toEqual(mockBuildAgents); }); @@ -252,23 +181,6 @@ describe('BuildAgentsComponent', () => { expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agents'); }); - it('should set recent build jobs duration', () => { - mockBuildAgentsService.getBuildAgents.mockReturnValue(of(mockBuildAgents)); - mockWebsocketService.receive.mockReturnValue(of(mockBuildAgents)); - - component.ngOnInit(); - - for (const buildAgent of component.buildAgents) { - for (const recentBuildJob of buildAgent.recentBuildJobs || []) { - const { jobTimingInfo } = recentBuildJob; - const { buildCompletionDate, buildStartDate, buildDuration } = jobTimingInfo || {}; - if (buildDuration && jobTimingInfo) { - expect(buildDuration).toEqual(buildCompletionDate!.diff(buildStartDate!, 'milliseconds') / 1000); - } - } - } - }); - it('should cancel a build job', () => { const buildJob = mockRunningJobs1[0]; const spy = jest.spyOn(component, 'cancelBuildJob'); @@ -288,4 +200,22 @@ describe('BuildAgentsComponent', () => { expect(spy).toHaveBeenCalledExactlyOnceWith(buildAgent.name!); }); + + it('should calculate the build capacity and current builds', () => { + mockWebsocketService.receive.mockReturnValue(of(mockBuildAgents)); + + component.ngOnInit(); + + expect(component.buildCapacity).toBe(4); + expect(component.currentBuilds).toBe(4); + }); + + it('should calculate the build capacity and current builds when there are no build agents', () => { + mockWebsocketService.receive.mockReturnValue(of([])); + + component.ngOnInit(); + + expect(component.buildCapacity).toBe(0); + expect(component.currentBuilds).toBe(0); + }); }); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts index 2b5899bc68d9..20ac1e1e4011 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts @@ -91,7 +91,7 @@ describe('BuildAgentsService', () => { it('should return build agents', () => { const expectedResponse = [element]; // Expecting an array - service.getBuildAgents().subscribe((data) => { + service.getBuildAgentSummary().subscribe((data) => { expect(data).toEqual(expectedResponse); // Check if the response matches expected }); @@ -100,6 +100,18 @@ describe('BuildAgentsService', () => { req.flush(expectedResponse); // Flush an array of elements }); + it('should return build agent details', () => { + const expectedResponse = element; + + service.getBuildAgentDetails('buildAgent1').subscribe((data) => { + expect(data).toEqual(expectedResponse); + }); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/build-agent?agentName=buildAgent1`); + expect(req.request.method).toBe('GET'); + req.flush(expectedResponse); + }); + afterEach(() => { httpMock.verify(); // Verify that there are no outstanding requests. }); diff --git a/src/test/javascript/spec/component/modeling-editor/modeling-editor.component.spec.ts b/src/test/javascript/spec/component/modeling-editor/modeling-editor.component.spec.ts index 176892880df3..290a32892330 100644 --- a/src/test/javascript/spec/component/modeling-editor/modeling-editor.component.spec.ts +++ b/src/test/javascript/spec/component/modeling-editor/modeling-editor.component.spec.ts @@ -57,7 +57,7 @@ describe('ModelingEditorComponent', () => { fixture.detectChanges(); // test - component.ngAfterViewInit(); + await component.ngAfterViewInit(); const editor: ApollonEditor = component['apollonEditor'] as ApollonEditor; // Check that editor exists expect(editor).toBeDefined(); @@ -81,7 +81,7 @@ describe('ModelingEditorComponent', () => { const model = classDiagram; component.umlModel = model; fixture.detectChanges(); - component.ngAfterViewInit(); + await component.ngAfterViewInit(); const changedModel = cloneDeep(model) as any; changedModel.elements = {}; diff --git a/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts index 2ca897b50b02..beb9799b6290 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts @@ -11,7 +11,6 @@ import { ProgrammingExerciseStudentParticipation } from 'app/entities/participat import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { QuizBatch, QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; -import { Result } from 'app/entities/result.model'; import { Team } from 'app/entities/team.model'; import { TextExercise } from 'app/entities/text-exercise.model'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; @@ -33,6 +32,7 @@ import { MockRouter } from '../../../helpers/mocks/mock-router'; import { MockCourseExerciseService } from '../../../helpers/mocks/service/mock-course-exercise.service'; import { MockSyncStorage } from '../../../helpers/mocks/service/mock-sync-storage.service'; import { ArtemisTestModule } from '../../../test.module'; +import { AssessmentType } from 'app/entities/assessment-type.model'; describe('ExerciseDetailsStudentActionsComponent', () => { let comp: ExerciseDetailsStudentActionsComponent; @@ -312,32 +312,6 @@ describe('ExerciseDetailsStudentActionsComponent', () => { expect(comp.exercise.studentParticipations).toEqual([activeParticipation, practiceParticipation]); }); - it('should disable the feedback request button', () => { - const result: Result = { score: 50, rated: true }; - const participation: StudentParticipation = { - results: [result], - individualDueDate: undefined, - }; - - comp.exercise = { ...exercise, allowManualFeedbackRequests: true }; - comp.gradedParticipation = participation; - - expect(comp.isFeedbackRequestButtonDisabled()).toBeTrue(); - }); - - it('should enable the feedback request button', () => { - const result: Result = { score: 100, rated: true }; - const participation: StudentParticipation = { - results: [result], - individualDueDate: undefined, - }; - - comp.exercise = { ...exercise, allowManualFeedbackRequests: true }; - comp.gradedParticipation = participation; - - expect(comp.isFeedbackRequestButtonDisabled()).toBeFalse(); - }); - it('should show correct buttons in exam mode', fakeAsync(() => { const exercise = { type: ExerciseType.PROGRAMMING, allowOfflineIde: false, allowOnlineEditor: true } as ProgrammingExercise; exercise.studentParticipations = [{ initializationState: InitializationState.INITIALIZED } as StudentParticipation]; @@ -525,4 +499,128 @@ describe('ExerciseDetailsStudentActionsComponent', () => { } }), ); + + // until a policy is set + it.skip('assureConditionsSatisfied should alert and return false if not all hidden tests have passed', () => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + comp.exercise = { + type: ExerciseType.PROGRAMMING, + dueDate: dayjs().subtract(5, 'minutes'), + studentParticipations: [ + { + id: 2, + results: [ + { + assessmentType: AssessmentType.AUTOMATIC, + score: 80, + }, + ], + }, + ] as StudentParticipation[], + } as ProgrammingExercise; + + const result = comp.assureConditionsSatisfied(); + + expect(window.alert).toHaveBeenCalledWith('artemisApp.exercise.notEnoughPoints'); + expect(result).toBeFalse(); + }); + + it('assureConditionsSatisfied should alert and return false if the feedback request has already been sent', () => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + comp.exercise = { + type: ExerciseType.PROGRAMMING, + dueDate: dayjs().add(5, 'minutes'), + studentParticipations: [ + { + id: 2, + individualDueDate: dayjs().subtract(5, 'days'), + results: [ + { + assessmentType: AssessmentType.AUTOMATIC, + score: 100, + }, + ], + }, + ] as StudentParticipation[], + } as ProgrammingExercise; + + const result = comp.assureConditionsSatisfied(); + + expect(window.alert).toHaveBeenCalledWith('artemisApp.exercise.feedbackRequestAlreadySent'); + expect(result).toBeFalse(); + }); + + it('assureConditionsSatisfied should alert and return false if the request is made after the due date', () => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + comp.exercise = { + type: ExerciseType.PROGRAMMING, + dueDate: dayjs().subtract(5, 'minutes'), + studentParticipations: [ + { + id: 2, + results: [ + { + assessmentType: AssessmentType.AUTOMATIC, + score: 100, + }, + ], + }, + ] as StudentParticipation[], + } as ProgrammingExercise; + + const result = comp.assureConditionsSatisfied(); + + expect(window.alert).toHaveBeenCalledWith('artemisApp.exercise.feedbackRequestAfterDueDate'); + expect(result).toBeFalse(); + }); + + it('assureConditionsSatisfied should return true if all conditions are satisfied', () => { + comp.exercise = { + type: ExerciseType.PROGRAMMING, + dueDate: dayjs().add(5, 'minutes'), + studentParticipations: [ + { + id: 2, + results: [ + { + assessmentType: AssessmentType.AUTOMATIC, + score: 100, + }, + ], + }, + ] as StudentParticipation[], + } as ProgrammingExercise; + + const result = comp.assureConditionsSatisfied(); + + expect(result).toBeTrue(); + }); + + it('assureConditionsSatisfied should alert and return false if the maximum number of successful Athena results is reached', () => { + jest.spyOn(window, 'alert').mockImplementation(() => {}); + comp.exercise = { + type: ExerciseType.PROGRAMMING, + dueDate: dayjs().add(5, 'minutes'), + studentParticipations: [ + { + id: 2, + individualDueDate: undefined, + results: [ + { + assessmentType: AssessmentType.AUTOMATIC, + score: 100, + }, + { assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }, + { assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }, + { assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }, + ], + }, + ] as StudentParticipation[], + } as ProgrammingExercise; + + const result = comp.assureConditionsSatisfied(); + + expect(window.alert).toHaveBeenCalledWith('artemisApp.exercise.maxAthenaResultsReached'); + expect(result).toBeFalse(); + }); }); diff --git a/src/test/javascript/spec/component/programming-assessment/code-editor-tutor-assessment-inline-feedback.component.spec.ts b/src/test/javascript/spec/component/programming-assessment/code-editor-tutor-assessment-inline-feedback.component.spec.ts index 3692928951bc..6cacd3126733 100644 --- a/src/test/javascript/spec/component/programming-assessment/code-editor-tutor-assessment-inline-feedback.component.spec.ts +++ b/src/test/javascript/spec/component/programming-assessment/code-editor-tutor-assessment-inline-feedback.component.spec.ts @@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { CodeEditorTutorAssessmentInlineFeedbackComponent } from 'app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component'; -import { Feedback, FeedbackType } from 'app/entities/feedback.model'; +import { Feedback, FeedbackType, NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER } from 'app/entities/feedback.model'; import { GradingInstruction } from 'app/exercises/shared/structured-grading-criterion/grading-instruction.model'; import { StructuredGradingCriterionService } from 'app/exercises/shared/structured-grading-criterion/structured-grading-criterion.service'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; @@ -14,6 +14,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { QuotePipe } from 'app/shared/pipes/quote.pipe'; import { FeedbackContentPipe } from 'app/shared/pipes/feedback-content.pipe'; +import { By } from '@angular/platform-browser'; describe('CodeEditorTutorAssessmentInlineFeedbackComponent', () => { let comp: CodeEditorTutorAssessmentInlineFeedbackComponent; @@ -146,4 +147,54 @@ describe('CodeEditorTutorAssessmentInlineFeedbackComponent', () => { const textToBeDisplayed = comp.buildFeedbackTextForCodeEditor(feedbackWithSpecialCharacters); expect(textToBeDisplayed).toEqual(expectedTextToBeDisplayed); }); + + it('should not display credits and icons for non-graded feedback suggestions', () => { + comp.feedback = { + type: FeedbackType.AUTOMATIC, + text: NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER + 'feedback', + } as Feedback; + fixture.detectChanges(); + + const badgeElement = fixture.debugElement.query(By.css('.badge')); + expect(badgeElement).toBeNull(); + }); + + it('should display credits and icons for graded feedback', () => { + comp.feedback = { + credits: 1, + type: FeedbackType.AUTOMATIC, + text: 'feedback', + } as Feedback; + fixture.detectChanges(); + + const badgeElement = fixture.debugElement.query(By.css('.badge')); + expect(badgeElement).not.toBeNull(); + expect(badgeElement.nativeElement.textContent).toContain('1P'); + }); + + it('should use the correct translation key for non-graded feedback', () => { + comp.feedback = { + type: FeedbackType.AUTOMATIC, + text: NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER + 'feedback', + } as Feedback; + fixture.detectChanges(); + + const headerElement = fixture.debugElement.query(By.css('.col h6')).nativeElement; + expect(headerElement.attributes['jhiTranslate'].value).toBe('artemisApp.assessment.detail.feedback'); + const paragraphElement = fixture.debugElement.query(By.css('.col p')).nativeElement; + expect(paragraphElement.innerHTML).toContain(comp.buildFeedbackTextForCodeEditor(comp.feedback)); + }); + + it('should use the correct translation key for graded feedback', () => { + comp.feedback = { + type: FeedbackType.MANUAL, + text: 'feedback', + } as Feedback; + fixture.detectChanges(); + + const headerElement = fixture.debugElement.query(By.css('.col h6')).nativeElement; + expect(headerElement.attributes['jhiTranslate'].value).toBe('artemisApp.assessment.detail.tutorComment'); + const paragraphElement = fixture.debugElement.query(By.css('.col p')).nativeElement; + expect(paragraphElement.innerHTML).toContain(comp.buildFeedbackTextForCodeEditor(comp.feedback)); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts index c43c56fbacfe..cf5f7bb8a545 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts @@ -129,12 +129,12 @@ describe('ProgrammingExerciseLifecycleComponent', () => { }); it('should change feedback request allowed after toggling', () => { - comp.exercise = { ...exercise, allowManualFeedbackRequests: false }; - expect(comp.exercise.allowManualFeedbackRequests).toBeFalse(); + comp.exercise = { ...exercise, allowFeedbackRequests: false }; + expect(comp.exercise.allowFeedbackRequests).toBeFalse(); - comp.toggleManualFeedbackRequests(); + comp.toggleFeedbackRequests(); - expect(comp.exercise.allowManualFeedbackRequests).toBeTrue(); + expect(comp.exercise.allowFeedbackRequests).toBeTrue(); }); it('should change assessment type from automatic to semi-automatic after toggling', () => { diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-student-repo-download.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-student-repo-download.component.spec.ts new file mode 100644 index 000000000000..ae746b6195e2 --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-student-repo-download.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProgrammingExerciseInstructorRepoDownloadComponent } from 'app/exercises/programming/shared/actions/programming-exercise-instructor-repo-download.component'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { ProgrammingExerciseStudentRepoDownloadComponent } from 'app/exercises/programming/shared/actions/programming-exercise-student-repo-download.component'; + +describe('ProgrammingExerciseStudentRepoDownloadComponent', () => { + let comp: ProgrammingExerciseStudentRepoDownloadComponent; + let fixture: ComponentFixture<ProgrammingExerciseStudentRepoDownloadComponent>; + let programmingExerciseService: ProgrammingExerciseService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ProgrammingExerciseInstructorRepoDownloadComponent, MockComponent(ButtonComponent)], + providers: [{ provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }], + }).compileComponents(); + + fixture = TestBed.createComponent(ProgrammingExerciseStudentRepoDownloadComponent); + comp = fixture.componentInstance; + programmingExerciseService = TestBed.inject(ProgrammingExerciseService); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should not attempt to download if the exercise id is missing', () => { + const exportSpy = jest.spyOn(programmingExerciseService, 'exportStudentRepository'); + comp.participationId = 100; + fixture.detectChanges(); + comp.exportRepository(); + expect(exportSpy).not.toHaveBeenCalled(); + }); + + it('should not attempt to download if the participation id is missing', () => { + const exportSpy = jest.spyOn(programmingExerciseService, 'exportStudentRepository'); + comp.exerciseId = 100; + fixture.detectChanges(); + comp.exportRepository(); + expect(exportSpy).not.toHaveBeenCalled(); + }); + + it('should download the correct repository', () => { + const exportSpy = jest.spyOn(programmingExerciseService, 'exportStudentRepository'); + comp.exerciseId = 10; + comp.participationId = 20; + fixture.detectChanges(); + comp.exportRepository(); + expect(exportSpy).toHaveBeenCalledExactlyOnceWith(10, 20); + }); +}); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts index ba22ea3cae26..756832768cd3 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts @@ -1041,13 +1041,12 @@ describe('ProgrammingExerciseUpdateComponent', () => { expect(comp.programmingExercise.exampleSolutionPublicationDate).toBeUndefined(); expect(comp.programmingExercise.zipFileForImport?.name).toBe('test.zip'); expect(comp.programmingExercise.allowComplaintsForAutomaticAssessments).toBeFalse(); - expect(comp.programmingExercise.allowManualFeedbackRequests).toBeFalse(); expect(comp.programmingExercise.allowOfflineIde).toBeTrue(); expect(comp.programmingExercise.allowOnlineEditor).toBeTrue(); expect(comp.programmingExercise.programmingLanguage).toBe(ProgrammingLanguage.JAVA); expect(comp.programmingExercise.projectType).toBe(ProjectType.PLAIN_MAVEN); // allow manual feedback requests and complaints for automatic assessments should be set to false because we reset all dates and hence they can only be false - expect(comp.programmingExercise.allowManualFeedbackRequests).toBeFalse(); + expect(comp.programmingExercise.allowFeedbackRequests).toBeFalse(); expect(comp.programmingExercise.allowComplaintsForAutomaticAssessments).toBeFalse(); // name and short name should also be imported expect(comp.programmingExercise.title).toEqual(importedProgrammingExercise.title); @@ -1071,7 +1070,7 @@ const getProgrammingExerciseForImport = () => { programmingExercise.allowOfflineIde = true; programmingExercise.allowOnlineEditor = true; programmingExercise.allowComplaintsForAutomaticAssessments = true; - programmingExercise.allowManualFeedbackRequests = true; + programmingExercise.allowFeedbackRequests = true; history.pushState({ programmingExerciseForImportFromFile: programmingExercise }, ''); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts new file mode 100644 index 000000000000..350a6ed235d3 --- /dev/null +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts @@ -0,0 +1,87 @@ +import { Theme, ThemeService } from 'app/core/theme/theme.service'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; +import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; +import { MonacoDiffEditorComponent } from 'app/shared/monaco-editor/monaco-diff-editor.component'; +import { BehaviorSubject } from 'rxjs'; + +describe('MonacoDiffEditorComponent', () => { + let fixture: ComponentFixture<MonacoDiffEditorComponent>; + let comp: MonacoDiffEditorComponent; + let mockThemeService: ThemeService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, MonacoEditorModule], + declarations: [MonacoDiffEditorComponent], + providers: [], + }) + .compileComponents() + .then(() => { + global.ResizeObserver = jest.fn().mockImplementation((callback: ResizeObserverCallback) => { + return new MockResizeObserver(callback); + }); + mockThemeService = TestBed.inject(ThemeService); + fixture = TestBed.createComponent(MonacoDiffEditorComponent); + comp = fixture.componentInstance; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should adjust its theme to the global theme', () => { + const themeSubject = new BehaviorSubject<Theme>(Theme.LIGHT); + const subscribeStub = jest.spyOn(mockThemeService, 'getCurrentThemeObservable').mockReturnValue(themeSubject.asObservable()); + const changeThemeSpy = jest.spyOn(comp, 'changeTheme'); + fixture.detectChanges(); + themeSubject.next(Theme.DARK); + expect(subscribeStub).toHaveBeenCalledOnce(); + expect(changeThemeSpy).toHaveBeenCalledTimes(2); + expect(changeThemeSpy).toHaveBeenNthCalledWith(1, Theme.LIGHT); + expect(changeThemeSpy).toHaveBeenNthCalledWith(2, Theme.DARK); + }); + + it('should dispose its listeners and subscriptions when destroyed', () => { + fixture.detectChanges(); + const resizeObserverDisconnectSpy = jest.spyOn(comp.resizeObserver!, 'disconnect'); + const themeSubscriptionUnsubscribeSpy = jest.spyOn(comp.themeSubscription!, 'unsubscribe'); + const listenerDisposeSpies = comp.listeners.map((listener) => jest.spyOn(listener, 'dispose')); + comp.ngOnDestroy(); + for (const spy of [resizeObserverDisconnectSpy, themeSubscriptionUnsubscribeSpy, ...listenerDisposeSpies]) { + expect(spy).toHaveBeenCalledOnce(); + } + }); + + it('should update the size of its container and layout the editor', () => { + const layoutSpy = jest.spyOn(comp, 'layout'); + fixture.detectChanges(); + const element = document.createElement('div'); + comp.monacoDiffEditorContainerElement = element; + comp.adjustHeightAndLayout(100); + expect(element.style.height).toBe('100px'); + expect(layoutSpy).toHaveBeenCalledOnce(); + }); + + it('should set the text of the editor', () => { + const original = 'some original content'; + const modified = 'some modified content'; + fixture.detectChanges(); + comp.setFileContents(original, 'originalFileName.java', modified, 'modifiedFileName.java'); + expect(comp.getText()).toEqual({ original, modified }); + }); + + it('should notify about its readiness to display', async () => { + const readyCallbackStub = jest.fn(); + comp.onReadyForDisplayChange.subscribe(readyCallbackStub); + fixture.detectChanges(); + comp.setFileContents('original', 'file', 'modified', 'file'); + // Wait for the diff computation, which is handled by Monaco. + await new Promise((r) => setTimeout(r, 200)); + expect(readyCallbackStub).toHaveBeenCalledTimes(2); + expect(readyCallbackStub).toHaveBeenNthCalledWith(1, false); + expect(readyCallbackStub).toHaveBeenNthCalledWith(2, true); + }); +}); diff --git a/src/test/javascript/spec/component/shared/result.component.spec.ts b/src/test/javascript/spec/component/shared/result.component.spec.ts index 7332b1a5fa07..14e8bea7a123 100644 --- a/src/test/javascript/spec/component/shared/result.component.spec.ts +++ b/src/test/javascript/spec/component/shared/result.component.spec.ts @@ -294,4 +294,77 @@ describe('ResultComponent', () => { const submittedSpan = fixture.nativeElement.querySelector('#test-late'); expect(submittedSpan).toBeTruthy(); }); + + describe('ResultComponent - Graded Results', () => { + beforeEach(() => { + comp.participation = mockParticipation; + }); + + it('should display the first rated result if showUngradedResults is false', () => { + comp.participation.results = [{ id: 2, rated: false, score: 50 } as Result, mockResult, { id: 3, rated: false, score: 70 } as Result]; + comp.showUngradedResults = false; + comp.ngOnInit(); + + expect(comp.result).toEqual(mockResult); + }); + + it('should display the first result if showUngradedResults is true', () => { + comp.participation.results = [{ id: 2, rated: false, score: 50 } as Result, mockResult]; + comp.showUngradedResults = true; + comp.ngOnInit(); + + expect(comp.result).toEqual(comp.participation.results[0]); + }); + + it('should not have a result if there are no rated results and showUngradedResults is false', () => { + comp.participation.results = [{ id: 2, rated: false, score: 50 } as Result, { id: 3, rated: false, score: 70 } as Result]; + comp.showUngradedResults = false; + comp.ngOnInit(); + + expect(comp.result).toBeUndefined(); + }); + }); + + describe('ResultComponent - Feedback Generation', () => { + beforeEach(() => { + jest.useFakeTimers(); + comp.result = { ...mockResult, assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: undefined, completionDate: dayjs().add(1, 'minute') }; + comp.exercise = mockExercise; + comp.participation = mockParticipation; + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should call evaluate again after the specified due time', () => { + comp.result = { ...comp.result, completionDate: dayjs().add(2, 'seconds') }; + comp.templateStatus = ResultTemplateStatus.IS_GENERATING_FEEDBACK; + comp.evaluate(); + + comp.result.completionDate = dayjs().subtract(2, 'seconds'); + jest.runOnlyPendingTimers(); + + expect(comp.templateStatus).not.toEqual(ResultTemplateStatus.IS_GENERATING_FEEDBACK); + }); + + it('should clear the timeout if the component is destroyed before the feedback generation is complete', () => { + comp.templateStatus = ResultTemplateStatus.IS_GENERATING_FEEDBACK; + comp.evaluate(); + expect(jest.getTimerCount()).toBe(1); + + comp.ngOnDestroy(); + expect(jest.getTimerCount()).toBe(0); + }); + }); + + it('should use special handling if result is an automatic AI result', () => { + comp.result = { ...mockResult, score: 90, assessmentType: AssessmentType.AUTOMATIC_ATHENA }; + jest.spyOn(Result, 'isAthenaAIResult').mockReturnValue(true); + + comp.evaluate(); + + expect(comp.templateStatus).toEqual(ResultTemplateStatus.HAS_RESULT); + expect(comp.resultTooltip).toContain('artemisApp.result.resultString.automaticAIFeedbackSuccessfulTooltip'); + }); }); diff --git a/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts b/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts index 11f2b12d64ab..1638cbcb9176 100644 --- a/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts +++ b/src/test/javascript/spec/component/standardized-competencies/admin-import-standardized-competencies.spec.ts @@ -14,6 +14,7 @@ import { AdminStandardizedCompetencyService } from 'app/admin/standardized-compe import { HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { KnowledgeAreasForImportDTO } from 'app/entities/competency/standardized-competency.model'; +import { StandardizedCompetencyDetailComponent } from 'app/shared/standardized-competencies/standardized-competency-detail.component'; describe('ImportStandardizedCompetenciesComponent', () => { let componentFixture: ComponentFixture<AdminImportStandardizedCompetenciesComponent>; @@ -22,7 +23,13 @@ describe('ImportStandardizedCompetenciesComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, NgbCollapseMocksModule], - declarations: [AdminImportStandardizedCompetenciesComponent, MockPipe(HtmlForMarkdownPipe), KnowledgeAreaTreeStubComponent, MockComponent(ButtonComponent)], + declarations: [ + AdminImportStandardizedCompetenciesComponent, + MockPipe(HtmlForMarkdownPipe), + KnowledgeAreaTreeStubComponent, + MockComponent(ButtonComponent), + MockComponent(StandardizedCompetencyDetailComponent), + ], providers: [{ provide: Router, useClass: MockRouter }, MockProvider(AlertService)], }) .compileComponents() @@ -149,4 +156,23 @@ describe('ImportStandardizedCompetenciesComponent', () => { expect(component['isCollapsed']).toBeTrue(); }); + + it('should open details', () => { + const competencyToOpen = { id: 2, isVisible: true }; + const knowledgeAreaTitle = 'knowledgeArea'; + + component['openCompetencyDetails'](competencyToOpen, knowledgeAreaTitle); + + expect(component['selectedCompetency']).toEqual(competencyToOpen); + expect(component['knowledgeAreaTitle']).toEqual(knowledgeAreaTitle); + }); + + it('should close details', () => { + component['selectedCompetency'] = { id: 2, isVisible: true }; + + component['closeCompetencyDetails'](); + + expect(component['selectedCompetency']).toBeUndefined(); + expect(component['knowledgeAreaTitle']).toBe(''); + }); }); diff --git a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-group-detail.component.spec.ts b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-group-detail.component.spec.ts index f6e3f1db303b..1671d52febd5 100644 --- a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-group-detail.component.spec.ts +++ b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-group-detail.component.spec.ts @@ -5,15 +5,15 @@ import { TutorialGroupDetailStubComponent } from '../stubs/tutorial-group-detail import { TutorialGroupsService } from 'app/course/tutorial-groups/services/tutorial-groups.service'; import { MockProvider } from 'ng-mocks'; import { AlertService } from 'app/core/util/alert.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MockRouter } from '../../../helpers/mocks/mock-router'; import { generateExampleTutorialGroup } from '../helpers/tutorialGroupExampleModels'; import { HttpResponse } from '@angular/common/http'; import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { of } from 'rxjs'; -import { mockedActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route-query-param-map'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Course } from 'app/entities/course.model'; +import { ArtemisTestModule } from '../../../test.module'; describe('CourseTutorialGroupDetailComponent', () => { let fixture: ComponentFixture<CourseTutorialGroupDetailComponent>; @@ -25,18 +25,20 @@ describe('CourseTutorialGroupDetailComponent', () => { let findStub: jest.SpyInstance; let findByIdStub: jest.SpyInstance; + const parentParams = { courseId: 2 }; + const parentRoute = { parent: { params: of(parentParams) } } as any as ActivatedRoute; + const route = { params: of({ tutorialGroupId: 1 }), parent: parentRoute } as any as ActivatedRoute; + beforeEach(() => { TestBed.configureTestingModule({ + imports: [ArtemisTestModule], declarations: [CourseTutorialGroupDetailComponent, TutorialGroupDetailStubComponent, LoadingIndicatorContainerStubComponent], providers: [ MockProvider(TutorialGroupsService), MockProvider(CourseManagementService), MockProvider(AlertService), { provide: Router, useClass: MockRouter }, - mockedActivatedRoute({ - courseId: 2, - tutorialGroupId: 1, - }), + { provide: ActivatedRoute, useValue: route }, ], }) .compileComponents() diff --git a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-overview.component.spec.ts b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-overview.component.spec.ts deleted file mode 100644 index 060a1ee46550..000000000000 --- a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-overview.component.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CourseTutorialGroupsOverviewComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups-overview/course-tutorial-groups-overview.component'; -import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; -import { TutorialGroupsService } from 'app/course/tutorial-groups/services/tutorial-groups.service'; -import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { TutorialGroupsTableStubComponent } from '../stubs/tutorial-groups-table-stub.component'; -import { MockPipe, MockProvider } from 'ng-mocks'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { AlertService } from 'app/core/util/alert.service'; -import { Router } from '@angular/router'; -import { generateExampleTutorialGroup } from '../helpers/tutorialGroupExampleModels'; -import { By } from '@angular/platform-browser'; -import { Course } from 'app/entities/course.model'; - -describe('CourseTutorialGroupsOverviewComponent', () => { - let fixture: ComponentFixture<CourseTutorialGroupsOverviewComponent>; - let component: CourseTutorialGroupsOverviewComponent; - - let tutorialGroupTwo: TutorialGroup; - let tutorialGroupOne: TutorialGroup; - const course = { id: 1 } as Course; - - const router = new MockRouter(); - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [CourseTutorialGroupsOverviewComponent, TutorialGroupsTableStubComponent, MockPipe(ArtemisTranslatePipe)], - providers: [MockProvider(TutorialGroupsService), MockProvider(AlertService), { provide: Router, useValue: router }], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(CourseTutorialGroupsOverviewComponent); - component = fixture.componentInstance; - tutorialGroupOne = generateExampleTutorialGroup({ id: 1, numberOfRegisteredUsers: 5 }); - tutorialGroupTwo = generateExampleTutorialGroup({ id: 2, numberOfRegisteredUsers: 10 }); - component.tutorialGroups = [tutorialGroupOne, tutorialGroupTwo]; - component.course = course; - fixture.detectChanges(); - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should initialize', () => { - expect(component).not.toBeNull(); - }); - - it('should pass the tutorial group and course id to the table', () => { - const tableComponentInstance = fixture.debugElement.query(By.directive(TutorialGroupsTableStubComponent)).componentInstance; - expect(tableComponentInstance).not.toBeNull(); - expect(tableComponentInstance.tutorialGroups).toEqual([tutorialGroupOne, tutorialGroupTwo]); - expect(tableComponentInstance.course).toEqual(course); - }); -}); diff --git a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-registered.component.spec.ts b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-registered.component.spec.ts deleted file mode 100644 index 1aae0089b3f3..000000000000 --- a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups-registered.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CourseTutorialGroupsRegisteredComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups-registered/course-tutorial-groups-registered.component'; -import { Component, Input } from '@angular/core'; -import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; -import { MockPipe } from 'ng-mocks'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { generateExampleTutorialGroup } from '../helpers/tutorialGroupExampleModels'; -import { Course } from 'app/entities/course.model'; - -@Component({ selector: 'jhi-course-tutorial-group-card', template: '' }) -class MockCourseTutorialGroupCardComponent { - @Input() - course: Course; - @Input() - tutorialGroup: TutorialGroup; - - @Input() - showChannelLink = false; -} - -describe('CourseTutorialGroupsRegisteredComponent', () => { - let component: CourseTutorialGroupsRegisteredComponent; - let fixture: ComponentFixture<CourseTutorialGroupsRegisteredComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CourseTutorialGroupsRegisteredComponent, MockCourseTutorialGroupCardComponent, MockPipe(ArtemisTranslatePipe)], - }).compileComponents(); - - fixture = TestBed.createComponent(CourseTutorialGroupsRegisteredComponent); - component = fixture.componentInstance; - component.course = { id: 1, postsEnabled: true } as Course; - component.registeredTutorialGroups = [generateExampleTutorialGroup({})]; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups.component.spec.ts b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups.component.spec.ts index 9b4638d33de1..7a722cf69505 100644 --- a/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups.component.spec.ts +++ b/src/test/javascript/spec/component/tutorial-groups/course-tutorial-groups/course-tutorial-groups.component.spec.ts @@ -1,48 +1,23 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, Input } from '@angular/core'; import { TutorialGroup } from 'app/entities/tutorial-group/tutorial-group.model'; import { TutorialGroupsService } from 'app/course/tutorial-groups/services/tutorial-groups.service'; import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { AlertService } from 'app/core/util/alert.service'; -import { ActivatedRoute, Params, Router, convertToParamMap } from '@angular/router'; +import { ActivatedRoute, Router, RouterModule, convertToParamMap } from '@angular/router'; import { generateExampleTutorialGroup } from '../helpers/tutorialGroupExampleModels'; import { CourseTutorialGroupsComponent } from 'app/overview/course-tutorial-groups/course-tutorial-groups.component'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { BehaviorSubject, of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; import { Course } from 'app/entities/course.model'; -import { runOnPushChangeDetection } from '../../../helpers/on-push-change-detection.helper'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; -import { TutorialGroupsConfiguration } from 'app/entities/tutorial-group/tutorial-groups-configuration.model'; - -@Component({ - selector: 'jhi-course-tutorial-groups-overview', - template: '<div id="tutorialGroupsOverview">Hello World :)</div>', -}) -class MockCourseTutorialGroupsOverviewComponent { - @Input() - course: Course; - @Input() - tutorialGroups: TutorialGroup[] = []; - @Input() - configuration?: TutorialGroupsConfiguration; -} - -@Component({ - selector: 'jhi-course-tutorial-groups-registered', - template: '<div id="registeredTutorialGroups">Hello World ;)</div>', -}) -class MockCourseTutorialGroupsRegisteredComponent { - @Input() - registeredTutorialGroups: TutorialGroup[] = []; - @Input() - course: Course; - - @Input() - configuration?: TutorialGroupsConfiguration; -} +import { ArtemisTestModule } from '../../../test.module'; +import { SidebarComponent } from 'app/shared/sidebar/sidebar.component'; +import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; +import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; describe('CourseTutorialGroupsComponent', () => { let fixture: ComponentFixture<CourseTutorialGroupsComponent>; @@ -50,18 +25,15 @@ describe('CourseTutorialGroupsComponent', () => { let tutorialGroupOne: TutorialGroup; let tutorialGroupTwo: TutorialGroup; - let tutorialGroupThree: TutorialGroup; const router = new MockRouter(); - let queryParamsSubject: BehaviorSubject<Params>; - beforeEach(() => { router.navigate.mockImplementation(() => Promise.resolve(true)); - queryParamsSubject = new BehaviorSubject(convertToParamMap({})); TestBed.configureTestingModule({ - declarations: [CourseTutorialGroupsComponent, MockCourseTutorialGroupsOverviewComponent, MockCourseTutorialGroupsRegisteredComponent, MockPipe(ArtemisTranslatePipe)], + imports: [ArtemisTestModule, RouterModule, MockModule(FormsModule), MockModule(ReactiveFormsModule)], + declarations: [CourseTutorialGroupsComponent, MockPipe(ArtemisTranslatePipe), SidebarComponent, SearchFilterComponent, MockPipe(SearchFilterPipe)], providers: [ MockProvider(TutorialGroupsService), MockProvider(CourseStorageService), @@ -72,15 +44,13 @@ describe('CourseTutorialGroupsComponent', () => { provide: ActivatedRoute, useValue: { parent: { - parent: { - paramMap: new BehaviorSubject( - convertToParamMap({ - courseId: 1, - }), - ), - }, + paramMap: new BehaviorSubject( + convertToParamMap({ + courseId: 1, + }), + ), }, - queryParams: queryParamsSubject, + params: of({ tutorialGroupId: 5 }), }, }, ], @@ -89,9 +59,9 @@ describe('CourseTutorialGroupsComponent', () => { .then(() => { fixture = TestBed.createComponent(CourseTutorialGroupsComponent); component = fixture.componentInstance; + component.sidebarData = { groupByCategory: true, sidebarType: 'default', storageId: 'tutorialGroup' }; tutorialGroupOne = generateExampleTutorialGroup({ id: 1, isUserTutor: true }); tutorialGroupTwo = generateExampleTutorialGroup({ id: 2, isUserRegistered: true }); - tutorialGroupThree = generateExampleTutorialGroup({ id: 3 }); }); }); @@ -144,38 +114,4 @@ describe('CourseTutorialGroupsComponent', () => { expect(getAllOfCourseSpy).not.toHaveBeenCalled(); expect(updateCourseSpy).not.toHaveBeenCalled(); }); - - it('should set the filter depending on the query param', () => { - fixture.detectChanges(); - queryParamsSubject.next({ filter: 'all' }); - expect(component.selectedFilter).toBe('all'); - queryParamsSubject.next({ filter: 'registered' }); - runOnPushChangeDetection(fixture); - expect(component.selectedFilter).toBe('registered'); - }); - - it('should set the query params when a different filter is selected', () => { - fixture.detectChanges(); - component.onFilterChange('all'); - const activatedRoute = TestBed.inject(ActivatedRoute); - const navigateSpy = jest.spyOn(router, 'navigate'); - expect(navigateSpy).toHaveBeenCalledWith([], { - relativeTo: activatedRoute, - queryParams: { filter: 'all' }, - queryParamsHandling: 'merge', - replaceUrl: true, - }); - }); - - it('should filter registered tutorial groups for student', () => { - component.tutorialGroups = [tutorialGroupOne, tutorialGroupTwo, tutorialGroupThree]; - component.course = { id: 1, title: 'Test Course' } as Course; - expect(component.registeredTutorialGroups).toEqual([tutorialGroupTwo]); - }); - - it('should filter registered tutorial groups for tutor', () => { - component.tutorialGroups = [tutorialGroupOne, tutorialGroupTwo, tutorialGroupThree]; - component.course = { id: 1, title: 'Test Course', isAtLeastTutor: true } as Course; - expect(component.registeredTutorialGroups).toEqual([tutorialGroupOne]); - }); }); diff --git a/src/test/javascript/spec/entities/feedback.model.spec.ts b/src/test/javascript/spec/entities/feedback.model.spec.ts index 581c85084525..53a9e818208d 100644 --- a/src/test/javascript/spec/entities/feedback.model.spec.ts +++ b/src/test/javascript/spec/entities/feedback.model.spec.ts @@ -2,6 +2,7 @@ import { Feedback, FeedbackSuggestionType, FeedbackType, + NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER, STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER, SUBMISSION_POLICY_FEEDBACK_IDENTIFIER, buildFeedbackTextForReview, @@ -159,5 +160,10 @@ describe('Feedback', () => { const feedback: Feedback = { type: FeedbackType.AUTOMATIC, detailText: 'content', testCase: { testName: 'test1' } }; expect(Feedback.isTestCaseFeedback(feedback)).toBeTrue(); }); + + it('should correctly detect non graded automatically generated feedback', () => { + const feedback: Feedback = { type: FeedbackType.AUTOMATIC, text: NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER + 'content' }; + expect(Feedback.isNonGradedFeedbackSuggestion(feedback)).toBeTrue(); + }); }); }); diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-lti-configuration-service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-lti-configuration-service.ts index 5da5edcbbf7f..8fae6e8aa43a 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-lti-configuration-service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-lti-configuration-service.ts @@ -1,5 +1,6 @@ -import { of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; +import { HttpResponse } from '@angular/common/http'; export class MockLtiConfigurationService { private dummyLtiPlatforms: LtiPlatformConfiguration[] = [ @@ -25,8 +26,13 @@ export class MockLtiConfigurationService { }, ]; - public findAll() { - return of(this.dummyLtiPlatforms); + public query(req: any): Observable<HttpResponse<LtiPlatformConfiguration[]>> { + return of( + new HttpResponse({ + body: this.dummyLtiPlatforms, + status: 200, // Assuming a successful response + }), + ); } public updateLtiPlatformConfiguration(config: LtiPlatformConfiguration) { diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts index b3a397f64b44..e50b444b40bf 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts @@ -11,6 +11,7 @@ export class MockProgrammingExerciseService { getProgrammingExerciseTestCaseState = (exerciseId: number) => of({ body: { released: true, hasStudentResult: true, testCasesChanged: false } }); exportInstructorExercise = (exerciseId: number) => of({ body: undefined }); exportInstructorRepository = (exerciseId: number, repositoryType: ProgrammingExerciseInstructorRepositoryType) => of({ body: undefined }); + exportStudentRepository = (exerciseId: number, participationId: number) => of({ body: undefined }); exportStudentRequestedRepository = (exerciseId: number, includeTests: boolean) => of({ body: undefined }); getTasksAndTestsExtractedFromProblemStatement = (exerciseId: number) => of(); deleteTasksWithSolutionEntries = (exerciseId: number) => of(); diff --git a/src/test/javascript/spec/service/lti-configuration.service.spec.ts b/src/test/javascript/spec/service/lti-configuration.service.spec.ts index 13f3aa85e496..5fafdbdb34da 100644 --- a/src/test/javascript/spec/service/lti-configuration.service.spec.ts +++ b/src/test/javascript/spec/service/lti-configuration.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { LtiPlatformConfiguration } from 'app/admin/lti-configuration/lti-configuration.model'; import { LtiConfigurationService } from 'app/admin/lti-configuration/lti-configuration.service'; +import { ITEMS_PER_PAGE } from 'app/shared/constants/pagination.constants'; +import { HttpErrorResponse } from '@angular/common/http'; describe('LtiConfigurationService', () => { let service: LtiConfigurationService; @@ -56,12 +58,18 @@ describe('LtiConfigurationService', () => { }, ]; - service.findAll().subscribe((platforms) => { - expect(platforms.length).toHaveLength(2); - expect(platforms).toEqual(dummyLtiPlatforms); - }); + service + .query({ + page: 0, + size: ITEMS_PER_PAGE, + sort: ['id', 'desc'], + }) + .subscribe((platforms) => { + expect(platforms.body?.length).toHaveLength(2); + expect(platforms).toEqual(dummyLtiPlatforms); + }); - const req = httpMock.expectOne('api/lti-platforms'); + const req = httpMock.expectOne('api/lti-platforms?page=0&size=50&sort=id&sort=desc'); expect(req.request.method).toBe('GET'); req.flush(dummyLtiPlatforms); }); @@ -117,4 +125,31 @@ describe('LtiConfigurationService', () => { expect(req.request.method).toBe('POST'); req.flush(dummyResponse); }); + + it('should query with different sorting parameters', () => { + service.query({ sort: ['name,asc', 'id,desc'] }).subscribe(); + const req = httpMock.expectOne('api/lti-platforms?sort=name,asc&sort=id,desc'); + expect(req.request.method).toBe('GET'); + req.flush([]); + }); + + it('should handle errors when querying LTI platforms', () => { + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: { message: 'No data found' }, + }); + + service.query({ page: 0, size: ITEMS_PER_PAGE, sort: ['id', 'desc'] }).subscribe({ + next: () => {}, + error: (error) => { + expect(error.status).toBe(404); + expect(error.message).toContain('No data found'); + }, + }); + + const req = httpMock.expectOne('api/lti-platforms?page=0&size=50&sort=id&sort=desc'); + expect(req.request.method).toBe('GET'); + req.flush(null, errorResponse); + }); }); diff --git a/src/test/javascript/spec/service/programming-exercise.service.spec.ts b/src/test/javascript/spec/service/programming-exercise.service.spec.ts index 28bb9b1da21b..b5b04c245db4 100644 --- a/src/test/javascript/spec/service/programming-exercise.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise.service.spec.ts @@ -391,6 +391,16 @@ describe('ProgrammingExercise Service', () => { tick(); })); + it('should export a student repository', fakeAsync(() => { + const exerciseId = 1; + const participationId = 5; + service.exportStudentRepository(exerciseId, participationId).subscribe(); + const url = `${resourceUrl}/${exerciseId}/export-student-repository/${participationId}`; + const req = httpMock.expectOne({ method: 'GET', url }); + req.flush(new Blob()); + tick(); + })); + it('should check plagiarism report', fakeAsync(() => { const exerciseId = 1; service.checkPlagiarismJPlagReport(exerciseId).subscribe(); diff --git a/src/test/javascript/spec/service/result.service.spec.ts b/src/test/javascript/spec/service/result.service.spec.ts index 9039e8257821..7e112a991554 100644 --- a/src/test/javascript/spec/service/result.service.spec.ts +++ b/src/test/javascript/spec/service/result.service.spec.ts @@ -20,8 +20,14 @@ import { MockAccountService } from '../helpers/mocks/service/mock-account.servic import { SubmissionService } from 'app/exercises/shared/submission/submission.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; -import { FeedbackType, STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER, SUBMISSION_POLICY_FEEDBACK_IDENTIFIER } from 'app/entities/feedback.model'; +import { + FeedbackType, + NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER, + STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER, + SUBMISSION_POLICY_FEEDBACK_IDENTIFIER, +} from 'app/entities/feedback.model'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; +import * as Sentry from '@sentry/angular-ivy'; // Preliminary mock before import to prevent errors jest.mock('@sentry/angular-ivy', () => { const originalModule = jest.requireActual('@sentry/angular-ivy'); @@ -30,7 +36,6 @@ jest.mock('@sentry/angular-ivy', () => { captureException: jest.fn(), }; }); -import * as Sentry from '@sentry/angular-ivy'; describe('ResultService', () => { let resultService: ResultService; @@ -84,6 +89,34 @@ describe('ResultService', () => { completionDate: dayjs().subtract(5, 'minutes'), score: 80, }; + const result6: Result = { + feedbacks: [{ text: NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER + 'AI feedback', type: FeedbackType.AUTOMATIC }], + participation: { type: ParticipationType.PROGRAMMING }, + completionDate: dayjs().subtract(5, 'minutes'), + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: true, + }; + const result7: Result = { + feedbacks: [{ text: NON_GRADED_FEEDBACK_SUGGESTION_IDENTIFIER + 'AI feedback', type: FeedbackType.AUTOMATIC }], + participation: { type: ParticipationType.PROGRAMMING }, + completionDate: dayjs().subtract(5, 'minutes'), + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: false, + }; + const result8: Result = { + feedbacks: [], + participation: { type: ParticipationType.PROGRAMMING }, + completionDate: dayjs().subtract(5, 'minutes'), + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: undefined, + }; + const result9: Result = { + feedbacks: [], + participation: { type: ParticipationType.PROGRAMMING }, + completionDate: dayjs().add(5, 'minutes'), + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: undefined, + }; const modelingExercise: ModelingExercise = { maxPoints: 50, @@ -270,6 +303,36 @@ describe('ResultService', () => { expect(translateServiceSpy).toHaveBeenCalledWith('artemisApp.result.preliminary'); }); + it('should return correct string for Athena non graded successful feedback', () => { + programmingExercise.assessmentDueDate = dayjs().subtract(5, 'minutes'); + + expect(resultService.getResultString(result6, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); + expect(translateServiceSpy).toHaveBeenCalledOnce(); + }); + + it('should return correct string for Athena non graded unsuccessful feedback', () => { + programmingExercise.assessmentDueDate = dayjs().subtract(5, 'minutes'); + + expect(resultService.getResultString(result7, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackFailed'); + expect(translateServiceSpy).toHaveBeenCalledOnce(); + }); + + it('should return correct string for Athena timed out non graded feedback', () => { + programmingExercise.assessmentDueDate = dayjs().add(5, 'minutes'); + + expect(resultService.getResultString(result8, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackTimedOut'); + expect(translateServiceSpy).toHaveBeenCalledOnce(); + expect(translateServiceSpy).toHaveBeenCalledWith('artemisApp.result.resultString.automaticAIFeedbackTimedOut'); + }); + + it('should return correct string for in progress Athena non-graded feedback', () => { + programmingExercise.assessmentDueDate = dayjs().add(5, 'minutes'); + + expect(resultService.getResultString(result9, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackInProgress'); + expect(translateServiceSpy).toHaveBeenCalledOnce(); + expect(translateServiceSpy).toHaveBeenCalledWith('artemisApp.result.resultString.automaticAIFeedbackInProgress'); + }); + it('reports to Sentry if result or exercise is undefined', () => { // Re-mock to get reference because direct import doesn't work here const captureExceptionSpy = jest.spyOn(Sentry, 'captureException'); diff --git a/src/test/playwright/e2e/course/CourseCommunication.spec.ts b/src/test/playwright/e2e/course/CourseCommunication.spec.ts index cce1368b3bbf..42150299dd72 100644 --- a/src/test/playwright/e2e/course/CourseCommunication.spec.ts +++ b/src/test/playwright/e2e/course/CourseCommunication.spec.ts @@ -42,7 +42,6 @@ courseConfigsToTest.forEach((configToTest) => { }); test('Instructor should be able to select answer', async ({ page, login, communicationAPIRequests, courseCommunication }) => { - test.fixme(); const content = 'Answer Post Content'; await login(studentOne, `/courses/${course.id}/discussion`); const post = await communicationAPIRequests.createCourseWideMessage(course, courseWideRandomChannel.id!, content); diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index adfb0219d6fc..c273cd5440f9 100644 --- a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts +++ b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts @@ -164,11 +164,10 @@ test.describe('Exam assessment', () => { }); test('Assesses quiz automatically', async ({ page, login, examManagement, courseAssessment, examParticipation }) => { - test.fixme(); await login(instructor); await examManagement.verifySubmitted(course.id!, exam.id!, studentOneName); if (dayjs().isBefore(examEnd)) { - await page.waitForTimeout(examEnd.diff(dayjs(), 'ms') + 1000); + await page.waitForTimeout(examEnd.diff(dayjs(), 'ms') + 10000); } await examManagement.openAssessmentDashboard(course.id!, exam.id!, 60000); await page.goto(`/course-management/${course.id}/exams/${exam.id}/assessment-dashboard`); @@ -177,7 +176,7 @@ test.describe('Exam assessment', () => { if (dayjs().isBefore(resultDate)) { await page.waitForTimeout(resultDate.diff(dayjs(), 'ms') + 1000); } - await examManagement.checkQuizSubmission(course.id!, exam.id!, studentOneName, '50%'); + await examManagement.checkQuizSubmission(course.id!, exam.id!, studentOneName, '[5 / 10 Points] 50%'); await login(studentOne, `/courses/${course.id}/exams/${exam.id}`); await examParticipation.checkResultScore('50%'); }); @@ -296,7 +295,7 @@ test.afterAll('Delete course', async ({ browser }) => { await courseManagementAPIRequests.deleteCourse(course, admin); }); -async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page): Promise<Exam> { +export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page): Promise<Exam> { const examAPIRequests = new ExamAPIRequests(page); const exerciseAPIRequests = new ExerciseAPIRequests(page); const examExerciseGroupCreation = new ExamExerciseGroupCreationPage(page, examAPIRequests, exerciseAPIRequests); diff --git a/src/test/playwright/e2e/exam/ExamResults.spec.ts b/src/test/playwright/e2e/exam/ExamResults.spec.ts new file mode 100644 index 000000000000..9823f62846c1 --- /dev/null +++ b/src/test/playwright/e2e/exam/ExamResults.spec.ts @@ -0,0 +1,216 @@ +import { test } from '../../support/fixtures'; +import { Exam } from 'app/entities/exam.model'; +import { Commands } from '../../support/commands'; +import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor } from '../../support/users'; +import { Course } from 'app/entities/course.model'; +import dayjs from 'dayjs'; +import { generateUUID } from '../../support/utils'; +import { Exercise, ExerciseType } from '../../support/constants'; +import { ExamManagementPage } from '../../support/pageobjects/exam/ExamManagementPage'; +import { CourseAssessmentDashboardPage } from '../../support/pageobjects/assessment/CourseAssessmentDashboardPage'; +import { ExerciseAssessmentDashboardPage } from '../../support/pageobjects/assessment/ExerciseAssessmentDashboardPage'; +import { ExamAssessmentPage } from '../../support/pageobjects/assessment/ExamAssessmentPage'; +import javaPartiallySuccessfulSubmission from '../../fixtures/exercise/programming/java/partially_successful/submission.json'; +import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; +import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; +import { ExerciseAPIRequests } from '../../support/requests/ExerciseAPIRequests'; +import { ExamParticipation } from '../../support/pageobjects/exam/ExamParticipation'; +import { CourseManagementAPIRequests } from '../../support/requests/CourseManagementAPIRequests'; +import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar'; +import { CourseOverviewPage } from '../../support/pageobjects/course/CourseOverviewPage'; +import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; +import { CoursesPage } from '../../support/pageobjects/course/CoursesPage'; +import { ModelingEditor } from '../../support/pageobjects/exercises/modeling/ModelingEditor'; +import { OnlineEditorPage } from '../../support/pageobjects/exercises/programming/OnlineEditorPage'; +import { MultipleChoiceQuiz } from '../../support/pageobjects/exercises/quiz/MultipleChoiceQuiz'; +import { TextEditorPage } from '../../support/pageobjects/exercises/text/TextEditorPage'; +import { ModelingExerciseAssessmentEditor } from '../../support/pageobjects/assessment/ModelingExerciseAssessmentEditor'; +import { ProgrammingExerciseTaskStatus } from '../../support/pageobjects/exam/ExamResultsPage'; + +test.describe.configure({ mode: 'default' }); + +test.describe('Exam Results', () => { + let course: Course; + + test.beforeAll('Create course', async ({ browser }) => { + const page = await browser.newPage(); + const courseManagementAPIRequests = new CourseManagementAPIRequests(page); + + await Commands.login(page, admin); + course = await courseManagementAPIRequests.createCourse({ customizeGroups: true }); + await courseManagementAPIRequests.addStudentToCourse(course, studentOne); + await courseManagementAPIRequests.addStudentToCourse(course, studentTwo); + await courseManagementAPIRequests.addStudentToCourse(course, studentThree); + await courseManagementAPIRequests.addStudentToCourse(course, studentFour); + await courseManagementAPIRequests.addTutorToCourse(course, tutor); + await courseManagementAPIRequests.addInstructorToCourse(course, instructor); + }); + + test.describe('Check exam exercise results', () => { + let exam: Exam; + let exerciseArray: Array<Exercise> = []; + + test.beforeAll('Prepare exam and assess a student submission', async ({ browser }) => { + const page = await browser.newPage(); + const examAPIRequests = new ExamAPIRequests(page); + const exerciseAPIRequests = new ExerciseAPIRequests(page); + const examExerciseGroupCreation = new ExamExerciseGroupCreationPage(page, examAPIRequests, exerciseAPIRequests); + + await Commands.login(page, admin); + const endDate = dayjs().add(1, 'minutes').add(30, 'seconds'); + const examConfig = { + course, + title: 'exam' + generateUUID(), + visibleDate: dayjs().subtract(3, 'minutes'), + startDate: dayjs().subtract(2, 'minutes'), + endDate: endDate, + publishResultsDate: endDate.add(1, 'seconds'), + examMaxPoints: 40, + numberOfExercisesInExam: 4, + }; + exam = await examAPIRequests.createExam(examConfig); + const textExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture: 'loremIpsum.txt' }); + const programmingExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.PROGRAMMING, { submission: javaPartiallySuccessfulSubmission }); + const quizExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.QUIZ, { quizExerciseID: 0 }); + const modelingExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.MODELING); + exerciseArray = [textExercise, programmingExercise, quizExercise, modelingExercise]; + + await examAPIRequests.registerStudentForExam(exam, studentOne); + await examAPIRequests.generateMissingIndividualExams(exam); + await examAPIRequests.prepareExerciseStartForExam(exam); + const courseList = new CoursesPage(page); + const courseOverview = new CourseOverviewPage(page); + + const examParticipation = new ExamParticipation( + courseList, + courseOverview, + new ExamNavigationBar(page), + new ExamStartEndPage(page), + new ModelingEditor(page), + new OnlineEditorPage(page, courseList, courseOverview), + new MultipleChoiceQuiz(page), + new TextEditorPage(page), + page, + ); + + await examParticipation.startParticipation(studentOne, course, exam); + + const examNavigation = new ExamNavigationBar(page); + const examStartEnd = new ExamStartEndPage(page); + + for (let j = 0; j < exerciseArray.length; j++) { + const exercise = exerciseArray[j]; + await examNavigation.openExerciseAtIndex(j); + await examParticipation.makeSubmission(exercise.id!, exercise.type!, exercise.additionalData); + } + + await examParticipation.handInEarly(); + await examStartEnd.pressShowSummary(); + }); + + test.beforeAll('Assess student submissions', async ({ browser }) => { + const page = await browser.newPage(); + const examManagement = new ExamManagementPage(page); + const examAssessment = new ExamAssessmentPage(page); + const courseAssessment = new CourseAssessmentDashboardPage(page); + const exerciseAssessment = new ExerciseAssessmentDashboardPage(page); + const exerciseAPIRequests = new ExerciseAPIRequests(page); + + await Commands.login(page, tutor); + await startAssessing(course.id!, exam.id!, 0, 60000, examManagement, courseAssessment, exerciseAssessment); + await examAssessment.addNewFeedback(7, 'Good job'); + await examAssessment.submitTextAssessment(); + await startAssessing(course.id!, exam.id!, 1, 60000, examManagement, courseAssessment, exerciseAssessment); + + const modelingExerciseAssessment = new ModelingExerciseAssessmentEditor(page); + await modelingExerciseAssessment.addNewFeedback(5, 'Good'); + await modelingExerciseAssessment.openAssessmentForComponent(0); + await modelingExerciseAssessment.assessComponent(-1, 'Wrong'); + await modelingExerciseAssessment.clickNextAssessment(); + await modelingExerciseAssessment.assessComponent(0, 'Neutral'); + await modelingExerciseAssessment.clickNextAssessment(); + await examAssessment.submitModelingAssessment(); + await Commands.login(page, instructor); + await exerciseAPIRequests.evaluateExamQuizzes(exam); + }); + + test('Check exam result overview', async ({ page, login, examAPIRequests, examResultsPage }) => { + await login(studentOne); + await page.goto(`/courses/${course.id}/exams/${exam.id}`); + const gradeSummary = await examAPIRequests.getGradeSummary(exam); + await examResultsPage.checkGradeSummary(gradeSummary); + }); + + test('Check exam text exercise results', async ({ page, login, examParticipation, examResultsPage }) => { + await login(studentOne); + await page.goto(`/courses/${course.id}/exams/${exam.id}`); + const exercise = exerciseArray[0]; + await examParticipation.checkResultScore('70%', exercise.id!); + await examResultsPage.checkTextExerciseContent(exercise.id!, exercise.additionalData!.textFixture!); + await examResultsPage.checkAdditionalFeedback(exercise.id!, 7, 'Good job'); + }); + + test('Check exam programming exercise results', async ({ page, login, examParticipation, examResultsPage }) => { + test.fixme(); + await login(studentOne); + await page.goto(`/courses/${course.id}/exams/${exam.id}`); + const exercise = exerciseArray[1]; + await examParticipation.checkResultScore('46.2%', exercise.id!); + await examResultsPage.checkProgrammingExerciseAssessments(exercise.id!, 'Wrong', 7); + await examResultsPage.checkProgrammingExerciseAssessments(exercise.id!, 'Correct', 6); + const taskStatuses: ProgrammingExerciseTaskStatus[] = [ + ProgrammingExerciseTaskStatus.SUCCESS, + ProgrammingExerciseTaskStatus.SUCCESS, + ProgrammingExerciseTaskStatus.SUCCESS, + ProgrammingExerciseTaskStatus.FAILURE, + ProgrammingExerciseTaskStatus.FAILURE, + ProgrammingExerciseTaskStatus.FAILURE, + ProgrammingExerciseTaskStatus.FAILURE, + ]; + await examResultsPage.checkProgrammingExerciseTasks(exercise.id!, taskStatuses); + }); + + test('Check exam quiz exercise results', async ({ page, login, examParticipation, examResultsPage }) => { + await login(studentOne); + await page.goto(`/courses/${course.id}/exams/${exam.id}`); + const exercise = exerciseArray[2]; + await examParticipation.checkResultScore('50%', exercise.id!); + await examResultsPage.checkQuizExerciseScore(exercise.id!, 5, 10); + const studentAnswers = [true, false, true, false]; + const correctAnswers = [true, true, false, false]; + await examResultsPage.checkQuizExerciseAnswers(exercise.id!, studentAnswers, correctAnswers); + }); + + test('Check exam modelling exercise results', async ({ page, login, examParticipation, examResultsPage }) => { + await login(studentOne); + await page.goto(`/courses/${course.id}/exams/${exam.id}`); + const exercise = exerciseArray[3]; + await examParticipation.checkResultScore('40%', exercise.id!); + await examResultsPage.checkAdditionalFeedback(exercise.id!, 5, 'Good'); + await examResultsPage.checkModellingExerciseAssessment(exercise.id!, 'class Class', 'Wrong', -1); + await examResultsPage.checkModellingExerciseAssessment(exercise.id!, 'abstract class Abstract', 'Neutral', 0); + }); + }); + + test.afterAll('Delete course', async ({ browser }) => { + const page = await browser.newPage(); + const courseManagementAPIRequests = new CourseManagementAPIRequests(page); + await courseManagementAPIRequests.deleteCourse(course, admin); + }); +}); + +async function startAssessing( + courseID: number, + examID: number, + exerciseIndex: number = 0, + timeout: number, + examManagement: ExamManagementPage, + courseAssessment: CourseAssessmentDashboardPage, + exerciseAssessment: ExerciseAssessmentDashboardPage, +) { + await examManagement.openAssessmentDashboard(courseID, examID, timeout); + await courseAssessment.clickExerciseDashboardButton(exerciseIndex); + await exerciseAssessment.clickHaveReadInstructionsButton(); + await exerciseAssessment.clickStartNewAssessment(); + exerciseAssessment.getLockedMessage(); +} diff --git a/src/test/playwright/e2e/exercise/programming/ProgrammingExerciseManagement.spec.ts b/src/test/playwright/e2e/exercise/programming/ProgrammingExerciseManagement.spec.ts index 18b7cd4334b4..bd8bff6a31b1 100644 --- a/src/test/playwright/e2e/exercise/programming/ProgrammingExerciseManagement.spec.ts +++ b/src/test/playwright/e2e/exercise/programming/ProgrammingExerciseManagement.spec.ts @@ -1,11 +1,11 @@ import { Course } from 'app/entities/course.model'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; -import { admin } from '../../../support/users'; +import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor } from '../../../support/users'; import { test } from '../../../support/fixtures'; import { generateUUID } from '../../../support/utils'; -import { Exercise } from 'app/entities/exercise.model'; import { expect } from '@playwright/test'; +import { Exercise, ExerciseMode } from '../../../support/constants'; test.describe('Programming Exercise Management', () => { let course: Course; @@ -52,6 +52,54 @@ test.describe('Programming Exercise Management', () => { }); }); + test.describe('Programming exercise team creation', () => { + let exercise: ProgrammingExercise; + + test.beforeEach('Setup team programming exercise', async ({ login, exerciseAPIRequests }) => { + await login(admin); + const teamAssignmentConfig = { minTeamSize: 2, maxTeamSize: 3 }; + exercise = await exerciseAPIRequests.createProgrammingExercise({ + course, + mode: ExerciseMode.TEAM, + teamAssignmentConfig, + }); + }); + + test('Create an exercise team', async ({ login, page, navigationBar, courseManagement, courseManagementExercises, exerciseTeams, programmingExerciseOverview }) => { + await login(instructor, '/'); + await navigationBar.openCourseManagement(); + await courseManagement.openExercisesOfCourse(course.id!); + await courseManagementExercises.openExerciseTeams(exercise.id!); + await page.getByRole('table').waitFor({ state: 'visible' }); + await exerciseTeams.createTeam(); + + const teamId = generateUUID(); + const teamName = `Team ${teamId}`; + const teamShortName = `team${teamId}`; + await exerciseTeams.enterTeamName(teamName); + await exerciseTeams.enterTeamShortName(teamShortName); + await exerciseTeams.setTeamTutor(tutor.username); + + await exerciseTeams.addStudentToTeam(studentOne.username); + await expect(exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox()).toBeVisible(); + await expect(exerciseTeams.getSaveButton()).toBeDisabled(); + await exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox().check(); + await expect(exerciseTeams.getSaveButton()).toBeEnabled(); + await exerciseTeams.getIgnoreTeamSizeRecommendationCheckbox().uncheck(); + + await exerciseTeams.addStudentToTeam(studentTwo.username); + await exerciseTeams.addStudentToTeam(studentThree.username); + await expect(exerciseTeams.getSaveButton()).toBeEnabled(); + await exerciseTeams.getSaveButton().click(); + await exerciseTeams.checkTeamOnList(teamShortName); + + await login(studentOne, `/courses/${course.id}/exercises/${exercise.id}`); + await expect(programmingExerciseOverview.getExerciseDetails().locator('.view-team')).toBeVisible(); + await login(studentFour, `/courses/${course.id}/exercises/${exercise.id}`); + await expect(programmingExerciseOverview.getExerciseDetails()).toHaveText(/No team yet/); + }); + }); + test.afterEach('Delete course', async ({ courseManagementAPIRequests }) => { await courseManagementAPIRequests.deleteCourse(course, admin); }); diff --git a/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseManagement.spec.ts b/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseManagement.spec.ts index 58e89ee418b7..884ceb2c62c4 100644 --- a/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseManagement.spec.ts +++ b/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseManagement.spec.ts @@ -41,7 +41,7 @@ test.describe('Quiz Exercise Management', () => { await expect(page.getByText(title)).toBeVisible(); }); - test.skip('Creates a Quiz with Drag and Drop', async ({ page, quizExerciseCreation }) => { + test('Creates a Quiz with Drag and Drop', async ({ page, quizExerciseCreation }) => { const quizQuestionTitle = 'Quiz Question'; await quizExerciseCreation.addDragAndDropQuestion(quizQuestionTitle); const response = await quizExerciseCreation.saveQuiz(); diff --git a/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseParticipation.spec.ts b/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseParticipation.spec.ts index 014ee704dfe2..04e0724e17c5 100644 --- a/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseParticipation.spec.ts +++ b/src/test/playwright/e2e/exercise/quiz-exercise/QuizExerciseParticipation.spec.ts @@ -68,7 +68,7 @@ test.describe('Quiz Exercise Participation', () => { }); }); - test.describe.skip('DnD Quiz participation', () => { + test.describe('DnD Quiz participation', () => { test.beforeEach('Create DND quiz', async ({ login, courseManagementExercises, exerciseAPIRequests, quizExerciseCreation }) => { await login(admin, '/course-management/' + course.id + '/exercises'); await courseManagementExercises.createQuizExercise(); diff --git a/src/test/playwright/fixtures/exercise/programming/python/template.json b/src/test/playwright/fixtures/exercise/programming/python/template.json index 611d4377a73f..50230140fa4a 100644 --- a/src/test/playwright/fixtures/exercise/programming/python/template.json +++ b/src/test/playwright/fixtures/exercise/programming/python/template.json @@ -1,6 +1,6 @@ { "allowComplaintsForAutomaticAssessments": false, - "allowManualFeedbackRequests": false, + "allowFeedbackRequests": false, "allowOfflineIde": true, "allowOnlineEditor": true, "assessmentDueDateError": false, diff --git a/src/test/playwright/support/commands.ts b/src/test/playwright/support/commands.ts index 94d6460e2a4c..9b42d25c8060 100644 --- a/src/test/playwright/support/commands.ts +++ b/src/test/playwright/support/commands.ts @@ -1,6 +1,6 @@ import { UserCredentials } from './users'; import { BASE_API } from './constants'; -import { Page, expect } from '@playwright/test'; +import { Locator, Page, expect } from '@playwright/test'; /** * A class that encapsulates static helper command methods. @@ -48,18 +48,18 @@ export class Commands { await page.request.post(`${BASE_API}/public/logout`); }; - static reloadUntilFound = async (page: Page, selector: string, interval = 2000, timeout = 20000) => { + static reloadUntilFound = async (page: Page, locator: Locator, interval = 2000, timeout = 20000) => { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { - await page.waitForSelector(selector, { timeout: interval }); + await locator.waitFor({ state: 'visible', timeout: interval }); return; } catch (error) { await page.reload(); } } - throw new Error(`Timed out finding an element matching the "${selector}" selector`); + throw new Error(`Timed out finding an element matching the "${locator}" locator`); }; } diff --git a/src/test/playwright/support/constants.ts b/src/test/playwright/support/constants.ts index 5202eecb53cb..f8e02b52a06e 100644 --- a/src/test/playwright/support/constants.ts +++ b/src/test/playwright/support/constants.ts @@ -83,6 +83,12 @@ export type Exercise = { exerciseGroup?: ExerciseGroup; }; +// ExerciseMode +export enum ExerciseMode { + INDIVIDUAL = 'INDIVIDUAL', + TEAM = 'TEAM', +} + // Exercise commit entity displayed in commit history export type ExerciseCommit = { message: string; diff --git a/src/test/playwright/support/fixtures.ts b/src/test/playwright/support/fixtures.ts index f99b0c604f0b..0e0e9c49969f 100644 --- a/src/test/playwright/support/fixtures.ts +++ b/src/test/playwright/support/fixtures.ts @@ -60,6 +60,8 @@ import { ProgrammingExerciseOverviewPage } from './pageobjects/exercises/program import { RepositoryPage } from './pageobjects/exercises/programming/RepositoryPage'; import { ExamGradingPage } from './pageobjects/exam/ExamGradingPage'; import { ExamScoresPage } from './pageobjects/exam/ExamScoresPage'; +import { ExamResultsPage } from './pageobjects/exam/ExamResultsPage'; +import { ExerciseTeamsPage } from './pageobjects/exercises/ExerciseTeamsPage'; /* * Define custom types for fixtures @@ -96,6 +98,7 @@ export type ArtemisPageObjects = { examNavigation: ExamNavigationBar; examManagement: ExamManagementPage; examParticipation: ExamParticipation; + examResultsPage: ExamResultsPage; examScores: ExamScoresPage; examStartEnd: ExamStartEndPage; examTestRun: ExamTestRunPage; @@ -123,6 +126,7 @@ export type ArtemisPageObjects = { textExerciseExampleSubmissionCreation: TextExerciseExampleSubmissionCreationPage; textExerciseFeedback: TextExerciseFeedbackPage; exerciseResult: ExerciseResultPage; + exerciseTeams: ExerciseTeamsPage; }; export type ArtemisRequests = { @@ -238,6 +242,9 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe ), ); }, + examResultsPage: async ({ page }, use) => { + await use(new ExamResultsPage(page)); + }, examScores: async ({ page }, use) => { await use(new ExamScoresPage(page)); }, @@ -271,8 +278,8 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe programmingExerciseCreation: async ({ page }, use) => { await use(new ProgrammingExerciseCreationPage(page)); }, - programmingExerciseEditor: async ({ page, courseList, courseOverview }, use) => { - await use(new OnlineEditorPage(page, courseList, courseOverview)); + programmingExerciseEditor: async ({ page }, use) => { + await use(new OnlineEditorPage(page)); }, programmingExerciseFeedback: async ({ page }, use) => { await use(new ProgrammingExerciseFeedbackPage(page)); @@ -319,6 +326,9 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe exerciseResult: async ({ page }, use) => { await use(new ExerciseResultPage(page)); }, + exerciseTeams: async ({ page }, use) => { + await use(new ExerciseTeamsPage(page)); + }, courseManagementAPIRequests: async ({ page }, use) => { await use(new CourseManagementAPIRequests(page)); }, diff --git a/src/test/playwright/support/pageobjects/assessment/CourseAssessmentDashboardPage.ts b/src/test/playwright/support/pageobjects/assessment/CourseAssessmentDashboardPage.ts index c1d5eec7c612..a074710e6687 100644 --- a/src/test/playwright/support/pageobjects/assessment/CourseAssessmentDashboardPage.ts +++ b/src/test/playwright/support/pageobjects/assessment/CourseAssessmentDashboardPage.ts @@ -17,10 +17,11 @@ export class CourseAssessmentDashboardPage { await this.page.locator('#show-complaint').click(); } - async clickExerciseDashboardButton() { + async clickExerciseDashboardButton(exerciseIndex: number = 0) { // Sometimes the page does not load properly, so we reload it if the button is not found - await Commands.reloadUntilFound(this.page, '#open-exercise-dashboard'); - await this.page.locator('#open-exercise-dashboard').click(); + const openExerciseDashboardLocator = this.page.locator('#open-exercise-dashboard').nth(exerciseIndex); + await Commands.reloadUntilFound(this.page, openExerciseDashboardLocator); + await openExerciseDashboardLocator.click(); } async clickEvaluateQuizzes() { diff --git a/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts b/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts index 7e9205815a29..b501531c27f7 100644 --- a/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts +++ b/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts @@ -14,9 +14,9 @@ export class ExerciseAssessmentDashboardPage { } async clickStartNewAssessment() { - const startAssessingSelector = '#start-new-assessment'; - await Commands.reloadUntilFound(this.page, startAssessingSelector); - await this.page.locator(startAssessingSelector).click(); + const startAssessingButton = this.page.locator('#start-new-assessment'); + await Commands.reloadUntilFound(this.page, startAssessingButton); + await startAssessingButton.click(); } async clickOpenAssessment() { diff --git a/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts b/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts index 70c0d6fb1989..5e8495d6172f 100644 --- a/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseCommunicationPage.ts @@ -288,7 +288,7 @@ export class CourseCommunicationPage { */ async checkResolved(postID: number) { const pageLocator = this.getSinglePost(postID); - const messageResolvedIcon = pageLocator.locator(`fa-icon[ng-reflect-ngb-tooltip='Message has been resolved']`); + const messageResolvedIcon = pageLocator.locator(`.svg-inline--fa.fa-square-check`); await expect(messageResolvedIcon).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/course/CourseManagementExercisesPage.ts b/src/test/playwright/support/pageobjects/course/CourseManagementExercisesPage.ts index 355fd8eecc16..cec1b2cca513 100644 --- a/src/test/playwright/support/pageobjects/course/CourseManagementExercisesPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseManagementExercisesPage.ts @@ -136,4 +136,10 @@ export class CourseManagementExercisesPage { getModelingExerciseMaxPoints(exerciseID: number) { return this.page.locator(`#exercise-card-${exerciseID}`).locator(`#modeling-exercise-${exerciseID}-maxPoints`); } + + async openExerciseTeams(exerciseId: number) { + const exerciseElement = this.getExercise(exerciseId); + const teamsButton = exerciseElement.locator('.btn', { hasText: 'Teams' }); + await teamsButton.click(); + } } diff --git a/src/test/playwright/support/pageobjects/course/CourseOverviewPage.ts b/src/test/playwright/support/pageobjects/course/CourseOverviewPage.ts index 49bd6e339448..a0a14f4a5a21 100644 --- a/src/test/playwright/support/pageobjects/course/CourseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/course/CourseOverviewPage.ts @@ -77,4 +77,11 @@ export class CourseOverviewPage { async openExam(examId: number): Promise<void> { await this.page.locator(`#exam-${examId} .clickable`).click(); } + + /** + * Opens the team info for the exercise. + */ + async openTeam() { + await this.page.locator('.view-team').click(); + } } diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 56c4077b0546..f1988e8114cb 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -105,7 +105,7 @@ export class ExamManagementPage { await this.page.goto(`/course-management/${courseID}/exams/${examID}/student-exams`); await this.page.locator('#student-exam .datatable-body-row', { hasText: username }).locator('.view-submission').click(); await this.page.locator('.summery').click(); - await expect(this.page.locator('#exercise-result-score')).toHaveText(score); + await expect(this.page.locator('#exercise-result-score')).toHaveText(score, { useInnerText: true }); } async clickEdit() { diff --git a/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts b/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts index e6a9245981c1..9ae85065182c 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts @@ -127,13 +127,15 @@ export class ExamParticipation { await expect(this.page.locator('#exam-title')).toContainText(title); } - async getResultScore() { - await Commands.reloadUntilFound(this.page, '#exercise-result-score'); - return this.page.locator('#exercise-result-score'); + async getResultScore(exerciseID?: number) { + const parentComponent = exerciseID ? getExercise(this.page, exerciseID) : this.page; + const resultScoreLocator = parentComponent.locator('#exercise-result-score'); + await Commands.reloadUntilFound(this.page, resultScoreLocator); + return resultScoreLocator; } - async checkResultScore(scoreText: string) { - const scoreElement = await this.getResultScore(); + async checkResultScore(scoreText: string, exerciseID?: number) { + const scoreElement = await this.getResultScore(exerciseID); await expect(scoreElement.getByText(new RegExp(scoreText))).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/exam/ExamResultsPage.ts b/src/test/playwright/support/pageobjects/exam/ExamResultsPage.ts new file mode 100644 index 000000000000..77a78e007fb6 --- /dev/null +++ b/src/test/playwright/support/pageobjects/exam/ExamResultsPage.ts @@ -0,0 +1,105 @@ +import { Page, expect } from '@playwright/test'; +import { Fixtures } from '../../../fixtures/fixtures'; +import { getExercise } from '../../utils'; + +export class ExamResultsPage { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async checkGradeSummary(gradeSummary: any) { + const examSummary = this.page.locator('#exam-summary-result-overview .exam-points-summary-container'); + for (const exercise of gradeSummary.studentExam.exercises) { + const exerciseGroup = exercise.exerciseGroup; + const exerciseRow = examSummary.locator('tr', { hasText: exerciseGroup.title }); + + const exerciseResult = gradeSummary.studentResult.exerciseGroupIdToExerciseResult[exerciseGroup.id]; + const achievedPoints = Math.floor(exerciseResult.achievedPoints).toString(); + const achievablePoints = Math.floor(exerciseResult.maxScore).toString(); + const achievedPercentage = exerciseResult.achievedScore.toString(); + const bonusPoints = Math.floor(exercise.bonusPoints).toString(); + + await expect(exerciseRow.locator('td').nth(1).getByText(achievedPoints)).toBeVisible(); + await expect(exerciseRow.locator('td').nth(2).getByText(achievablePoints)).toBeVisible(); + await expect(exerciseRow.locator('td').nth(3).getByText(`${achievedPercentage} %`)).toBeVisible(); + await expect(exerciseRow.locator('td').nth(4).getByText(bonusPoints)).toBeVisible(); + } + } + + async checkTextExerciseContent(exerciseId: number, textFixture: string) { + const textExercise = getExercise(this.page, exerciseId); + const submissionText = await Fixtures.get(textFixture); + await expect(textExercise.locator('span', { hasText: submissionText })).toBeVisible(); + } + + async checkAdditionalFeedback(exerciseId: number, points: number, feedback: string) { + const exercise = getExercise(this.page, exerciseId); + const feedbackElement = exercise.locator(`#additional-feedback`); + await expect(feedbackElement.locator('.feedback-points', { hasText: points.toString() })).toBeVisible(); + await expect(feedbackElement.locator('span', { hasText: feedback })).toBeVisible(); + } + + async checkProgrammingExerciseAssessments(exerciseId: number, resultType: string, count: number) { + const exercise = getExercise(this.page, exerciseId); + const results = exercise.locator('.feedback-item-group', { hasText: resultType }); + await expect(results.getByText(`(${count})`)).toBeVisible(); + } + + async checkProgrammingExerciseTasks(exerciseId: number, taskFeedbacks: ProgrammingExerciseTaskStatus[]) { + const exercise = getExercise(this.page, exerciseId); + const tasks = exercise.locator('.stepwizard .stepwizard-step'); + for (let taskIndex = 0; taskIndex < taskFeedbacks.length; taskIndex++) { + const taskElement = tasks.nth(taskIndex); + switch (taskFeedbacks[taskIndex]) { + case ProgrammingExerciseTaskStatus.PENDING: + await expect(taskElement.locator('.stepwizard-step--no-result')).toBeVisible(); + break; + case ProgrammingExerciseTaskStatus.SUCCESS: + await expect(taskElement.locator('.stepwizard-step--success')).toBeVisible(); + break; + case ProgrammingExerciseTaskStatus.FAILURE: + await expect(taskElement.locator('.stepwizard-step--failed')).toBeVisible(); + break; + } + } + } + + async checkQuizExerciseScore(exerciseId: number, score: number, maxScore: number) { + const exercise = getExercise(this.page, exerciseId); + await expect(exercise.locator('.question-score').getByText(`${score}/${maxScore}`)).toBeVisible(); + } + + async checkQuizExerciseAnswers(exerciseId: number, studentAnswers: boolean[], correctAnswers: boolean[]) { + const exercise = getExercise(this.page, exerciseId); + + for (let i = 0; i < studentAnswers.length; i++) { + const selectedAnswer = exercise.locator('.selection').nth(i + 1); + if (studentAnswers[i]) { + await expect(selectedAnswer.locator('.fa-square-check')).toBeVisible(); + } + if (!studentAnswers[i]) { + await expect(selectedAnswer.locator('.fa-square')).toBeVisible(); + } + + const solution = exercise.locator('.solution').nth(i + 1); + await expect(solution).toHaveText(correctAnswers[i] ? 'Correct' : 'Wrong'); + } + } + + async checkModellingExerciseAssessment(exerciseId: number, element: string, feedback: string, points: number) { + const exercise = getExercise(this.page, exerciseId); + const componentFeedbacks = exercise.locator('#component-feedback-table'); + const assessmentRow = componentFeedbacks.locator('tr', { hasText: element }); + await expect(assessmentRow).toBeVisible(); + await expect(assessmentRow.getByText(`Feedback: ${feedback}`)).toBeVisible(); + await expect(assessmentRow.getByText(`${points}`)).toBeVisible(); + } +} + +export enum ProgrammingExerciseTaskStatus { + PENDING = 'pending', + SUCCESS = 'success', + FAILURE = 'failure', +} diff --git a/src/test/playwright/support/pageobjects/exercises/AbstractExerciseFeedbackPage.ts b/src/test/playwright/support/pageobjects/exercises/AbstractExerciseFeedbackPage.ts index 46c011a5d4bc..6a4a32da9a2f 100644 --- a/src/test/playwright/support/pageobjects/exercises/AbstractExerciseFeedbackPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/AbstractExerciseFeedbackPage.ts @@ -10,7 +10,6 @@ export abstract class AbstractExerciseFeedback { readonly resultSelector = '#result'; readonly additionalFeedbackSelector = '#additional-feedback'; - readonly complainButtonSelector = '#complain'; constructor(page: Page) { this.page = page; @@ -31,8 +30,9 @@ export abstract class AbstractExerciseFeedback { } async complain(complaint: string) { - await Commands.reloadUntilFound(this.page, this.complainButtonSelector); - await this.page.locator(this.complainButtonSelector).click(); + const complainButton = this.page.locator('#complain'); + await Commands.reloadUntilFound(this.page, complainButton); + await complainButton.click(); await this.page.locator('#complainTextArea').fill(complaint); const responsePromise = this.page.waitForResponse(`${BASE_API}/complaints`); await this.page.locator('#submit-complaint').click(); diff --git a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts index 5873d3f2da84..6f9da1803fa2 100644 --- a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts @@ -26,7 +26,7 @@ export class ExerciseResultPage { } async shouldShowScore(percentage: number) { - await Commands.reloadUntilFound(this.page, '#submission-result-graded'); + await Commands.reloadUntilFound(this.page, this.page.locator('#submission-result-graded')); await expect(this.page.locator('.tab-bar-exercise-details').getByText(`${percentage}%`)).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/exercises/ExerciseTeamsPage.ts b/src/test/playwright/support/pageobjects/exercises/ExerciseTeamsPage.ts new file mode 100644 index 000000000000..ee506beba321 --- /dev/null +++ b/src/test/playwright/support/pageobjects/exercises/ExerciseTeamsPage.ts @@ -0,0 +1,80 @@ +import { Page } from 'playwright'; +import { expect } from '@playwright/test'; + +/** + * Page object for the exercise teams page. + */ +export class ExerciseTeamsPage { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + /** + * Clicks the "Create team" button. + */ + async createTeam() { + await this.page.locator('button', { hasText: 'Create team' }).click(); + } + + /** + * Enters the team name. + * @param teamName - the team name. + */ + async enterTeamName(teamName: string) { + await this.page.locator('#teamName').fill(teamName); + } + + /** + * Enters the team short name. + * @param teamShortName - the team short name. + */ + async enterTeamShortName(teamShortName: string) { + await this.page.locator('#teamShortName').fill(teamShortName); + } + + /** + * Sets the team owner/tutor. + * @param username - the tutor username. + */ + async setTeamTutor(username: string) { + const tutorSearchInput = this.page.locator('#owner-search-input'); + await tutorSearchInput.fill(username); + await this.page.getByRole('listbox').waitFor({ state: 'visible' }); + await tutorSearchInput.press('Enter'); + } + + /** + * Adds a student to the team. + * @param username - the student username. + */ + async addStudentToTeam(username: string) { + const studentSearchInput = this.page.locator('#student-search-input'); + await studentSearchInput.fill(username); + await this.page.getByRole('listbox').waitFor({ state: 'visible' }); + await studentSearchInput.press('Enter'); + } + + /** + * Checks if the team is on the list of teams. + * @param teamShortName - the team short name. + */ + async checkTeamOnList(teamShortName: string) { + await expect(this.page.getByRole('table').getByRole('row').getByText(teamShortName)).toBeVisible(); + } + + /** + * Retrieves the Locator to ignore team size recommendation checkbox. + */ + getIgnoreTeamSizeRecommendationCheckbox() { + return this.page.locator('#ignoreTeamSizeRecommendation'); + } + + /** + * Retrieves the Locator for the save button. + */ + getSaveButton() { + return this.page.locator('button', { hasText: 'Save' }); + } +} diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseFeedbackPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseFeedbackPage.ts index a81b34c9271b..6bdf30a9cbb8 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseFeedbackPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseFeedbackPage.ts @@ -8,7 +8,7 @@ import { expect } from '@playwright/test'; */ export class ProgrammingExerciseFeedbackPage extends AbstractExerciseFeedback { async shouldShowAdditionalFeedback(points: number, feedbackText: string) { - await Commands.reloadUntilFound(this.page, this.additionalFeedbackSelector); + await Commands.reloadUntilFound(this.page, this.page.locator(this.additionalFeedbackSelector)); await expect(this.page.locator(this.additionalFeedbackSelector).getByText(`${points} Points: ${feedbackText}`)).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts index 667ab882e262..cd1eba204546 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts @@ -29,14 +29,14 @@ export class ProgrammingExerciseOverviewPage { } async openCodeEditor(exerciseId: number) { - await Commands.reloadUntilFound(this.page, '#open-exercise-' + exerciseId); + await Commands.reloadUntilFound(this.page, this.page.locator(`#open-exercise-${exerciseId}`)); await this.courseOverview.openRunningProgrammingExercise(exerciseId); } async getRepoUrl() { - const cloneRepoSelector = '.clone-repository'; - await Commands.reloadUntilFound(this.page, cloneRepoSelector); - await this.page.locator(cloneRepoSelector).click(); + const cloneRepoLocator = this.page.locator('.clone-repository'); + await Commands.reloadUntilFound(this.page, cloneRepoLocator); + await cloneRepoLocator.click(); await this.page.locator('.popover-body').waitFor({ state: 'visible' }); return await this.page.locator('.clone-url').innerText(); } @@ -46,4 +46,11 @@ export class ProgrammingExerciseOverviewPage { await this.page.locator('a', { hasText: 'Open repository' }).click(); return await repositoryPage; } + + /** + * Retrieves the Locator for the exercise details bar. + */ + getExerciseDetails() { + return this.page.locator('.tab-bar-exercise-details'); + } } diff --git a/src/test/playwright/support/pageobjects/exercises/quiz/DragAndDropQuiz.ts b/src/test/playwright/support/pageobjects/exercises/quiz/DragAndDropQuiz.ts index 5a92149020a8..b52ac81eefd3 100644 --- a/src/test/playwright/support/pageobjects/exercises/quiz/DragAndDropQuiz.ts +++ b/src/test/playwright/support/pageobjects/exercises/quiz/DragAndDropQuiz.ts @@ -1,5 +1,6 @@ import { Page } from 'playwright'; import { EXERCISE_BASE, MODELING_EDITOR_CANVAS } from '../../../constants'; +import { drag } from '../../../utils'; import { Locator } from '@playwright/test'; export class DragAndDropQuiz { @@ -19,7 +20,7 @@ export class DragAndDropQuiz { async dragItemIntoDragArea(itemIndex: number) { const dragLocation = this.page.locator(`#drag-item-${itemIndex}`); const dropLocation = this.page.locator('#drop-location'); - await dragLocation.dragTo(dropLocation); + await drag(this.page, dragLocation, dropLocation); } async setTitle(title: string) { @@ -56,9 +57,8 @@ export class DragAndDropQuiz { async activateInteractiveMode() { const modelingEditorSidebar = this.page.locator('#modeling-editor-sidebar'); - const container = modelingEditorSidebar.locator('div').nth(0); - const interactiveButton = container.locator('button').nth(1); - await interactiveButton.click(); + const modeDropdownList = modelingEditorSidebar.locator('.dropdown').locator('select'); + await modeDropdownList.selectOption('Exporting'); } async markElementAsInteractive(nthElementOnCanvas: number, nthChildOfElement: number) { diff --git a/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts b/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts index 8a03c82d09da..36408d767c4a 100644 --- a/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/quiz/QuizExerciseCreationPage.ts @@ -1,7 +1,7 @@ -import { Page } from '@playwright/test'; -import { clearTextField, enterDate } from '../../../utils'; +import { Page, expect } from '@playwright/test'; +import { clearTextField, drag, enterDate } from '../../../utils'; import { Dayjs } from 'dayjs'; -import { BASE_API, QUIZ_EXERCISE_BASE } from '../../../constants'; +import { QUIZ_EXERCISE_BASE } from '../../../constants'; import { Fixtures } from '../../../../fixtures/fixtures'; export class QuizExerciseCreationPage { @@ -46,17 +46,21 @@ export class QuizExerciseCreationPage { await this.page.locator('#drag-and-drop-question-title').fill(title); await this.uploadDragAndDropBackground(); - await this.page.mouse.move(50, 50); + const element = this.page.locator('.background-area'); + const boundingBox = await element?.boundingBox(); + + expect(boundingBox, { message: 'Could not get bounding box of element' }).not.toBeNull(); + await this.page.mouse.move(boundingBox.x + 800, boundingBox.y + 10); await this.page.mouse.down(); - await this.page.mouse.move(500, 300); + await this.page.mouse.move(boundingBox.x + 1000, boundingBox.y + 150); await this.page.mouse.up(); await this.createDragAndDropItem('Rick Astley'); const dragLocator = this.page.locator('#drag-item-0'); const dropLocator = this.page.locator('#drop-location'); - await dragLocator.dragTo(dropLocator); + await drag(this.page, dragLocator, dropLocator); - const fileContent = await Fixtures.get('fixtures/exercise/quiz/drag_and_drop/question.txt'); + const fileContent = await Fixtures.get('exercise/quiz/drag_and_drop/question.txt'); const textInputField = this.page.locator('.ace_text-input'); await clearTextField(textInputField); await textInputField.fill(fileContent!); @@ -71,11 +75,9 @@ export class QuizExerciseCreationPage { async uploadDragAndDropBackground() { const fileChooserPromise = this.page.waitForEvent('filechooser'); - const fileUploadPromise = this.page.waitForResponse(`${BASE_API}/fileUpload*`); - await this.page.locator('#background-image-input-form').click(); + await this.page.locator('#background-file-input-button').click(); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles('exercise/quiz/drag_and_drop/background.jpg'); - await fileUploadPromise; + await fileChooser.setFiles('./fixtures/exercise/quiz/drag_and_drop/background.jpg'); } async saveQuiz() { diff --git a/src/test/playwright/support/requests/ExamAPIRequests.ts b/src/test/playwright/support/requests/ExamAPIRequests.ts index ff7e62225cda..37a6e0c3b583 100644 --- a/src/test/playwright/support/requests/ExamAPIRequests.ts +++ b/src/test/playwright/support/requests/ExamAPIRequests.ts @@ -192,4 +192,9 @@ export class ExamAPIRequests { }; await this.page.request.post(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/grading-scale`, { data }); } + + async getGradeSummary(exam: Exam) { + const response = await this.page.request.get(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/student-exams/grade-summary`); + return await response.json(); + } } diff --git a/src/test/playwright/support/requests/ExerciseAPIRequests.ts b/src/test/playwright/support/requests/ExerciseAPIRequests.ts index 2f2198935814..160e26f15b73 100644 --- a/src/test/playwright/support/requests/ExerciseAPIRequests.ts +++ b/src/test/playwright/support/requests/ExerciseAPIRequests.ts @@ -21,6 +21,8 @@ import { BASE_API, COURSE_BASE, EXERCISE_BASE, + Exercise, + ExerciseMode, ExerciseType, MODELING_EXERCISE_BASE, PROGRAMMING_EXERCISE_BASE, @@ -35,9 +37,9 @@ import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { FileUploadExercise } from 'app/entities/file-upload-exercise.model'; import { Participation } from 'app/entities/participation/participation.model'; -import { Exercise } from 'app/entities/exercise.model'; import { Exam } from 'app/entities/exam.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { TeamAssignmentConfig } from 'app/entities/team-assignment-config.model'; export class ExerciseAPIRequests { private readonly page: Page; @@ -67,7 +69,7 @@ export class ExerciseAPIRequests { async createProgrammingExercise(options: { course?: Course; exerciseGroup?: ExerciseGroup; - scaMaxPenalty?: number | null; + scaMaxPenalty?: number | undefined; recordTestwiseCoverage?: boolean; releaseDate?: dayjs.Dayjs; dueDate?: dayjs.Dayjs; @@ -77,11 +79,13 @@ export class ExerciseAPIRequests { packageName?: string; assessmentDate?: dayjs.Dayjs; assessmentType?: ProgrammingExerciseAssessmentType; + mode?: ExerciseMode; + teamAssignmentConfig?: TeamAssignmentConfig; }): Promise<ProgrammingExercise> { const { course, exerciseGroup, - scaMaxPenalty = null, + scaMaxPenalty = undefined, recordTestwiseCoverage = false, releaseDate = dayjs(), dueDate = dayjs().add(1, 'day'), @@ -91,6 +95,8 @@ export class ExerciseAPIRequests { packageName = 'de.test', assessmentDate = dayjs().add(2, 'days'), assessmentType = ProgrammingExerciseAssessmentType.AUTOMATIC, + mode = ExerciseMode.INDIVIDUAL, + teamAssignmentConfig, } = options; let programmingExerciseTemplate = {}; @@ -127,6 +133,8 @@ export class ExerciseAPIRequests { exercise.programmingLanguage = programmingLanguage; exercise.testwiseCoverageEnabled = recordTestwiseCoverage; + exercise.mode = mode; + exercise.teamAssignmentConfig = teamAssignmentConfig; const response = await this.page.request.post(`${PROGRAMMING_EXERCISE_BASE}/setup`, { data: exercise }); return response.json(); @@ -176,10 +184,15 @@ export class ExerciseAPIRequests { * @param exerciseId - The ID of the text exercise for which the submission is made. * @param text - The text content of the submission. */ - async makeTextExerciseSubmission(exerciseId: number, text: string) { - await this.page.request.put(`${EXERCISE_BASE}/${exerciseId}/text-submissions`, { - data: { submissionExerciseType: 'text', text }, - }); + async makeTextExerciseSubmission(exerciseId: number, text: string, createNewSubmission = true) { + const url = `${EXERCISE_BASE}/${exerciseId}/text-submissions`; + const data = { submissionExerciseType: 'text', text }; + + if (createNewSubmission) { + await this.page.request.post(url, { data }); + } else { + await this.page.request.put(url, { data }); + } } /** diff --git a/src/test/playwright/support/utils.ts b/src/test/playwright/support/utils.ts index 5d1c8ccb2d8e..9d135bbc7282 100644 --- a/src/test/playwright/support/utils.ts +++ b/src/test/playwright/support/utils.ts @@ -151,3 +151,20 @@ export async function newBrowserPage(browser: Browser) { const context = await browser.newContext(); return await context.newPage(); } + +/** + * Drags an element to a droppable element. + * @param page - Playwright Page instance used during the test. + * @param draggable - Locator of the element to be dragged. + * @param droppable - Locator of the element to be dropped on. + */ +export async function drag(page: Page, draggable: Locator, droppable: Locator) { + const box = (await droppable.boundingBox())!; + await draggable.hover(); + + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { + steps: 5, + }); + await page.mouse.up(); +} diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel3v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel3v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/activity/activityModel3v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/activity/activityModel3v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/bpmn/bpmnModel3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/bpmn/bpmnModel3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/classdiagram/classModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/classdiagram/classModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/communication/communicationModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/communication/communicationModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel3v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel3v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/component/componentModel3v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/component/componentModel3v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel3v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel3v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/deployment/deploymentModel3v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/deployment/deploymentModel3v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1a.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1a.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1a.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1a.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1av3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1av3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1av3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1av3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1b.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1b.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1b.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1b.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1bv3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1bv3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel1bv3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel1bv3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/flowchart/flowchartModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/flowchart/flowchartModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/object/objectModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/object/objectModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1a.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1a.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1a.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1a.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1av3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1av3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1av3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1av3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1b.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1b.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1b.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1b.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1bv3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1bv3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel1bv3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel1bv3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/petrinet/petriNetModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/petrinet/petriNetModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1a.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1a.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1a.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1a.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1av3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1av3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1av3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1av3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1b.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1b.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1b.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1b.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1bv3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1bv3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel1bv3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel1bv3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/syntaxtree/syntaxTreeModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/syntaxtree/syntaxTreeModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel1.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel1.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel1.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel1.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel1v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel1v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel1v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel1v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel2.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel2.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel2.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel2.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel2v3.json b/src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel2v3.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/modelingexercise/compass/umlmodel/usecase/useCaseModel2v3.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/modeling/compass/umlmodel/usecase/useCaseModel2v3.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequest.json b/src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequest.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequest.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequest.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequestWithoutCommit.json b/src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequestWithoutCommit.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequestWithoutCommit.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequestWithoutCommit.json diff --git a/src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequestWrongCommitOrder.json b/src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequestWrongCommitOrder.json similarity index 100% rename from src/test/resources/de/tum/in/www1/artemis/exercise/programmingexercise/gitlabPushEventRequestWrongCommitOrder.json rename to src/test/resources/de/tum/in/www1/artemis/exercise/programming/gitlabPushEventRequestWrongCommitOrder.json