diff --git a/.gitignore b/.gitignore index 75cb003dda0c..fee3f7347d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -193,6 +193,7 @@ data-exports/ ###################### /src/test/playwright/test-reports/ /src/test/playwright/test-results/* +/src/test/playwright/ssh-keys/known_hosts ################################# # Files generated by prebuild.mjs diff --git a/LICENSE b/LICENSE index a7504c5ef00b..507cc7deda09 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 TUM Applied Software Engineering +Copyright (c) 2024 TUM Applied Education Technologies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build.gradle b/build.gradle index 4091d7584bc7..5cdc8cab7613 100644 --- a/build.gradle +++ b/build.gradle @@ -257,7 +257,7 @@ dependencies { implementation "org.apache.lucene:lucene-queryparser:${lucene_version}" implementation "org.apache.lucene:lucene-core:${lucene_version}" implementation "org.apache.lucene:lucene-analyzers-common:${lucene_version}" - implementation "com.google.protobuf:protobuf-java:4.29.1" + implementation "com.google.protobuf:protobuf-java:4.29.2" // we have to override those values to use the latest version implementation "org.slf4j:jcl-over-slf4j:${slf4j_version}" @@ -525,7 +525,7 @@ dependencies { } testImplementation "org.springframework.security:spring-security-test:${spring_security_version}" testImplementation "org.springframework.boot:spring-boot-test:${spring_boot_version}" - testImplementation "org.assertj:assertj-core:3.26.3" + testImplementation "org.assertj:assertj-core:3.27.0" testImplementation "org.mockito:mockito-core:${mockito_version}" testImplementation "org.mockito:mockito-junit-jupiter:${mockito_version}" @@ -614,7 +614,7 @@ tasks.withType(Test).configureEach { } wrapper { - gradleVersion = "8.12-rc-2" + gradleVersion = "8.12" } tasks.register("stage") { diff --git a/docker/artemis/config/playwright-local.env b/docker/artemis/config/playwright-local.env index 6b7805deacca..ecc6eefc0387 100644 --- a/docker/artemis/config/playwright-local.env +++ b/docker/artemis/config/playwright-local.env @@ -1,5 +1,5 @@ # ---------------------------------------------------------------------------------------------------------------------- -# Artemis configuration overrides for the Playwright E2E Postgres setups +# Artemis configuration overrides for the Playwright E2E Local CI/VC setups # ---------------------------------------------------------------------------------------------------------------------- SPRING_PROFILES_ACTIVE="artemis,scheduling,localvc,localci,buildagent,core,prod,docker" @@ -12,3 +12,5 @@ ARTEMIS_CONTINUOUSINTEGRATION_ARTEMISAUTHENTICATIONTOKENVALUE='demo' ARTEMIS_CONTINUOUSINTEGRATION_DOCKERCONNECTIONURI='unix:///var/run/docker.sock' ARTEMIS_GIT_NAME='artemis' ARTEMIS_GIT_EMAIL='artemis@example.com' +ARTEMIS_VERSIONCONTROL_SSHHOSTKEYPATH='/app/artemis/src/test/playwright/ssh-keys' +ARTEMIS_VERSIONCONTROL_SSHPORT='7921' diff --git a/docker/playwright-E2E-tests-mysql-localci.yml b/docker/playwright-E2E-tests-mysql-localci.yml index cf677b01fec6..13d2ba3b00ef 100644 --- a/docker/playwright-E2E-tests-mysql-localci.yml +++ b/docker/playwright-E2E-tests-mysql-localci.yml @@ -47,6 +47,8 @@ services: condition: service_healthy environment: PLAYWRIGHT_DB_TYPE: 'MySQL' + network_mode: service:artemis-app + networks: !reset [] networks: artemis: diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index 4e14204d7703..332190edd7bd 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -2,11 +2,12 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.12" + python: "3.13" sphinx: fail_on_warning: true + configuration: docs/conf.py python: install: - requirements: docs/requirements.txt diff --git a/docs/admin/setup/distributed.rst b/docs/admin/setup/distributed.rst index def7d2b7a980..5c6e5ce6248d 100644 --- a/docs/admin/setup/distributed.rst +++ b/docs/admin/setup/distributed.rst @@ -17,7 +17,7 @@ Setup with multiple instances There are certain scenarios, where a setup with multiple instances of the application server is required. This can e.g. be due to special requirements regarding fault tolerance or performance. -Artemis also supports this setup (which is also used at the Chair for Applied Software Engineering at TUM). +Artemis also supports this setup (which is also used at TUM). Multiple instances of the application server are used to distribute the load: diff --git a/docs/conf.py b/docs/conf.py index ec9f22d2b6ac..705a2a5e18db 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,8 +18,8 @@ # -- Project information ----------------------------------------------------- project = 'Artemis' -copyright = '2024, Technical University of Munich, Applied Software Engineering' -author = 'Technical University of Munich, Applied Software Engineering' +copyright = '2024, Applied Education Technologies, Technical University of Munich' +author = 'Applied Education Technologies, Technical University of Munich' # -- General configuration --------------------------------------------------- diff --git a/docs/dev/cypress/cypress-open-screenshot.png b/docs/dev/cypress/cypress-open-screenshot.png deleted file mode 100644 index 7c32c512b6e5..000000000000 Binary files a/docs/dev/cypress/cypress-open-screenshot.png and /dev/null differ diff --git a/docs/dev/cypress/cypress_bamboo_deployment_diagram.svg b/docs/dev/cypress/cypress_bamboo_deployment_diagram.svg deleted file mode 100644 index 8ff7e84792a7..000000000000 --- a/docs/dev/cypress/cypress_bamboo_deployment_diagram.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
<<virtual machine>>
Bamboo Build Agent
<<virtual machine>>...
<<execution environment>>
Docker container
<<execution environment>>...
<<infrastructure>>
University Data Center
<<infrastructure>>...
<<component>>
Bamboo
<<component>>...
<<component>>
Jira
<<component>>...
<<component>>
Bitbucket
<<component>>...
<<execution environment>>
Docker container
<<execution environment>>...
<<execution environment>>
Docker container
<<execution environment>>...
<<component>>
Artemis Server
<<component>>...
<<component>>
MySQL Database
<<component>>...
<<component>>
Artemis Client
<<component>>...
<<component>>
Cypress Test Runner
<<component>>...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/dev/cypress/cypress_test_environment_deployment_diagram.svg b/docs/dev/cypress/cypress_test_environment_deployment_diagram.svg deleted file mode 100644 index d7f6ccc7eac7..000000000000 --- a/docs/dev/cypress/cypress_test_environment_deployment_diagram.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
<<virtual machine>>
Bamboo Build Agent
<<virtual machine>>...
<<virtual machine>>
Test Server 3
<<virtual machine>>...
<<component>>
Artemis Server
<<component>>...
<<infrastructure>>
University Data Center
<<infrastructure>>...
<<component>>
Bamboo
<<component>>...
<<component>>
Jira
<<component>>...
<<component>>
Bitbucket
<<component>>...
<<component>>
MySQL Database
<<component>>...
<<execution environment>>
Docker container
<<execution environment>>...
<<component>>
Artemis Client
<<component>>...
<<component>>
Cypress Test Runner
<<component>>...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/dev/cypress/sorry-cypress-dashboard.png b/docs/dev/cypress/sorry-cypress-dashboard.png deleted file mode 100644 index bf2d38932af2..000000000000 Binary files a/docs/dev/cypress/sorry-cypress-dashboard.png and /dev/null differ diff --git a/docs/dev/cypress/sorry-cypress-run.png b/docs/dev/cypress/sorry-cypress-run.png deleted file mode 100644 index ca2e93daea70..000000000000 Binary files a/docs/dev/cypress/sorry-cypress-run.png and /dev/null differ diff --git a/docs/dev/cypress/sorry-cypress-runs.png b/docs/dev/cypress/sorry-cypress-runs.png deleted file mode 100644 index 60a47a352dbb..000000000000 Binary files a/docs/dev/cypress/sorry-cypress-runs.png and /dev/null differ diff --git a/docs/dev/cypress/sorry-cypress-test.png b/docs/dev/cypress/sorry-cypress-test.png deleted file mode 100644 index 83148bb2fed3..000000000000 Binary files a/docs/dev/cypress/sorry-cypress-test.png and /dev/null differ diff --git a/docs/dev/playwright.rst b/docs/dev/playwright.rst index c4edd85d5399..ce6536c96e54 100644 --- a/docs/dev/playwright.rst +++ b/docs/dev/playwright.rst @@ -1,6 +1,23 @@ E2E Testing with Playwright =========================== +**Background** + +The Playwright test suite contains system tests verifying the most important features of Artemis. +System tests test the whole system and therefore require a complete deployment of Artemis first. +In order to prevent as many faults (bugs) as possible from being introduced into the develop branch, +we want to execute the Playwright test suite whenever new commits are pushed to a Git branch +(just like the unit and integration test suites). + +To accomplish this we need to be able to dynamically deploy multiple different instances of Artemis at the same time. +An ideal setup would be to deploy the whole Artemis system using Kubernetes. +However, this setup is too complex at the moment. +The main reason for the complexity is that it is very hard to automatically setup Docker containers for +the external services (e.g. Gitlab, Jenkins) and connect them directly with Artemis. + +Therefore, the current setup only dynamically deploys the Artemis server and configures it to connect to +the prelive system, which is already properly setup in the university data center. + Set up Playwright locally ------------------------- @@ -137,8 +154,72 @@ To run tests sequentially (one after another), set the ``workers`` option to ``1 sequentially, while running test files in parallel, set the ``fullyParallel`` option to ``false``. -Best practices for writing tests in Playwright ----------------------------------------------- +Best practices when writing new E2E tests +----------------------------------------- + +**Understanding the System and Requirements** + +Before writing tests, a deep understanding of the system and its requirements is crucial. +This understanding guides determining what needs testing and what defines a successful test. +The best way to understand is to consolidate the original system`s developer or a person actively working on this +component. + +**Identify Main Test Scenarios** + +Identify what are the main ways the component is supposed to be used. Try +the action with all involved user roles and test as many different inputs as +feasible. + +**Identify Edge Test Scenarios** + +Next to the main test scenarios, there are also edge case scenarios. These +tests include inputs/actions that are not supposed to be performed (e.g. enter +a too-long input into a field) and test the error-handling capabilities of the +platform. + +**Write Tests as Development Progresses** + +Rather than leaving testing until the end, write tests alongside each piece of +functionality. This approach ensures the code remains testable and makes +identifying and fixing issues as they arise easier. + +**Keep Tests Focused** + +Keep each test focused on one specific aspect of the code. If a test fails, it is +easier to identify the issue when it does not check multiple functionalities at +the same time. + +**Make Tests Independent** + +Tests should operate independently from each other and external factors like +the current date or time. Each test should be isolated. Use API calls for unrelated tasks, such as creating a +course, and UI interaction for the appropriate testing steps. This also involves +setting up a clean environment for every test suite. + +**Use Descriptive Test Names** + +Ensure each test name clearly describes what the test does. This strategy +makes the test suite easier to understand and quickly identifies which test +has failed. + +**Use Similar Test Setups** + +Avoid using different setups for each test suit. For example, always check +for the same HTTP response when deleting a course. + +**Do Not Ignore Failing Tests** + +If a test consistently fails, pay attention to it. Investigate as soon as possible +and fx the issue, or update the test if the requirements have changed. + +**Regularly Review and Refactor Your Tests** + +Tests, like code, can accumulate technical debt. Regular reviews for duplication, +unnecessary complexity, and other issues help maintain tests and enhance reliability. + + +Playwright testing best practices +--------------------------------- 1. **Use page objects for common interactions**: @@ -230,3 +311,102 @@ Best practices for writing tests in Playwright Waiting for the page load state is not recommended if we are only interested in specific elements appearing on the page - use ``waitFor()`` function of a locator instead. + +Artemis Deployment on Bamboo Build Agent +---------------------------------------- +Every execution of the Playwright test suite requires its own deployment of Artemis. +The easiest way to accomplish this is to deploy Artemis locally on the build agent, which executes the Playwright tests. +Using ``docker compose`` we can start a MySQL database and the Artemis server locally on the build agent and +connect it to the prelive system in the university data center. + +.. figure:: playwright/playwright_bamboo_deployment_diagram.svg + :align: center + :alt: Artemis Deployment on Bamboo Build Agent for Playwright + + Artemis Deployment on Bamboo Build Agent for Playwright + +In total there are three Docker containers started in the Bamboo build agent: + +1. MySQL + + This container starts a MySQL database and exposes it on port 3306. + The container automatically creates a new database 'Artemis' and configures it + with the recommended settings for Artemis. + The Playwright setup reuses the already existing + `MySQL docker image `__ + from the standard Artemis Docker setup. + +2. Artemis Application + + The Docker image for the Artemis container is created from the already existing + `Dockerfile `__. + When the Bamboo build of the Playwright test suite starts, it retrieves the Artemis executable (.war file) + from the `Artemis build plan `_. + Upon creation of the Artemis Docker image the executable is copied into the image together with configuration files + for the Artemis server. + + The main configuration of the Artemis server are contained in the + `Playwright environment configuration files `__. + However, those files do not contain any security relevant information. + Security relevant settings are instead passed to the Docker container via environment variables. This information is + accessible to the Bamboo build agent via + `Bamboo plan variables `__. + + The Artemis container is also configured to + `depend on `__ + the MySQL container and uses + `health checks `__ + to wait until the MySQL container is up and running. + +3. Playwright + + Playwright offers a test environment `docker image `__ + to execute Playwright tests. + The image contains Playwright browsers and browser system dependencies. + However, Playwright itself is not included in the image. + This is convenient for us because the image is smaller and the Artemis Playwright project requires + additional dependencies to fully function. + Therefore, the Artemis Playwright Docker container is configured to install all dependencies + (using :code:`npm ci`) upon start. This will also install Playwright itself. + Afterwards the Artemis Playwright test suite is executed. + + The necessary configuration for the Playwright test suite is also passed in via environment variables. + Furthermore, the Playwright container depends on the Artemis container and is only started + once Artemis has been fully booted. + +**Bamboo webhook** + +The Artemis instance deployed on the build agent is not publicly available to improve the security of this setup. +However, in order to get the build results for programming exercise submissions Artemis relies on a webhook from Bamboo +to send POST requests to Artemis. +To allow this, an extra rule has been added to the firewall allowing only the Bamboo instance in the prelive system +to connect to the Artemis instance in the build agent. + +**Timing** + +As mentioned above, we want the Playwright test suite to be executed whenever new commits are pushed to a Git branch. +This has been achieved by adding the +`Playwright build plan `__ +as a `child dependency `__ +to the `Artemis Build build plan `__. +The *Artemis Build* build plan is triggered whenever a new commit has been pushed to a branch. + +The Playwright build plan is only triggered after a successful build of the Artemis executable. +This does imply a delay (about 10 minutes on average) between the push of new commits and the execution +of the Playwright test suite, since the new Artemis executable first has to be built. + +**NOTE:** The Playwright test suite is only automatically executed for internal branches and pull requests +(requires access to this GitHub repository), **not** for external ones. +In case you need access rights, please contact the maintainer `Stephan Krusche `__. + +Maintenance +----------- +The Artemis Dockerfile as well as the MySQL image are already maintained because they are used in +other Artemis Docker setups. +Therefore, only Playwright and the Playwright Docker image require active maintenance. +Since the Playwright test suite simulates a real user, it makes sense to execute the test suite with +the latest browser versions. +The Playwright Docker image we use always has browsers with specific versions installed. +Therefore, the +`docker-compose file `__ +should be updated every month to make sure that the latest Playwright image is used. diff --git a/docs/dev/playwright/playwright_bamboo_deployment_diagram.svg b/docs/dev/playwright/playwright_bamboo_deployment_diagram.svg new file mode 100644 index 000000000000..14bdb96ef47e --- /dev/null +++ b/docs/dev/playwright/playwright_bamboo_deployment_diagram.svg @@ -0,0 +1,4 @@ + + + +
<<virtual machine>>
Bamboo Build Agent
<<virtual machine>>...
<<docker container>>
artemis-app
<<docker container>>...
<<docker container>>
mysql
<<docker container>>...
<<component>>
MySQL Database
<<component>>...
<<component>>
Artemis Server
<<component>>...
<<component>>
Artemis Client
<<component>>...
<<docker container>>
artemis-playwright
<<docker container>>...
<<component>>
Playwright Test Environment
<<component>>...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/user/communication.rst b/docs/user/communication.rst index cf28dce9c8d8..026785e4861b 100644 --- a/docs/user/communication.rst +++ b/docs/user/communication.rst @@ -100,6 +100,240 @@ for multiple links. |link-preview-multiple| + +.. _communication features availability list: + +Communication Features Availability +----------------------------------- + +.. |AVAILABLE| raw:: html + + AVAILABLE + +.. |UNAVAILABLE| raw:: html + + UNAVAILABLE + +.. |PLANNED| raw:: html + + PLANNED + +.. |WIP| raw:: html + + WIP + +.. |NOT PLANNED| raw:: html + + NOT PLANNED + + +The following table represents the currently available communication features of Artemis on the different platforms. Note that not all +features are available to every user, which is why **Actor restrictions** have been added. The following sections will explore this in more +detail. + +Status explained +^^^^^^^^^^^^^^^^ + +.. list-table:: + :widths: 15 74 + + * - |AVAILABLE| + - This feature has been released to production. + * - |UNAVAILABLE| + - This feature is currently not available and not planned yet. + * - |PLANNED| + - This feature is planned and implemented within the next 2-4 months. + * - |WIP| + - This feature is currently being worked on and will be released soon. + * - |NOT PLANNED| + - This feature will not be implemented due to platform restrictions, or it does not make sense to implement it. + + + + +Available features on each platform +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Feature | Actor Restrictions | Web App | iOS | Android | ++======================================================+======================================+====================+=====================+=====================+ +| **General** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Send Messages | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Receive Messages | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Post Actions** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| React to Messages | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Reply in Thread | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Copy Text | | |NOT PLANNED| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Pin Messages | | Groups: group creators | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | | Channels: moderators | | | | +| | | DM: members of DM | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Delete Message | Moderators and authors | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Edit Message | Authors only | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Save Message for later | | |AVAILABLE| | |UNAVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Forward Messages | | |WIP| | |UNAVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Resolve Messages | At least tutor and authors | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Post action bar (thread view) | ||NOT PLANNED| | |AVAILABLE| | |WIP| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Markdown Textfield Options** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Tag other users | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Reference channels, lectures and exercises | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Tag FAQ | | |AVAILABLE| | |WIP| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Basic formatting (underline, bold, italic) | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Strikethrough formatting | | |AVAILABLE| | |UNAVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Preview | | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Code Block and inline code formatting | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Reference formatting | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Link formatting | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Messages** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Profile pictures | | |AVAILABLE| | |AVAILABLE| | |WIP| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Show if message was edited, resolved or pinned | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Render links to exercises, lectures, other chats, | | |AVAILABLE| | |AVAILABLE| | |WIP| | +| | lecture-units, slides, lecture-attachment with | | | | | +| | correct icon | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Render FAQ links | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Mark unread messages | | |UNAVAILABLE| | |UNAVAILABLE| | |UNAVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Render images | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Render links to uploaded files | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Filter messages (unresolved, own, reacted) | | |AVAILABLE| | |AVAILABLE| | |UNAVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Sort messages (ascending, descending) | | |AVAILABLE| | |NOT PLANNED| | |UNAVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Search for messages in chat | | |UNAVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Search for messages across all chats | | |AVAILABLE| | |UNAVAILABLE| | |UNAVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Open Profile info by clicking profile picture | | |PLANNED| | |AVAILABLE| | |WIP| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Start a conversation from Profile | | |WIP| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Link/Attachment Handling** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Open lecture, exercise, chat links correctly in | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | the appropriate view | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Open sent images full-screen | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Download sent images | | |AVAILABLE| | |PLANNED| | |UNAVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| View and download attachments | | |AVAILABLE| | |PLANNED| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Conversation Management** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Search for chats | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Filter chats (all, unread, favorites) | | |UNAVAILABLE| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Mark unread chats | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Mute, hide, favorite chat | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Edit Chat information (name, topic, description) | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Archive Chat | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Delete Chat | | |AVAILABLE| | |UNAVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| View Members | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Search Members | | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Filter Members (All Members, Instructors, | | |AVAILABLE| | |UNAVAILABLE| | |UNAVAILABLE| | +| | Tutors, Students, Moderators) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Add Members to existing chat | | Group: members of group | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | | Channel: at least instructor | | | | +| | | or moderator | | | | +| | | DM: not possible | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Filter Members while adding (Students, Tutors, | | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | +| | Instructors) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Add whole groups (All Students, All Tutors, All | | |AVAILABLE| | |PLANNED| | |UNAVAILABLE| | +| | Instructors) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Grant moderator roles in channels / revoke | Moderators only | |AVAILABLE| | |UNAVAILABLE| | |AVAILABLE| | +| | moderation roles | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Create direct chat | Everyone | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Create channel (public/private, | At least teaching assistant | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | announcement/unrestricted) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Update channel information (name, topic, | Moderators | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | description) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Create group chat | Everyone | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Remove users from group chat | Members of group chat | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Browse channels | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Show info in chat overview | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | (created by, created on) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Leave chat | For groups only | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Delete channel | | Creators with moderation | |AVAILABLE| | |UNAVAILABLE| | |UNAVAILABLE| | +| | | rights and instructors | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Archive channel | Moderators | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| **Notifications** | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| Notification overview for past notifications | | |AVAILABLE| | |AVAILABLE| | |PLANNED| | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ +| | Notification settings (unsubscribe/subscribe | | |AVAILABLE| | |AVAILABLE| | |AVAILABLE| | +| | to various notification types) | | | | | ++------------------------------------------------------+--------------------------------------+--------------------+---------------------+---------------------+ + +.. note:: + - Leave chat option is available on the web app for groups only, on iOS for groups and non course-wide channels, and on Android for channels, groups, and DMs. + - Creating a group chat on iOS and Android can be achieved via the 'Create Chat' option. It becomes a group when more than one user is added. + - Downloading sent images in the chat is only available through the browser option on the web app. + Features for Users ------------------ diff --git a/docs/user/mobile-applications.rst b/docs/user/mobile-applications.rst index 1182384974fe..64485cb2e2fd 100644 --- a/docs/user/mobile-applications.rst +++ b/docs/user/mobile-applications.rst @@ -10,7 +10,7 @@ Mobile Applications Overview -------- -Artemis supports native mobile applications available for both `Android `_ and `iOS `_. We designed them to be applicable in lecture usage. Users can, for example, participate in quizzes and write questions. Furthermore, they can communicate with each other. +Artemis supports native mobile applications available for both `Android `_ and `iOS `_. We designed them to be applicable in lecture usage. Users can, for example, participate in quizzes and write questions. Furthermore, they can communicate with each other (available communication features on iOS and Android can be checked using :ref:`this list `). Both apps use native user interface components and are adapted to their associated operating system. Therefore, they can differ in their usage. diff --git a/gradle.properties b/gradle.properties index c110eb971ddc..c32073344f44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ slf4j_version=2.0.16 sentry_version=7.19.0 liquibase_version=4.30.0 docker_java_version=3.4.1 -logback_version=1.5.12 +logback_version=1.5.14 java_parser_version=3.26.2 byte_buddy_version=1.15.11 netty_version=4.1.115.Final diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fb4b1a2e2ced..cea7a793a84b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-rc-2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAffectedStudentDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAffectedStudentDTO.java index 71c6b73a208f..3919ec9cd858 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAffectedStudentDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAffectedStudentDTO.java @@ -1,4 +1,4 @@ package de.tum.cit.aet.artemis.assessment.dto; -public record FeedbackAffectedStudentDTO(long courseId, long participationId, String firstName, String lastName, String login, String repositoryURI) { +public record FeedbackAffectedStudentDTO(long participationId, String firstName, String lastName, String login, String repositoryURI) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java index e56722f079cf..c93578cd10c5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java @@ -9,5 +9,5 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems, Set taskNames, List testCaseNames, - List errorCategories) { + List errorCategories, long highestOccurrenceOfGroupedFeedback) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java index d22a036e7489..0fee28e9672c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java @@ -6,11 +6,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackDetailDTO(List concatenatedFeedbackIds, long count, double relativeCount, String detailText, String testCaseName, String taskName, - String errorCategory) { +public record FeedbackDetailDTO(List feedbackIds, long count, double relativeCount, List detailTexts, String testCaseName, String taskName, String errorCategory) { - public FeedbackDetailDTO(String concatenatedFeedbackIds, long count, double relativeCount, String detailText, String testCaseName, String taskName, String errorCategory) { - this(Arrays.stream(concatenatedFeedbackIds.split(",")).map(Long::valueOf).toList(), count, relativeCount, detailText, testCaseName, taskName, errorCategory); + public FeedbackDetailDTO(String feedbackId, long count, double relativeCount, String detailText, String testCaseName, String taskName, String errorCategory) { + // Feedback IDs are gathered in the query using a comma separator, and the detail texts are stored in a list because, in case aggregation is applied, the detail texts are + // grouped together + this(Arrays.stream(feedbackId.split(",")).map(Long::valueOf).toList(), count, relativeCount, List.of(detailText), testCaseName, taskName, errorCategory); } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/FeedbackCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/FeedbackCleanupRepository.java index e0fa96965e4a..47e6e2e2f406 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/FeedbackCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/FeedbackCleanupRepository.java @@ -43,6 +43,23 @@ WHERE f.result IN ( """) int deleteFeedbackForOrphanResults(); + /** + * Counts {@link Feedback} entries where the associated {@link Result} has no submission and no participation. + * + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(f) + FROM Feedback f + WHERE f.result IN ( + SELECT r + FROM Result r + WHERE r.submission IS NULL + AND r.participation IS NULL + ) + """) + int countFeedbackForOrphanResults(); + /** * Deletes {@link Feedback} entries with a {@code null} result. * @@ -56,6 +73,18 @@ WHERE f.result IN ( """) int deleteOrphanFeedback(); + /** + * Counts {@link Feedback} entries with a {@code null} result. + * + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(f) + FROM Feedback f + WHERE f.result IS NULL + """) + int countOrphanFeedback(); + /** * Deletes {@link Feedback} entries associated with rated {@link Result} that are not the latest rated result * for a {@link Participation}, within courses conducted between the specified date range. @@ -89,6 +118,36 @@ SELECT MAX(r2.id) """) int deleteOldFeedbackThatAreNotLatestRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** + * Counts {@link Feedback} entries associated with rated {@link Result} that are not the latest rated result + * for a {@link Participation}, within courses conducted between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(f) + FROM Feedback f + WHERE f.result IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = TRUE + ) + AND r.rated = TRUE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countOldFeedbackThatAreNotLatestRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes non-rated {@link Feedback} entries that are not the latest non-rated result, where the associated course's start and end dates * are between the specified date range. @@ -120,4 +179,33 @@ SELECT MAX(r2.id) ) """) int deleteOldNonRatedFeedbackWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + + /** + * Counts non-rated {@link Feedback} entries that are not the latest non-rated result, where the associated course's start and end dates + * are between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(f) + FROM Feedback f + WHERE f.result IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + ) + AND r.rated = FALSE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countOldNonRatedFeedbackWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/LongFeedbackTextCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/LongFeedbackTextCleanupRepository.java index 09dbf01baff2..f91bac77e415 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/LongFeedbackTextCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/LongFeedbackTextCleanupRepository.java @@ -44,6 +44,24 @@ WHERE lft.feedback.id IN ( """) int deleteLongFeedbackTextForOrphanResult(); + /** + * Counts {@link LongFeedbackText} entries linked to {@link Feedback} where the associated + * {@link Result} has no participation and no submission. + * + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(lft) + FROM LongFeedbackText lft + WHERE lft.feedback.id IN ( + SELECT f.id + FROM Feedback f + WHERE f.result.participation IS NULL + AND f.result.submission IS NULL + ) + """) + int countLongFeedbackTextForOrphanResult(); + /** * Deletes {@link LongFeedbackText} linked to {@link Feedback} with a {@code null} result. * @@ -61,6 +79,22 @@ WHERE lft.feedback IN ( """) int deleteLongFeedbackTextForOrphanedFeedback(); + /** + * Counts {@link LongFeedbackText} linked to {@link Feedback} with a {@code null} result. + * + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(lft) + FROM LongFeedbackText lft + WHERE lft.feedback IN ( + SELECT f + FROM Feedback f + WHERE f.result IS NULL + ) + """) + int countLongFeedbackTextForOrphanedFeedback(); + /** * Deletes {@link LongFeedbackText} entries associated with rated {@link Result} that are not the latest rated result * for a {@link Participation}, within courses conducted between the specified date range. @@ -95,6 +129,37 @@ SELECT MAX(r2.id) """) int deleteLongFeedbackTextForRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** + * Counts {@link LongFeedbackText} entries associated with rated {@link Result} that are not the latest rated result + * for a {@link Participation}, within courses conducted between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(lft) + FROM LongFeedbackText lft + WHERE lft.feedback IN ( + SELECT f + FROM Feedback f + LEFT JOIN f.result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE f.result.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = TRUE + ) + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + AND r.rated = TRUE + ) + """) + int countLongFeedbackTextForRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes {@link LongFeedbackText} entries linked to non-rated {@link Feedback} that are not the latest non-rated result where the associated course's start * and end dates are between the specified date range. @@ -128,4 +193,35 @@ SELECT MAX(r2.id) ) """) int deleteLongFeedbackTextForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + + /** + * Counts {@link LongFeedbackText} entries linked to non-rated {@link Feedback} that are not the latest non-rated result where the associated course's start + * and end dates are between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(lft) + FROM LongFeedbackText lft + WHERE lft.feedback IN ( + SELECT f + FROM Feedback f + LEFT JOIN f.result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE f.result.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = FALSE + ) + AND r.rated = FALSE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countLongFeedbackTextForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ParticipantScoreCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ParticipantScoreCleanupRepository.java index 50fcdd5b14b3..7770731b4d25 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ParticipantScoreCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ParticipantScoreCleanupRepository.java @@ -57,6 +57,37 @@ SELECT MAX(r2.id) """) int deleteParticipantScoresForNonLatestLastResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** + * Counts {@link ParticipantScore} entries where the associated {@link Result} is not the latest rated result + * for a {@link Participation}, within courses conducted between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted upon execution of the cleanup operation + * + */ + @Query(""" + SELECT COUNT(ps) + FROM ParticipantScore ps + WHERE ps.lastResult IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = TRUE + ) + AND r.rated = TRUE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countParticipantScoresForNonLatestLastResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes {@link ParticipantScore} entries where the associated last rated {@link Result} is not the latest rated result * for a {@link Participation}, within courses conducted between the specified date range. @@ -90,6 +121,36 @@ SELECT MAX(r2.id) """) int deleteParticipantScoresForNonLatestLastRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** + * Counts {@link ParticipantScore} entries where the associated last rated {@link Result} is not the latest rated result + * for a {@link Participation}, within courses conducted between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted upon execution of the cleanup operation + */ + @Query(""" + SELECT COUNT(ps) + FROM ParticipantScore ps + WHERE ps.lastRatedResult IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = TRUE + ) + AND r.rated = TRUE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countParticipantScoresForNonLatestLastRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes {@link ParticipantScore} entries where the associated {@link Result} is not the latest result and is non-rated, * and the course's start and end dates are between the specified date range. @@ -122,6 +183,36 @@ SELECT MAX(r2.id) """) int deleteParticipantScoresForLatestNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** + * Counts {@link ParticipantScore} entries where the associated {@link Result} is not the latest result and is non-rated, + * and the course's start and end dates are between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted upon execution of the cleanup operation + */ + @Query(""" + SELECT COUNT(ps) + FROM ParticipantScore ps + WHERE ps.lastResult IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = FALSE + ) + AND r.rated = FALSE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countParticipantScoresForLatestNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes {@link ParticipantScore} entries where the associated {@link Result} is not latest and is non-rated, even though * it is marked as the last rated result, to prevent potential integrity violations. @@ -155,4 +246,34 @@ SELECT MAX(r2.id) ) """) int deleteParticipantScoresForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + + /** + * Counts {@link ParticipantScore} entries where the associated {@link Result} is not latest and is non-rated, even though + * it is marked as the last rated result. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted upon execution of the cleanup operation + */ + @Query(""" + SELECT COUNT(ps) + FROM ParticipantScore ps + WHERE ps.lastRatedResult IN ( + SELECT r + FROM Result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE r.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = FALSE + ) + AND r.rated = FALSE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countParticipantScoresForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/PlagiarismComparisonCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/PlagiarismComparisonCleanupRepository.java index a93ba9604223..543c6255639b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/PlagiarismComparisonCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/PlagiarismComparisonCleanupRepository.java @@ -41,6 +41,13 @@ public interface PlagiarismComparisonCleanupRepository extends ArtemisJpaReposit """) int deletePlagiarismSubmissionElementsByComparisonIdsIn(@Param("ids") List ids); + @Query(""" + SELECT COUNT(e) + FROM PlagiarismSubmissionElement e + WHERE e.plagiarismSubmission.plagiarismComparison.id IN :ids + """) + int countPlagiarismSubmissionElementsByComparisonIdsIn(@Param("ids") List ids); + @Modifying @Transactional // ok because of delete @Query(""" @@ -50,6 +57,13 @@ public interface PlagiarismComparisonCleanupRepository extends ArtemisJpaReposit """) int deletePlagiarismSubmissionsByComparisonIdsIn(@Param("ids") List ids); + @Query(""" + SELECT COUNT(s) + FROM PlagiarismSubmission s + WHERE s.plagiarismComparison.id IN :ids + """) + int countPlagiarismSubmissionsByComparisonIdsIn(@Param("ids") List ids); + @Modifying @Transactional // ok because of modifying @Query(""" @@ -68,6 +82,13 @@ public interface PlagiarismComparisonCleanupRepository extends ArtemisJpaReposit """) int deletePlagiarismComparisonMatchesByComparisonIdsIn(@Param("ids") List ids); + @Query(nativeQuery = true, value = """ + SELECT COUNT(*) + FROM plagiarism_comparison_matches m + WHERE m.plagiarism_comparison_id IN :ids + """) + int countPlagiarismComparisonMatchesByComparisonIdsIn(@Param("ids") List ids); + /** * Retrieves a list of unnecessary plagiarism comparison IDs based on the associated course's date range. * A plagiarism comparison is considered unnecessary if its status is 'NONE' and the related course's diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/RatingCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/RatingCleanupRepository.java index 07f10ec6d3d3..c923cffbcef6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/RatingCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/RatingCleanupRepository.java @@ -38,4 +38,21 @@ WHERE rt.result IN ( ) """) int deleteOrphanRating(); + + /** + * Counts {@link Rating} entries where the associated {@link Result} has no submission and no participation. + * + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(rt) + FROM Rating rt + WHERE rt.result IN ( + SELECT r + FROM Result r + WHERE r.submission IS NULL + AND r.participation IS NULL + ) + """) + int countOrphanRating(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ResultCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ResultCleanupRepository.java index 7d9e413aa5e7..7bf86fe0c730 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ResultCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/ResultCleanupRepository.java @@ -39,6 +39,19 @@ public interface ResultCleanupRepository extends ArtemisJpaRepository :deleteFrom + ) + AND r.id NOT IN ( + SELECT max_id + FROM ( + SELECT MAX(r2.id) AS max_id + FROM Result r2 + WHERE r2.rated = FALSE + GROUP BY r2.participation.id + ) + ) + """) + int countNonLatestNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes rated {@link Result} entries that are not the latest rated result for a {@link Participation}, within courses * conducted between the specified date range. @@ -112,4 +159,38 @@ SELECT MAX(r2.id) AS max_id ) """) int deleteNonLatestRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + + /** + * Counts rated {@link Result} entries that are not the latest rated result for a {@link Participation}, within courses + * conducted between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(r) + FROM Result r + WHERE r.rated = TRUE + AND r.participation IS NOT NULL + AND r.participation.exercise IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM Course c + LEFT JOIN c.exercises e + WHERE e = r.participation.exercise + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + AND r.id NOT IN ( + SELECT max_id + FROM ( + SELECT MAX(r2.id) AS max_id + FROM Result r2 + WHERE r2.rated = TRUE + GROUP BY r2.participation.id + ) + ) + """) + int countNonLatestRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/StudentScoreCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/StudentScoreCleanupRepository.java index d45bf8243e50..e7c679fe34cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/StudentScoreCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/StudentScoreCleanupRepository.java @@ -31,4 +31,16 @@ public interface StudentScoreCleanupRepository extends ArtemisJpaRepository { + + /** + * Deletes {@link SubmissionVersion} entities where the created date is after {@code deleteFrom} + * and before {@code deleteTo}. + * + * @param deleteFrom the start date for selecting submissions + * @param deleteTo the end date for selecting submissions + * @return the number of deleted entities + */ + @Modifying + @Transactional // ok because of delete + @Query(""" + DELETE FROM SubmissionVersion sv + WHERE sv.createdDate > :deleteFrom + AND sv.createdDate < :deleteTo + """) + int deleteSubmissionVersionsByCreatedDateRange(@Param("deleteFrom") Instant deleteFrom, @Param("deleteTo") Instant deleteTo); + + /** + * Counts {@link SubmissionVersion} entities where the created date is after {@code deleteFrom} + * and before {@code deleteTo}. + * + * @param deleteFrom the start date for selecting submissions + * @param deleteTo the end date for selecting submissions + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(sv) + FROM SubmissionVersion sv + WHERE sv.createdDate > :deleteFrom + AND sv.createdDate < :deleteTo + """) + int countSubmissionVersionsByCreatedDateRange(@Param("deleteFrom") Instant deleteFrom, @Param("deleteTo") Instant deleteTo); + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/TeamScoreCleanupRepository.java b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/TeamScoreCleanupRepository.java index 2c1009f481b8..b1a17076fb47 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/TeamScoreCleanupRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/repository/cleanup/TeamScoreCleanupRepository.java @@ -31,4 +31,16 @@ public interface TeamScoreCleanupRepository extends ArtemisJpaRepository :deleteFrom + ) + """) + int countTextBlockForRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + /** * Deletes {@link TextBlock} entries linked to non-rated {@link Result} that are not the latest non-rated result * for a {@link Participation}, where the associated course's start and end dates @@ -130,4 +194,36 @@ SELECT MAX(r2.id) ) """) int deleteTextBlockForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); + + /** + * Counts {@link TextBlock} entries linked to non-rated {@link Result} that are not the latest non-rated result + * for a {@link Participation}, where the associated course's start and end dates + * are between the specified date range. + * + * @param deleteFrom the start date for selecting courses + * @param deleteTo the end date for selecting courses + * @return the number of entities that would be deleted + */ + @Query(""" + SELECT COUNT(tb) + FROM TextBlock tb + WHERE tb.feedback IN ( + SELECT f + FROM Feedback f + LEFT JOIN f.result r + LEFT JOIN r.participation p + LEFT JOIN p.exercise e + LEFT JOIN e.course c + WHERE f.result.id NOT IN ( + SELECT MAX(r2.id) + FROM Result r2 + WHERE r2.participation.id = p.id + AND r2.rated = FALSE + ) + AND r.rated = FALSE + AND c.endDate < :deleteTo + AND c.startDate > :deleteFrom + ) + """) + int countTextBlockForNonRatedResultsWhereCourseDateBetween(@Param("deleteFrom") ZonedDateTime deleteFrom, @Param("deleteTo") ZonedDateTime deleteTo); } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index 1f62ef78665b..1dce0090c001 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -4,7 +4,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -25,7 +24,7 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; @@ -49,7 +48,7 @@ import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; -import de.tum.cit.aet.artemis.core.dto.pageablesearch.PageableSearchDTO; +import de.tum.cit.aet.artemis.core.dto.SortingOrder; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; @@ -64,6 +63,7 @@ import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService; import de.tum.cit.aet.artemis.lti.service.LtiNewResultService; +import de.tum.cit.aet.artemis.modeling.service.compass.strategy.NameSimilarity; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -126,6 +126,10 @@ public class ResultService { private final ProgrammingExerciseRepository programmingExerciseRepository; + private static final int MAX_FEEDBACK_IDS = 5; + + private static final double SIMILARITY_THRESHOLD = 0.9; + public ResultService(UserRepository userRepository, ResultRepository resultRepository, Optional ltiNewResultService, ResultWebsocketService resultWebsocketService, ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository, FeedbackRepository feedbackRepository, LongFeedbackTextRepository longFeedbackTextRepository, ComplaintRepository complaintRepository, @@ -570,10 +574,12 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * Pagination and sorting: * - Sorting is applied based on the specified column and order (ascending or descending). * - The result is paginated according to the provided page number and page size. + * Additionally one can group the feedback detail text. * - * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters - * (task names, test cases, occurrence range, error categories). + * @param exerciseId The ID of the exercise for which feedback details should be retrieved. + * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters + * (task names, test cases, occurrence range, error categories). + * @param groupFeedback The flag to enable grouping and aggregation of feedback details. * @return A {@link FeedbackAnalysisResponseDTO} object containing: * - A {@link SearchResultPageDTO} of paginated feedback details. * - The total number of distinct results for the exercise. @@ -581,7 +587,7 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * - A list of active test case names used in the feedback. * - A list of predefined error categories ("Student Error," "Ares Error," "AST Error") available for filtering. */ - public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data) { + public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data, boolean groupFeedback) { // 1. Fetch programming exercise with associated test cases ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTestCasesByIdElseThrow(exerciseId); @@ -598,12 +604,12 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee Set taskNames = tasks.stream().map(ProgrammingExerciseTask::getTaskName).collect(Collectors.toSet()); // 5. Include unassigned tasks if specified by the filter; otherwise, only include specified tasks - List includeUnassignedTasks = new ArrayList<>(taskNames); + List includeNotAssignedToTask = new ArrayList<>(taskNames); if (!data.getFilterTasks().isEmpty()) { - includeUnassignedTasks.removeAll(data.getFilterTasks()); + includeNotAssignedToTask.removeAll(data.getFilterTasks()); } else { - includeUnassignedTasks.clear(); + includeNotAssignedToTask.clear(); } // 6. Define the occurrence range based on filter parameters @@ -614,22 +620,113 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee List filterErrorCategories = data.getFilterErrorCategories(); // 8. Set up pagination and sorting based on input data - final var pageable = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS); + final Pageable pageable = groupFeedback ? Pageable.unpaged() : PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS); - // 9. Query the database to retrieve paginated and filtered feedback + // 9. Query the database based on groupFeedback attribute to retrieve paginated and filtered feedback final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, - StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence, + StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeNotAssignedToTask, minOccurrence, maxOccurrence, filterErrorCategories, pageable); + ; + List processedDetails; + int totalPages = 0; + long totalCount = 0; + long highestOccurrenceOfGroupedFeedback = 0; + if (!groupFeedback) { + // Process and map feedback details, calculating relative count and assigning task names + processedDetails = feedbackDetailPage.getContent().stream() + .map(detail -> new FeedbackDetailDTO(detail.feedbackIds().subList(0, Math.min(detail.feedbackIds().size(), MAX_FEEDBACK_IDS)), detail.count(), + (detail.count() * 100.00) / distinctResultCount, detail.detailTexts(), detail.testCaseName(), detail.taskName(), detail.errorCategory())) + .toList(); + totalPages = feedbackDetailPage.getTotalPages(); + totalCount = feedbackDetailPage.getTotalElements(); + } + else { + // Fetch all feedback details + List allFeedbackDetails = feedbackDetailPage.getContent(); + + // Apply grouping and aggregation with a similarity threshold of 90% + List aggregatedFeedbackDetails = aggregateFeedback(allFeedbackDetails, SIMILARITY_THRESHOLD); + + highestOccurrenceOfGroupedFeedback = aggregatedFeedbackDetails.stream().mapToLong(FeedbackDetailDTO::count).max().orElse(0); + // Apply manual sorting + Comparator comparator = getComparatorForFeedbackDetails(data); + List processedDetailsPreSort = new ArrayList<>(aggregatedFeedbackDetails); + processedDetailsPreSort.sort(comparator); + // Apply manual pagination + int page = data.getPage(); + int pageSize = data.getPageSize(); + int start = Math.max(0, (page - 1) * pageSize); + int end = Math.min(start + pageSize, processedDetailsPreSort.size()); + processedDetails = processedDetailsPreSort.subList(start, end); + processedDetails = processedDetails.stream().map(detail -> new FeedbackDetailDTO(detail.feedbackIds().subList(0, Math.min(detail.feedbackIds().size(), 5)), + detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailTexts(), detail.testCaseName(), detail.taskName(), detail.errorCategory())) + .toList(); + totalPages = (int) Math.ceil((double) processedDetailsPreSort.size() / pageSize); + totalCount = aggregatedFeedbackDetails.size(); + } - // 10. Process and map feedback details, calculating relative count and assigning task names - List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.concatenatedFeedbackIds(), detail.count(), - (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), detail.taskName(), detail.errorCategory())).toList(); - // 11. Predefined error categories available for filtering on the client side + // 10. Predefined error categories available for filtering on the client side final List ERROR_CATEGORIES = List.of("Student Error", "Ares Error", "AST Error"); - // 12. Return response containing processed feedback details, task names, active test case names, and error categories - return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), taskNames, - activeTestCaseNames, ERROR_CATEGORIES); + // 11. Return response containing processed feedback details, task names, active test case names, and error categories + return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, totalPages), totalCount, taskNames, activeTestCaseNames, ERROR_CATEGORIES, + highestOccurrenceOfGroupedFeedback); + } + + private Comparator getComparatorForFeedbackDetails(FeedbackPageableDTO search) { + Map> comparators = Map.of("count", Comparator.comparingLong(FeedbackDetailDTO::count), "detailTexts", + Comparator.comparing(detail -> detail.detailTexts().isEmpty() ? "" : detail.detailTexts().getFirst(), // Sort by the first element of the list + String.CASE_INSENSITIVE_ORDER), + "testCaseName", Comparator.comparing(FeedbackDetailDTO::testCaseName, String.CASE_INSENSITIVE_ORDER), "taskName", + Comparator.comparing(FeedbackDetailDTO::taskName, String.CASE_INSENSITIVE_ORDER)); + + Comparator comparator = comparators.getOrDefault(search.getSortedColumn(), (a, b) -> 0); + return search.getSortingOrder() == SortingOrder.ASCENDING ? comparator : comparator.reversed(); + } + + private List aggregateFeedback(List feedbackDetails, double similarityThreshold) { + List processedDetails = new ArrayList<>(); + + for (FeedbackDetailDTO base : feedbackDetails) { + boolean isMerged = false; + + for (FeedbackDetailDTO processed : processedDetails) { + // Ensure feedbacks have the same testCaseName and taskName + if (base.testCaseName().equals(processed.testCaseName()) && base.taskName().equals(processed.taskName())) { + double similarity = NameSimilarity.levenshteinSimilarity(base.detailTexts().getFirst(), processed.detailTexts().getFirst()); + + if (similarity > similarityThreshold) { + // Merge the current base feedback into the processed feedback + List mergedFeedbackIds = new ArrayList<>(processed.feedbackIds()); + if (processed.feedbackIds().size() < MAX_FEEDBACK_IDS) { + mergedFeedbackIds.addAll(base.feedbackIds()); + } + + List mergedTexts = new ArrayList<>(processed.detailTexts()); + mergedTexts.add(base.detailTexts().getFirst()); + + long mergedCount = processed.count() + base.count(); + + // Replace the processed entry with the updated one + processedDetails.remove(processed); + FeedbackDetailDTO updatedProcessed = new FeedbackDetailDTO(mergedFeedbackIds, mergedCount, 0, mergedTexts, processed.testCaseName(), processed.taskName(), + processed.errorCategory()); + processedDetails.add(updatedProcessed); // Add the updated entry + isMerged = true; + break; // No need to check further + } + } + } + + if (!isMerged) { + // If not merged, add it as a new entry in processedDetails + FeedbackDetailDTO newEntry = new FeedbackDetailDTO(base.feedbackIds(), base.count(), 0, List.of(base.detailTexts().getFirst()), base.testCaseName(), + base.taskName(), base.errorCategory()); + processedDetails.add(newEntry); + } + } + + return processedDetails; } /** @@ -648,20 +745,15 @@ public long getMaxCountForExercise(long exerciseId) { /** * Retrieves a paginated list of students affected by specific feedback entries for a given exercise. *
- * This method filters students based on feedback IDs and returns participation details for each affected student. It uses - * pagination and sorting (order based on the {@link PageUtil.ColumnMapping#AFFECTED_STUDENTS}) to allow efficient retrieval and sorting of the results, thus supporting large - * datasets. + * This method filters students based on feedback IDs and returns participation details for each affected student. *
* * @param exerciseId for which the affected student participation data is requested. * @param feedbackIds used to filter the participation to only those affected by specific feedback entries. - * @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters. - * @return A {@link Page} of {@link FeedbackAffectedStudentDTO} objects, each representing a student affected by the feedback. + * @return A {@link List} of {@link FeedbackAffectedStudentDTO} objects, each representing a student affected by the feedback. */ - public Page getAffectedStudentsWithFeedbackId(long exerciseId, String feedbackIds, PageableSearchDTO data) { - List feedbackIdLongs = Arrays.stream(feedbackIds.split(",")).map(Long::valueOf).toList(); - PageRequest pageRequest = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.AFFECTED_STUDENTS); - return studentParticipationRepository.findAffectedStudentsByFeedbackId(exerciseId, feedbackIdLongs, pageRequest); + public List getAffectedStudentsWithFeedbackIds(long exerciseId, List feedbackIds) { + return studentParticipationRepository.findAffectedStudentsByFeedbackIds(exerciseId, feedbackIds); } /** @@ -692,15 +784,4 @@ public void deleteLongFeedback(List feedbackList, Result result) { List feedbacks = new ArrayList<>(feedbackList); result.updateAllFeedbackItems(feedbacks, true); } - - /** - * Retrieves the number of students affected by a specific feedback detail text for a given exercise. - * - * @param exerciseId for which the affected student count is requested. - * @param detailText used to filter affected students. - * @return the total number of distinct students affected by the feedback detail text. - */ - public long getAffectedStudentCountByFeedbackDetailText(long exerciseId, String detailText) { - return studentParticipationRepository.countAffectedStudentsByFeedbackDetailText(exerciseId, detailText); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index ed6bc5ce12d3..a78718f35a39 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -7,14 +7,15 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; -import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -23,7 +24,6 @@ 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.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -39,7 +39,6 @@ import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; -import de.tum.cit.aet.artemis.core.dto.pageablesearch.PageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; @@ -297,7 +296,7 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * Pagination, sorting, and filtering options allow flexible data retrieval: *