diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 89a46315a1..7b1a727718 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -206,11 +206,11 @@ jobs: images: | inventree/inventree ghcr.io/${{ github.repository }} - - uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # pin@v1 + - uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1 - name: Push Docker Images id: push-docker if: github.event_name != 'pull_request' - uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # pin@v1 + uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # pin@v1 with: project: jczzbjkk68 context: . diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 985c0e213d..6b3a9cb590 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -23,8 +23,6 @@ env: INVENTREE_SITE_URL: http://localhost:8000 INVENTREE_DEBUG: true - use_performance: false - permissions: contents: read @@ -42,6 +40,8 @@ jobs: cicd: ${{ steps.filter.outputs.cicd }} requirements: ${{ steps.filter.outputs.requirements }} runner-perf: ${{ steps.runner-perf.outputs.runner }} + performance: ${{ steps.performance.outputs.force-performance }} + submit-performance: ${{ steps.runner-perf.outputs.submit-performance }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 @@ -77,10 +77,29 @@ jobs: if: | contains(github.event.pull_request.labels.*.name, 'dependency') || contains(github.event.pull_request.labels.*.name, 'full-run') + - name: Is performance testing being forced? + run: echo "force-performance=true" >> $GITHUB_OUTPUT + id: performance + if: | + contains(github.event.pull_request.labels.*.name, 'performance-run') - name: Which runner to use? + env: + GITHUB_REF: ${{ github.ref }} + PERFORMANCE: ${{ steps.performance.outputs.force-performance }} id: runner-perf # decide if we are running in inventree/inventree -> use codspeed-macro runner else ubuntu-24.04 - run: echo "runner=$([[ '${{ github.repository }}' == 'inventree/InvenTree' && '${{ env.use_performance }}' == 'true' ]] && echo 'codspeed-macro' || echo 'ubuntu-24.04')" >> $GITHUB_OUTPUT + run: | + is_main_push=false + if [[ '${{ github.event_name }}' == 'push' && "$GITHUB_REF" == 'refs/heads/master' ]]; then + is_main_push=true + fi + if [[ '${{ github.repository }}' == 'inventree/InvenTree' && ( "$is_main_push" == 'true' || "$PERFORMANCE" == 'true' ) ]]; then + echo "runner=codspeed-macro" >> "$GITHUB_OUTPUT" + echo "submit-performance=true" >> "$GITHUB_OUTPUT" + else + echo "runner=ubuntu-24.04" >> "$GITHUB_OUTPUT" + echo "submit-performance=false" >> "$GITHUB_OUTPUT" + fi pre-commit: name: Style [pre-commit] @@ -183,7 +202,7 @@ jobs: - name: Export API Documentation run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml - name: Upload schema - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: schema.yml path: src/backend/InvenTree/schema.yml @@ -232,17 +251,17 @@ jobs: - name: Extract settings / tags run: invoke int.export-definitions --basedir docs - name: Upload settings - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: inventree_settings.json path: docs/generated/inventree_settings.json - name: Upload tags - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: inventree_tags.yml path: docs/generated/inventree_tags.yml - name: Upload filters - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: inventree_filters.yml path: docs/generated/inventree_filters.yml @@ -265,7 +284,7 @@ jobs: - name: Create artifact directory run: mkdir -p artifact - name: Download schema artifact - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # pin@v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0 with: path: artifact merge-multiple: true @@ -292,7 +311,7 @@ jobs: runs-on: ${{ needs.paths-filter.outputs.runner-perf }} needs: ["pre-commit", "paths-filter"] - if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true' + if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true' || needs.paths-filter.outputs.performance == 'true' permissions: contents: read id-token: write @@ -341,10 +360,11 @@ jobs: pip uninstall pytest-django -y cd ${WRAPPER_NAME} pip install . + if: needs.paths-filter.outputs.submit-performance == 'true' - name: Performance Reporting uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # pin@v4 # check if we are in inventree/inventree - reporting only works in that OIDC context - if: github.repository == 'inventree/InvenTree' + if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true' with: mode: walltime run: pytest ./src/performance --codspeed @@ -387,7 +407,7 @@ jobs: - name: Coverage Tests run: invoke dev.test --check --coverage --translations - name: Upload raw coverage to artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: coverage path: .coverage @@ -406,7 +426,7 @@ jobs: needs: ["pre-commit", "paths-filter"] # check if we are in inventree/inventree - reporting only works in that OIDC context - if: (needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true') && github.repository == 'inventree/InvenTree' + if: (needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true') && github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true' permissions: contents: read id-token: write @@ -701,7 +721,7 @@ jobs: invoke static env INVENTREE_CUSTOM_SPLASH="img/playwright_custom_splash.png" INVENTREE_CUSTOM_LOGO="img/playwright_custom_logo.png" npx nyc playwright test --project=customization npx nyc playwright test --project=chromium --project=firefox - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 if: ${{ !cancelled() && steps.tests.outcome == 'failure' }} with: name: playwright-report @@ -746,7 +766,7 @@ jobs: run: | cd src/backend/InvenTree/web/static zip -r frontend-build.zip web/ web/.vite - - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: frontend-build path: src/backend/InvenTree/web/static/web diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 40f48e9723..e48d8987af 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -55,7 +55,7 @@ jobs: - name: Build frontend run: cd src/frontend && npm run compile && npm run build - name: Create SBOM for frontend - uses: anchore/sbom-action@0b82b0b1a22399a1c542d4d656f70cd903571b5c # pin@v0 + uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # pin@v0 with: artifact-name: frontend-build.spdx path: src/frontend @@ -73,12 +73,12 @@ jobs: zip -r ../frontend-build.zip * .vite - name: Attest Build Provenance id: attest - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # pin@v1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # pin@v1 with: subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip" - name: Upload frontend - uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3 + uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: src/backend/InvenTree/web/static/frontend-build.zip @@ -86,12 +86,12 @@ jobs: tag: ${{ github.ref }} overwrite: true - name: Upload frontend to artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0 with: name: frontend-build path: src/backend/InvenTree/web/static/frontend-build.zip - name: Upload Attestation - uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3 + uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4 with: repo_token: ${{ secrets.GITHUB_TOKEN }} asset_name: frontend-build.intoto.jsonl @@ -134,7 +134,7 @@ jobs: cd docs/site zip -r docs-html.zip * - name: Publish documentation - uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3 + uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: docs/site/docs-html.zip @@ -163,7 +163,7 @@ jobs: persist-credentials: false - name: Get frontend artifact - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # pin@v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0 with: name: frontend-build - name: Setup @@ -244,7 +244,7 @@ jobs: channel: ${{ env.pkg_channel }} file: ${{ steps.package.outputs.package_path }} - name: Publish to artifact - uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3 + uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ steps.package.outputs.package_path }} diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index 9b779ce73a..11926cacbc 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index d8b024b852..fc770c386e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -16,7 +16,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # pin@v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # pin@v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue seems stale. Please react to show this is still important." diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index e743100701..20249055bc 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -56,7 +56,7 @@ jobs: echo "Resetting to HEAD~" git reset HEAD~ || true - name: crowdin action - uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # pin@v2 + uses: crowdin/github-action@8818ff65bfc4322384f983ea37e3926948c11745 # pin@v2 with: upload_sources: true upload_translations: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6832270e7b..a4d6e46287 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: files: docs/requirements\.(in|txt)$ - id: pip-compile name: pip-compile requirements.txt - args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, -c, src/backend/requirements.txt] + args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.14, -c, src/backend/requirements.txt] files: contrib/container/requirements\.(in|txt)$ - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.36.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index c135487674..a7970539d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -[#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs. - -[#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API. - -[#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules. +- [#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default. +- [#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs. +- [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API. +- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules. +- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality. +- [#11374](https://github.com/inventree/InvenTree/pull/11374) adds `updated_at` field on purchase, sales and return orders. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c62407f07..c589902e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,6 @@ InvenTree/ ├─ CONTRIBUTING.md # Contribution guidelines and overview ├─ Procfile # Process definition for Debian/Ubuntu packages ├─ README.md # General project information and overview -├─ runtime.txt # Python runtime settings for Debian/Ubuntu packages build ├─ SECURITY.md # Project security policy ├─ tasks.py # Action definitions for development, testing and deployment ``` diff --git a/contrib/container/Dockerfile b/contrib/container/Dockerfile index f33549827c..b7ee19c4f6 100644 --- a/contrib/container/Dockerfile +++ b/contrib/container/Dockerfile @@ -9,8 +9,8 @@ # - Runs InvenTree web server under django development server # - Monitors source files for any changes, and live-reloads server -# Base image last bumped 2026-02-12 -FROM python:3.11-slim-trixie@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf AS inventree_base +# Base image last bumped 2026-02-23 +FROM python:3.14-slim-trixie@sha256:486b8092bfb12997e10d4920897213a06563449c951c5506c2a2cfaf591c599f AS inventree_base # Build arguments for this image ARG commit_tag="" diff --git a/contrib/container/requirements.txt b/contrib/container/requirements.txt index 17049dbde5..698f701c22 100644 --- a/contrib/container/requirements.txt +++ b/contrib/container/requirements.txt @@ -1,14 +1,14 @@ # This file was autogenerated by uv via the following command: -# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.11 -c src/backend/requirements.txt +# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.14 -c src/backend/requirements.txt asgiref==3.11.1 \ --hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \ --hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133 # via # -c src/backend/requirements.txt # django -django==5.2.11 \ - --hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \ - --hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0 +django==5.2.12 \ + --hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \ + --hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb # via # -c src/backend/requirements.txt # -r contrib/container/requirements.in @@ -236,7 +236,6 @@ typing-extensions==4.15.0 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # -c src/backend/requirements.txt - # psycopg # psycopg-pool uv==0.9.22 \ --hash=sha256:012bdc5285a9cdb091ac514b7eb8a707e3b649af5355fe4afb4920bfe1958c00 \ diff --git a/contrib/dev_reqs/requirements.txt b/contrib/dev_reqs/requirements.txt index 73d6c609ec..35ea2c4c2a 100644 --- a/contrib/dev_reqs/requirements.txt +++ b/contrib/dev_reqs/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt -c src/backend/requirements.txt -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # -c src/backend/requirements.txt # requests diff --git a/docs/docs/concepts/user_interface.md b/docs/docs/concepts/user_interface.md index a7a20f99f0..665b9fcf02 100644 --- a/docs/docs/concepts/user_interface.md +++ b/docs/docs/concepts/user_interface.md @@ -291,3 +291,11 @@ Users may opt to disable the spotlight search functionality if they do not find Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization. If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability. + +## Language Support + +The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language. + +The default system language can be configured by the system administrator in the [server configuration options](../start/config.md#basic-options). + +Additionally, users can select their preferred language in their [user settings](../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with. diff --git a/docs/docs/develop/contributing.md b/docs/docs/develop/contributing.md index d3a2f03595..7918bd9572 100644 --- a/docs/docs/develop/contributing.md +++ b/docs/docs/develop/contributing.md @@ -62,6 +62,7 @@ InvenTree roughly follow the [GitLab flow](https://about.gitlab.com/topics/versi There are nominally 5 active branches: - `master` - The main development branch - `stable` - The latest stable release +- `next-breaking` - The next breaking release (e.g. 2.0, 3.0) with all deprecated features removed - `l10n` - Translation branch: Source to Crowdin - `l10_crowdin` - Translation branch: Source from Crowdin - `y.y.x` - Release branch for the currently supported version (e.g. `0.5.x`) @@ -115,6 +116,23 @@ The translation process is as follows: 4. Translations made in Crowdin are automatically pushed back to the `l10_crowdin` branch by Crowdin once they are approved 5. The `l10_crowdin` branch is merged back into `master` by a maintainer periodically +### `next-breaking` Branch + +Used for easier testing of plugins and integrations against the next major release. It is branched from master when a major release is cut and updated on minor release. The branch is not build into docker images or packages and not meant to be run in production. + + +All deprecated features (REST or python API endpoints mostly) are removed from this branch after each minor release. This allows plugin developers to test their plugins against the next major release early and identify any extensive changes before the major release is cut. + +Only breaking changes are added to this branch. No new features should be added at any point to this branch, only breaking removals / changes. + +Before a major release is cut (1.12.5 > 2.0.0), this branch is merged back into `master`. + + +During the life-time of a major release line (1.0.1, 1.1.x, 1.2.x, 1.3.x, ..., 1.12.5) all deprecation removals are collected in this branch. +On every minor release (1.11.8 > 1.12.0) the `master` is rebased onto the `next-breaking` branch. + +Every time a change with depreations is merged into `master`, a follow up PR that removes the newly-introduced deprecation is created targeting the `next-breaking` branch. After the next minor is released and `master` was rebased into `next-breaking` all the PRs from the previous minor release line can be merged into the `next-breaking` branch. Deprecation removals for the - possibly - long running major release line can be collected this way without having a large number of deprecation removals PRs open. + ## API versioning The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed. diff --git a/docs/docs/settings/error_codes.md b/docs/docs/settings/error_codes.md index ad09a3e6cd..59874a9402 100644 --- a/docs/docs/settings/error_codes.md +++ b/docs/docs/settings/error_codes.md @@ -84,23 +84,27 @@ This is a security measure to prevent plugins from changing the core functionali An error occurred when discovering or initializing a machine type from a plugin. This likely indicates a faulty or incompatible plugin. #### INVE-E13 - **Error reading InvenTree configuration file** An error occurred while reading the InvenTree configuration file. This might be caused by a syntax error or invalid value in the configuration file. #### INVE-E14 - **Could not import Django** Django is not installed in the current Python environment. This means that the InvenTree backend is not running within the correct [python virtual environment](../start/index.md#virtual-environment) or that the required Python packages were not installed correctly. #### INVE-E15 - **Python version not supported** This error occurs attempting to run InvenTree on a version of Python which is older than the minimum required version. We [require Python {{ config.extra.min_python_version }} or newer](../start/index.md#python-requirements) +#### INVE-E16 +**Restore was stopped due to critical issues with backup environment - Backend** + +A potentially critical mismatch between the backup environment and the current restore environment was detected during the restore process. The restore was stopped to prevent potential data loss or corruption. Check the logs for more information about the detected issues. + +While using [invoke](../start/invoke.md), this can be overridden with the `--restore-allow-newer-version` flag. + ### INVE-W (InvenTree Warning) Warnings - These are non-critical errors which should be addressed when possible. @@ -199,6 +203,14 @@ A user attempted to sign up but registration is currently disabled via the syste To enable registration, adjust the relevant settings (for regular or SSO registration) to allow user signups. +#### INVE-W13 +**Current environment inconsistent with backup environment - Backend** + +The environment in which the backup was taken does not match the current environment. This might lead to issues with restoring the backup, as the backup might contain data that is not compatible with the current environment. Plugins for example might be missing or are present in a different version - this can lead to issues with restoring the backup. + +This warning will not prevent you from restoring the backup but it is recommended to ensure the mentioned issues are resolved before restoring the backup to prevent issues with the restored instance. + + ### INVE-I (InvenTree Information) Information — These are not errors but information messages. They might point out potential issues or just provide information. @@ -215,5 +227,9 @@ An issue was detected with the application of a filtering serializer or decorato This warning should only be raised during development and not in production, if you recently installed a plugin you might want to contact the plugin author. +#### INVE-I3 +**Backup and restore information - Backend** +Information about the metadata of a backup being restored or the environment in which a backup is being restored to. This information can be helpful to understand the backup and restore process and to identify potential issues with the backup or the restore process. + ### INVE-M (InvenTree Miscellaneous) Miscellaneous — These are information messages that might be used to mark debug information or other messages helpful for the InvenTree team to understand behaviour. diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index d443084077..66dddda681 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -170,6 +170,7 @@ Configuration of label printing: {{ globalsetting("PART_SALABLE") }} {{ globalsetting("PART_VIRTUAL") }} {{ globalsetting("PART_COPY_BOM") }} +{{ globalsetting("PART_BOM_ALLOW_ZERO_QUANTITY") }} {{ globalsetting("PART_COPY_PARAMETERS") }} {{ globalsetting("PART_COPY_TESTS") }} {{ globalsetting("PART_CATEGORY_PARAMETERS") }} diff --git a/docs/docs/start/backup.md b/docs/docs/start/backup.md index 4861b7e77f..e96cc0088a 100644 --- a/docs/docs/start/backup.md +++ b/docs/docs/start/backup.md @@ -29,6 +29,7 @@ The following configuration options are available for backup: | INVENTREE_BACKUP_DATE_FORMAT | backup_date_format | Date format string used to format timestamps in backup filenames. | `%Y-%m-%d-%H%M%S` | | INVENTREE_BACKUP_DATABASE_FILENAME_TEMPLATE | backup_database_filename_template | Template string used to generate database backup filenames. | `InvenTree-db-{datetime}.{extension}` | | INVENTREE_BACKUP_MEDIA_FILENAME_TEMPLATE | backup_media_filename_template | Template string used to generate media backup filenames. | `InvenTree-media-{datetime}.{extension}` | +| INVENTREE_BACKUP_RESTORE_ALLOW_NEWER_VERSION | backup_restore_allow_newer_version | If True, allows restoring a backup created with a newer version of InvenTree. This is dangerous as it can lead to hard-to-debug data loss. | False | ### Storage Backend diff --git a/docs/requirements.txt b/docs/requirements.txt index 78b69bb50d..440c3c41f8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -28,9 +28,9 @@ bracex==2.6 \ --hash=sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952 \ --hash=sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7 # via wcmatch -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # -c src/backend/requirements.txt # httpcore @@ -391,9 +391,9 @@ mkdocs-macros-plugin==1.5.0 \ --hash=sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f \ --hash=sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f # via -r docs/requirements.in -mkdocs-material==9.7.1 \ - --hash=sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c \ - --hash=sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8 +mkdocs-material==9.7.3 \ + --hash=sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d \ + --hash=sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22 # via -r docs/requirements.in mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ @@ -562,9 +562,9 @@ requests==2.32.5 \ # mkdocs-macros-plugin # mkdocs-material # mkdocs-mermaid2-plugin -rich==14.3.2 \ - --hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \ - --hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8 +rich==14.3.3 \ + --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ + --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b # via neoteroi-mkdocs setuptools==82.0.0 \ --hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \ diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 2871dd1ec4..a41015e398 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,22 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 457 +INVENTREE_API_VERSION = 460 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v460 -> 2026-02-25 : https://github.com/inventree/InvenTree/pull/11374 + - Adds "updated_at" field to PurchaseOrder, SalesOrder and ReturnOrder API endpoints + - Adds "updated_before" and "updated_after" date filters to all three order list endpoints + - Adds "updated_at" ordering option to all three order list endpoints + +v459 -> 2026-02-23 : https://github.com/inventree/InvenTree/pull/11411 + - Changed PurchaseOrderLine "auto_pricing" default value from true to false + +v458 -> 2026-02-22 : https://github.com/inventree/InvenTree/pull/11401 + - Switches token refresh endpoint to use POST instead of GET (upstream allauth change) + v457 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/10887 - Extend the "auto allocate" wizard API to include tracked items diff --git a/src/backend/InvenTree/InvenTree/apps.py b/src/backend/InvenTree/InvenTree/apps.py index a04148c040..50d5b6ae5d 100644 --- a/src/backend/InvenTree/InvenTree/apps.py +++ b/src/backend/InvenTree/InvenTree/apps.py @@ -65,10 +65,12 @@ class InvenTreeConfig(AppConfig): self.start_background_tasks() if not InvenTree.ready.isInTestMode(): # pragma: no cover - # Update exchange rates - InvenTree.tasks.offload_task(InvenTree.tasks.update_exchange_rates) # Let the background worker check for migrations InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) + # Update exchange rates + InvenTree.tasks.offload_task( + InvenTree.tasks.update_exchange_rates, force_async=True + ) self.update_site_url() self.load_unit_registry() diff --git a/src/backend/InvenTree/InvenTree/backup.py b/src/backend/InvenTree/InvenTree/backup.py index 9a14da11b3..bb08308124 100644 --- a/src/backend/InvenTree/InvenTree/backup.py +++ b/src/backend/InvenTree/InvenTree/backup.py @@ -5,7 +5,16 @@ We use the django-dbbackup library to handle backup and restore operations. Ref: https://archmonger.github.io/django-dbbackup/latest/configuration/ """ +from datetime import datetime, timedelta + +from django.conf import settings + +import structlog + import InvenTree.config +import InvenTree.version + +logger = structlog.get_logger('inventree') def get_backup_connector_options() -> dict: @@ -131,3 +140,131 @@ def backup_media_filename_template() -> str: default_value='InvenTree-media-{datetime}.{extension}', typecast=str, ) + + +# schema for backup metadata +InvenTreeBackupMetadata = dict[str, str | int | bool | None] + + +def _gather_environment_metadata() -> InvenTreeBackupMetadata: + """Gather metadata about the current environment to be stored with the backup.""" + import plugin.installer + + new_data: InvenTreeBackupMetadata = {} + + new_data['ivt_1_debug'] = settings.DEBUG + new_data['ivt_1_version'] = InvenTree.version.inventreeVersion() + new_data['ivt_1_version_api'] = InvenTree.version.inventreeApiVersion() + new_data['ivt_1_plugins_enabled'] = settings.PLUGINS_ENABLED + new_data['ivt_1_plugins_file_hash'] = plugin.installer.plugins_file_hash() + new_data['ivt_1_installer'] = InvenTree.config.inventreeInstaller() + new_data['ivt_1_backup_time'] = datetime.now().isoformat() + + return new_data + + +def _parse_environment_metadata(metadata: InvenTreeBackupMetadata) -> dict[str, str]: + """Parse backup metadata to extract environment information.""" + data = {} + + data['debug'] = metadata.get('ivt_1_debug', False) + data['version'] = metadata.get('ivt_1_version', 'unknown') + data['version_api'] = metadata.get('ivt_1_version_api', 'unknown') + data['plugins_enabled'] = metadata.get('ivt_1_plugins_enabled', False) + data['plugins_file_hash'] = metadata.get('ivt_1_plugins_file_hash', 'unknown') + data['installer'] = metadata.get('ivt_1_installer', 'unknown') + data['backup_time'] = metadata.get('ivt_1_backup_time', 'unknown') + + return data + + +def metadata_set(metadata) -> InvenTreeBackupMetadata: + """Set backup metadata for the current backup operation.""" + return _gather_environment_metadata() + + +def validate_restore(metadata: InvenTreeBackupMetadata) -> bool | None: + """Validate whether a backup restore operation should proceed, based on the provided metadata.""" + if metadata.get('ivt_1_version') is None: + logger.warning( + 'INVE-W13: Backup metadata does not contain version information', + error_code='INVE-W13', + ) + return True + + current_environment = _parse_environment_metadata(_gather_environment_metadata()) + backup_environment = _parse_environment_metadata(metadata) + + # Version mismatch + if backup_environment['version'] != current_environment['version']: + logger.warning( + f'INVE-W13: Backup being restored was created with InvenTree version {backup_environment["version"]}, but current version is {current_environment["version"]}', + error_code='INVE-W13', + ) + + # Backup is from newer version - fail + try: + if int(backup_environment['version_api']) > int( + str(current_environment['version_api']) + ): + logger.error( + 'INVE-E16: Backup being restored was created with a newer version of InvenTree - restore cannot proceed. If you are using the invoke task for your restore this warning might be overridden once with `--restore-allow-newer-version`', + error_code='INVE-E16', + ) + # Check for pass flag to allow restore + if not settings.BACKUP_RESTORE_ALLOW_NEWER_VERSION: # defaults to False + return False + else: + logger.warning( + 'INVE-W13: Backup restore is allowing a restore from a newer version of InvenTree - this can lead to data loss or corruption', + error_code='INVE-W13', + ) + except ValueError: # pragma: no cover + logger.warning( + 'INVE-W13: Could not parse API version from backup metadata - cannot determine if backup is from newer version', + error_code='INVE-W13', + ) + + # Plugins enabled on backup but not restore environment - warn + if ( + backup_environment['plugins_enabled'] + and not current_environment['plugins_enabled'] + ): + logger.warning( + 'INVE-W13: Backup being restored was created with plugins enabled, but current environment has plugins disabled - this can lead to data loss', + error_code='INVE-W13', + ) + + # Plugins file hash mismatch - warn + if pg_hash := backup_environment['plugins_file_hash']: + if pg_hash != current_environment['plugins_file_hash']: + logger.warning( + 'INVE-W13: Backup being restored has a different plugins file hash to the current environment - this can lead to data loss or corruption', + error_code='INVE-W13', + ) + + # Installer mismatch - warn + if installer := backup_environment['installer']: + if installer != current_environment['installer']: + logger.warning( + f"INVE-W13: Backup being restored was created with installer '{installer}', but current environment has installer '{current_environment['installer']}'", + error_code='INVE-W13', + ) + + # Age of backup + last_backup_time = backup_environment.get('backup_time') + if datetime.now() - datetime.fromisoformat(last_backup_time) > timedelta(days=120): + logger.warning( + f'INVE-W13: Backup being restored is over 120 days old (last backup time: {last_backup_time})', + error_code='INVE-W13', + ) + + if settings.DEBUG: # pragma: no cover + logger.info( + f'INVE-I3: Backup environment: {backup_environment}', error_code='INVE-I3' + ) + logger.info( + f'INVE-I3: Current environment: {current_environment}', error_code='INVE-I3' + ) + + return True diff --git a/src/backend/InvenTree/InvenTree/filters.py b/src/backend/InvenTree/InvenTree/filters.py index 78c5c5e455..44319b843f 100644 --- a/src/backend/InvenTree/InvenTree/filters.py +++ b/src/backend/InvenTree/InvenTree/filters.py @@ -111,9 +111,11 @@ class InvenTreeOrderingFilter(filters.OrderingFilter): def get_ordering(self, request, queryset, view): """Override ordering for supporting aliases.""" - ordering = super().get_ordering(request, queryset, view) + ordering = list(super().get_ordering(request, queryset, view) or []) aliases = getattr(view, 'ordering_field_aliases', None) + lookup_field = getattr(view, 'lookup_field', 'pk') + lookup_reversed = len(ordering) > 0 and ordering[-1].startswith('-') # Attempt to map ordering fields based on provided aliases if ordering is not None and aliases is not None: @@ -123,9 +125,8 @@ class InvenTreeOrderingFilter(filters.OrderingFilter): ordering = [] for field in ordering_initial: - reverse = field.startswith('-') - - if reverse: + field_reversed = field.startswith('-') + if field_reversed: field = field[1:] # Are aliases defined for this field? @@ -153,11 +154,22 @@ class InvenTreeOrderingFilter(filters.OrderingFilter): continue for a in alias: - if reverse: + if field_reversed: a = '-' + a ordering.append(a) + # Ensure that any API filtering appends the primary-key field + # This is to prevent "ambiguous ordering" errors across pagination boundaries + # Ref: https://github.com/inventree/InvenTree/issues/11442 + if lookup_field and not any( + field in ordering for field in [lookup_field, f'-{lookup_field}'] + ): + if lookup_reversed: + ordering.append(f'-{lookup_field}') + else: + ordering.append(lookup_field) + return ordering @@ -220,18 +232,10 @@ class NumericInFilter(rest_filters.BaseInFilter): return super().filter(qs, numeric_values) -SEARCH_ORDER_FILTER = [ - drf_backend.DjangoFilterBackend, - InvenTreeSearchFilter, - filters.OrderingFilter, -] +ORDER_FILTER = [drf_backend.DjangoFilterBackend, InvenTreeOrderingFilter] -SEARCH_ORDER_FILTER_ALIAS = [ +SEARCH_ORDER_FILTER = [ drf_backend.DjangoFilterBackend, InvenTreeSearchFilter, InvenTreeOrderingFilter, ] - -ORDER_FILTER = [drf_backend.DjangoFilterBackend, filters.OrderingFilter] - -ORDER_FILTER_ALIAS = [drf_backend.DjangoFilterBackend, InvenTreeOrderingFilter] diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 8206e101cc..0d8831f365 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -27,7 +27,7 @@ from corsheaders.defaults import default_headers as default_cors_headers import InvenTree.backup from InvenTree.cache import get_cache_config, is_global_cache_enabled from InvenTree.config import get_boolean_setting, get_oidc_private_key, get_setting -from InvenTree.ready import isInMainThread +from InvenTree.ready import isInMainThread, isRunningBackup from InvenTree.sentry import default_sentry_dsn, init_sentry from InvenTree.version import checkMinPythonVersion, inventreeCommitHash from users.oauth2_scopes import oauth2_scopes @@ -258,12 +258,22 @@ DBBACKUP_EMAIL_SUBJECT_PREFIX = InvenTree.backup.backup_email_prefix() DBBACKUP_CONNECTORS = {'default': InvenTree.backup.get_backup_connector_options()} +DBBACKUP_BACKUP_METADATA_SETTER = InvenTree.backup.metadata_set +DBBACKUP_RESTORE_METADATA_VALIDATOR = InvenTree.backup.validate_restore + # Data storage options DBBACKUP_STORAGE_CONFIG = { 'BACKEND': InvenTree.backup.get_backup_storage_backend(), 'OPTIONS': InvenTree.backup.get_backup_storage_options(), } +# This can also be overridden with a command line flag --restore-allow-newer-version when running the restore command +BACKUP_RESTORE_ALLOW_NEWER_VERSION = get_boolean_setting( + 'INVENTREE_BACKUP_RESTORE_ALLOW_NEWER_VERSION', + 'backup_restore_allow_newer_version', + False, +) + # Enable django admin interface? INVENTREE_ADMIN_ENABLED = get_boolean_setting( 'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True @@ -862,7 +872,7 @@ TRACING_ENABLED = get_boolean_setting( ) TRACING_DETAILS: Optional[dict] = None -if TRACING_ENABLED: # pragma: no cover +if TRACING_ENABLED and not isRunningBackup(): # pragma: no cover from InvenTree.tracing import setup_instruments, setup_tracing _t_endpoint = get_setting('INVENTREE_TRACING_ENDPOINT', 'tracing.endpoint', None) diff --git a/src/backend/InvenTree/InvenTree/tasks.py b/src/backend/InvenTree/InvenTree/tasks.py index f5b207e805..887c11899d 100644 --- a/src/backend/InvenTree/InvenTree/tasks.py +++ b/src/backend/InvenTree/InvenTree/tasks.py @@ -587,7 +587,10 @@ def update_exchange_rates(force: bool = False): Arguments: force: If True, force the update to run regardless of the last update time """ - from InvenTree.ready import canAppAccessDatabase + from InvenTree.ready import canAppAccessDatabase, isRunningMigrations + + if isRunningMigrations(): + return # Do not update exchange rates if we cannot access the database if not canAppAccessDatabase(allow_test=True, allow_shell=True): diff --git a/src/backend/InvenTree/InvenTree/test_commands.py b/src/backend/InvenTree/InvenTree/test_commands.py index d744dd78d2..82caec7d73 100644 --- a/src/backend/InvenTree/InvenTree/test_commands.py +++ b/src/backend/InvenTree/InvenTree/test_commands.py @@ -2,10 +2,15 @@ from pathlib import Path +from django.conf import settings from django.contrib.auth.models import User from django.core.management import call_command from django.test import TestCase +from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor + +from InvenTree.config import get_testfolder_dir + class CommandTestCase(TestCase): """Test case for custom management commands.""" @@ -66,3 +71,144 @@ class CommandTestCase(TestCase): output = call_command('remove_mfa', username=my_admin3.username, verbosity=0) self.assertEqual(output, 'done') self.assertEqual(my_admin3.authenticator_set.all().count(), 0) + + def test_backup_metadata(self): + """Test the backup metadata functions.""" + from InvenTree.backup import ( + _gather_environment_metadata, + _parse_environment_metadata, + ) + + metadata = _gather_environment_metadata() + self.assertIn('ivt_1_version', metadata) + self.assertIn('ivt_1_plugins_enabled', metadata) + + parsed = _parse_environment_metadata(metadata) + self.assertIn('version', parsed) + self.assertIn('plugins_enabled', parsed) + + def test_restore_validation(self): + """Test the restore validation functions.""" + from InvenTree.backup import _gather_environment_metadata, validate_restore + + metadata = _gather_environment_metadata() + + self.assertTrue(validate_restore(metadata)) + + # Version + with self.assertLogs() as cm: + self.assertTrue(validate_restore({})) + self.assertIn( + 'INVE-W13: Backup metadata does not contain version information', str(cm[1]) + ) + with self.assertLogs() as cm: + self.assertTrue(validate_restore({**metadata, 'ivt_1_version': '123xx'})) + self.assertIn( + 'INVE-W13: Backup being restored was created with InvenTree version', + str(cm[1]), + ) + with self.assertLogs() as cm: + self.assertFalse( + validate_restore({ + **metadata, + 'ivt_1_version': '9999', + 'ivt_1_version_api': '9999', + }) + ) + self.assertIn( + 'INVE-E16: Backup being restored was created with a newer version', + str(cm[1]), + ) + # not with allow flag + with self.settings(BACKUP_RESTORE_ALLOW_NEWER_VERSION=True): + with self.assertLogs() as cm: + self.assertTrue( + validate_restore({ + **metadata, + 'ivt_1_version': '9999', + 'ivt_1_version_api': '9999', + }) + ) + self.assertIn( + 'INVE-W13: Backup restore is allowing a restore from a newer version of InvenTree', + str(cm[1]), + ) + + # Plugins enabled + with self.settings(PLUGINS_ENABLED=False): + with self.assertLogs() as cm: + self.assertTrue(validate_restore(metadata)) + self.assertIn( + 'INVE-W13: Backup being restored was created with plugins enabled', + str(cm[1]), + ) + + # Plugin hash + with self.assertLogs() as cm: + self.assertTrue( + validate_restore({**metadata, 'ivt_1_plugins_file_hash': '123xx'}) + ) + self.assertIn( + 'INVE-W13: Backup being restored has a different plugins file hash', + str(cm[1]), + ) + + # installer + with self.assertLogs() as cm: + self.assertTrue(validate_restore({**metadata, 'ivt_1_installer': '123xx'})) + self.assertIn( + 'INVE-W13: Backup being restored was created with installer', str(cm[1]) + ) + + # Age + with self.assertLogs() as cm: + self.assertTrue( + validate_restore({ + **metadata, + 'ivt_1_backup_time': '2020-02-02T00:00:00', + }) + ) + self.assertIn( + 'INVE-W13: Backup being restored is over 120 days old', str(cm[1]) + ) + + def test_backup_command_e2e(self): + """Test the backup command.""" + # we only test on sqlite, the environment in which we also run coverage + if settings.DB_ENGINE != 'django.db.backends.sqlite3': + self.skipTest('Backup command test only runs on sqlite database') + + # disable tracing for now + if settings.TRACING_ENABLED: # pragma: no cover + print('Disabling tracing for backup command test') + SQLite3Instrumentor().uninstrument() + + output_path = get_testfolder_dir().joinpath('backup.zip').resolve() + + # Backup + with self.assertLogs() as cm: + output = call_command( + 'dbbackup', noinput=True, verbosity=2, output_path=str(output_path) + ) + self.assertIsNone(output) + self.assertIn(f'Writing metadata file to {output_path}', str(cm[1])) + + # Restore + with self.assertLogs() as cm: + output = call_command( + 'dbrestore', + noinput=True, + interactive=False, + verbosity=2, + input_path=str(output_path), + ) + self.assertIsNone(output) + self.assertIn('Using connector from metadata', str(cm[1])) + + # Cleanup the generated backup file and metadata file + output_path.unlink() + Path(str(output_path) + '.metadata').unlink() + + if settings.TRACING_ENABLED: # pragma: no cover + print('Re-enabling tracing for backup command test') + SQLite3Instrumentor().instrument() diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index b155602b50..e94590e67f 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -27,7 +27,7 @@ from generic.states.api import StatusView from InvenTree.api import BulkDeleteMixin, ParameterListMixin, meta_path from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration from InvenTree.filters import ( - SEARCH_ORDER_FILTER_ALIAS, + SEARCH_ORDER_FILTER, InvenTreeDateFilter, NumberOrNullFilter, ) @@ -343,7 +343,7 @@ class BuildList( output_options = BuildListOutputOptions filterset_class = BuildFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_fields = [ 'reference', 'part', @@ -594,7 +594,7 @@ class BuildLineList( """API endpoint for accessing a list of BuildLine objects.""" filterset_class = BuildLineFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = BuildLineOutputOptions ordering_fields = [ 'part', @@ -951,7 +951,7 @@ class BuildItemList( output_options = BuildItemOutputOptions filterset_class = BuildItemFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER def get_queryset(self): """Override the queryset method, to perform custom prefetch.""" diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index eccd885d98..3b6cc14de5 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -49,11 +49,7 @@ from InvenTree.api import ( meta_path, ) from InvenTree.config import CONFIG_LOOKUPS -from InvenTree.filters import ( - ORDER_FILTER, - SEARCH_ORDER_FILTER, - SEARCH_ORDER_FILTER_ALIAS, -) +from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER from InvenTree.helpers import inheritors, str2bool from InvenTree.helpers_email import send_email from InvenTree.mixins import ( @@ -1079,7 +1075,7 @@ class ParameterList( """List API endpoint for Parameter objects.""" filterset_class = ParameterFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_fields = ['name', 'data', 'units', 'template', 'updated', 'updated_by'] diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 156e6e61b3..00fb35a558 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -637,6 +637,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'PART_BOM_ALLOW_ZERO_QUANTITY': { + 'name': _('Allow BOM Zero Quantity'), + 'description': _( + 'Accept a zero quantity for BOM item for part. Enables using setup quantity to define a quantity required per build, independent of build quantity' + ), + 'default': False, + 'validator': bool, + }, 'LABEL_ENABLE': { 'name': _('Enable label printing'), 'description': _('Enable label printing from the web interface'), diff --git a/src/backend/InvenTree/company/api.py b/src/backend/InvenTree/company/api.py index 8c1fe8db05..9ce3eaf27c 100644 --- a/src/backend/InvenTree/company/api.py +++ b/src/backend/InvenTree/company/api.py @@ -11,7 +11,7 @@ import part.models from data_exporter.mixins import DataExportViewMixin from InvenTree.api import ListCreateDestroyAPIView, ParameterListMixin, meta_path from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration -from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS +from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.mixins import ( ListCreateAPI, OutputOptionsMixin, @@ -197,7 +197,7 @@ class ManufacturerPartList( """ filterset_class = ManufacturerPartFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = ManufacturerOutputOptions ordering_fields = ['part', 'IPN', 'MPN', 'manufacturer'] @@ -360,7 +360,7 @@ class SupplierPartList( """ filterset_class = SupplierPartFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = SupplierPartOutputOptions ordering_fields = [ @@ -475,7 +475,7 @@ class SupplierPriceBreakList( output_options = SupplierPriceBreakOutputOptions filterset_class = SupplierPriceBreakFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_fields = ['quantity', 'supplier', 'SKU', 'price'] search_fields = ['part__SKU', 'part__supplier__name'] diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index 5fa96c96c3..c27c6e32ff 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -552,6 +552,12 @@ class DataImportRow(models.Model): self.valid = self.validate() super().save(*args, **kwargs) + def delete(self, *args, **kwargs): + """Update the session progress when a row is deleted.""" + session = self.session + super().delete(*args, **kwargs) + session.check_complete() + session = models.ForeignKey( DataImportSession, on_delete=models.CASCADE, diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 096e8d1dfb..b8e214a0ac 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -35,11 +35,7 @@ from InvenTree.api import ( meta_path, ) from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration -from InvenTree.filters import ( - SEARCH_ORDER_FILTER, - SEARCH_ORDER_FILTER_ALIAS, - InvenTreeDateFilter, -) +from InvenTree.filters import SEARCH_ORDER_FILTER, InvenTreeDateFilter from InvenTree.helpers import str2bool from InvenTree.helpers_model import construct_absolute_url, get_base_url from InvenTree.mixins import ( @@ -228,6 +224,14 @@ class OrderFilter(FilterSet): label=_('Target Date After'), field_name='target_date', lookup_expr='gt' ) + updated_before = InvenTreeDateFilter( + label=_('Updated Before'), field_name='updated_at', lookup_expr='lt' + ) + + updated_after = InvenTreeDateFilter( + label=_('Updated After'), field_name='updated_at', lookup_expr='gt' + ) + min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date') def filter_min_date(self, queryset, name, value): @@ -391,7 +395,7 @@ class PurchaseOrderList( """ filterset_class = PurchaseOrderFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = PurchaseOrderOutputOptions ordering_field_aliases = { @@ -420,6 +424,7 @@ class PurchaseOrderList( 'responsible', 'total_price', 'project_code', + 'updated_at', ] ordering = '-reference' @@ -700,7 +705,7 @@ class PurchaseOrderLineItemList( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_field_aliases = { 'MPN': 'part__manufacturer_part__MPN', @@ -859,7 +864,7 @@ class SalesOrderList( """ filterset_class = SalesOrderFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = SalesOrderOutputOptions ordering_field_aliases = { @@ -882,6 +887,7 @@ class SalesOrderList( 'shipment_date', 'total_price', 'project_code', + 'updated_at', ] search_fields = [ @@ -1043,7 +1049,7 @@ class SalesOrderLineItemList( filterset_class = SalesOrderLineItemFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = SalesOrderLineItemOutputOptions @@ -1289,7 +1295,7 @@ class SalesOrderAllocationList( """API endpoint for listing SalesOrderAllocation objects.""" filterset_class = SalesOrderAllocationFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = SalesOrderAllocationOutputOptions ordering_fields = [ @@ -1399,7 +1405,7 @@ class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI): """API list endpoint for SalesOrderShipment model.""" filterset_class = SalesOrderShipmentFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_fields = ['reference', 'delivery_date', 'shipment_date', 'allocated_items'] search_fields = [ @@ -1528,7 +1534,7 @@ class ReturnOrderList( """API endpoint for accessing a list of ReturnOrder objects.""" filterset_class = ReturnOrderFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = ReturnOrderOutputOptions @@ -1549,6 +1555,7 @@ class ReturnOrderList( 'target_date', 'complete_date', 'project_code', + 'updated_at', ] search_fields = [ @@ -1674,7 +1681,7 @@ class ReturnOrderLineItemList( filterset_class = ReturnOrderLineItemFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER output_options = ReturnOrderLineItemOutputOptions diff --git a/src/backend/InvenTree/order/migrations/0115_purchaseorder_updated_at_returnorder_updated_at_and_more.py b/src/backend/InvenTree/order/migrations/0115_purchaseorder_updated_at_returnorder_updated_at_and_more.py new file mode 100644 index 0000000000..837e2a35e4 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0115_purchaseorder_updated_at_returnorder_updated_at_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.11 on 2026-02-19 22:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("order", "0114_purchaseorderextraline_project_code_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="purchaseorder", + name="updated_at", + field=models.DateTimeField( + blank=True, + null=True, + help_text="Timestamp of last update", + verbose_name="Updated At", + ), + ), + migrations.AddField( + model_name="returnorder", + name="updated_at", + field=models.DateTimeField( + blank=True, + null=True, + help_text="Timestamp of last update", + verbose_name="Updated At", + ), + ), + migrations.AddField( + model_name="salesorder", + name="updated_at", + field=models.DateTimeField( + blank=True, + null=True, + help_text="Timestamp of last update", + verbose_name="Updated At", + ), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 2b9f8e08d0..0eb780984e 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -9,7 +9,7 @@ from django.core.validators import MinValueValidator from django.db import models, transaction from django.db.models import F, Q, QuerySet, Sum from django.db.models.functions import Coalesce -from django.db.models.signals import post_save +from django.db.models.signals import post_delete, post_save from django.dispatch.dispatcher import receiver from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -331,6 +331,8 @@ class Order( if not self.creation_date: self.creation_date = InvenTree.helpers.current_date() + self.updated_at = InvenTree.helpers.current_time() + super().save(*args, **kwargs) def check_locked(self, db: bool = False) -> bool: @@ -498,6 +500,13 @@ class Order( help_text=_('Date order was issued'), ) + updated_at = models.DateTimeField( + null=True, + blank=True, + verbose_name=_('Updated At'), + help_text=_('Timestamp of last update'), + ) + responsible = models.ForeignKey( UserModels.Owner, on_delete=models.SET_NULL, @@ -3073,3 +3082,43 @@ class ReturnOrderExtraLine(OrderExtraLine): verbose_name=_('Order'), help_text=_('Return Order'), ) + + +def _touch_order_updated_at(instance): + """Bump updated_at on the parent order without triggering a full save.""" + if not InvenTree.ready.canAppAccessDatabase(allow_test=True): + return + instance.order.__class__.objects.filter(pk=instance.order_id).update( + updated_at=InvenTree.helpers.current_time() + ) + + +@receiver(post_save, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_save') +@receiver( + post_delete, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_delete' +) +@receiver( + post_save, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_save' +) +@receiver( + post_delete, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_delete' +) +@receiver(post_save, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_save') +@receiver( + post_delete, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_delete' +) +@receiver(post_save, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_save') +@receiver( + post_delete, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_delete' +) +@receiver(post_save, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_save') +@receiver( + post_delete, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_delete' +) +@receiver(post_save, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_save') +@receiver( + post_delete, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_delete' +) +def update_order_on_lineitem_change(sender, instance, **kwargs): + """Update parent order updated_at when any line item is saved or deleted.""" + _touch_order_updated_at(instance) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index c62ce69d90..7613323271 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -373,8 +373,14 @@ class PurchaseOrderSerializer( 'total_price', 'order_currency', 'destination', + 'updated_at', ]) - read_only_fields = ['issue_date', 'complete_date', 'creation_date'] + read_only_fields = [ + 'issue_date', + 'complete_date', + 'creation_date', + 'updated_at', + ] extra_kwargs = { 'supplier': {'required': True}, 'order_currency': {'required': False}, @@ -639,7 +645,7 @@ class PurchaseOrderLineItemSerializer( help_text=_( 'Automatically calculate purchase price based on supplier part data' ), - default=True, + default=False, ) destination_detail = enable_filter( @@ -1026,8 +1032,9 @@ class SalesOrderSerializer( 'shipments_count', 'completed_shipments_count', 'allocated_lines', + 'updated_at', ]) - read_only_fields = ['status', 'creation_date', 'shipment_date'] + read_only_fields = ['status', 'creation_date', 'shipment_date', 'updated_at'] extra_kwargs = {'order_currency': {'required': False}} def skip_create_fields(self): @@ -1918,8 +1925,9 @@ class ReturnOrderSerializer( 'customer_reference', 'order_currency', 'total_price', + 'updated_at', ]) - read_only_fields = ['creation_date'] + read_only_fields = ['creation_date', 'updated_at'] def skip_create_fields(self): """Skip these fields when instantiating a new object.""" diff --git a/src/backend/InvenTree/order/tests.py b/src/backend/InvenTree/order/tests.py index 1712f68759..e4e04c82ee 100644 --- a/src/backend/InvenTree/order/tests.py +++ b/src/backend/InvenTree/order/tests.py @@ -25,7 +25,17 @@ from part.models import Part from stock.models import StockItem, StockLocation from users.models import Owner -from .models import PurchaseOrder, PurchaseOrderExtraLine, PurchaseOrderLineItem +from .models import ( + PurchaseOrder, + PurchaseOrderExtraLine, + PurchaseOrderLineItem, + ReturnOrder, + ReturnOrderExtraLine, + ReturnOrderLineItem, + SalesOrder, + SalesOrderExtraLine, + SalesOrderLineItem, +) class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase): @@ -369,7 +379,8 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase): order=po, part=sp_1, quantity=3, - purchase_price=Money(1000, 'USD'), # "Unit price" should be $100USD + # "Unit price" should be $100USD + purchase_price=Money(1000, 'USD'), ) # 13 x 0.1 = 1.3 @@ -569,3 +580,151 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase): p.set_metadata(k, k) self.assertEqual(len(p.metadata.keys()), 4) + + +class OrderUpdatedAtTest(TestCase): + """Tests to verify that the updated_at field is correctly maintained on all order types.""" + + def setUp(self): + """Set up objects for all three order types.""" + self.supplier = Company.objects.filter(is_supplier=True).first() + self.customer = Company.objects.filter(is_customer=True).first() + + self.po = PurchaseOrder.objects.create( + reference='PO-TEST-001', supplier=self.supplier + ) + self.so = SalesOrder.objects.create( + reference='SO-TEST-001', customer=self.customer + ) + self.ro = ReturnOrder.objects.create( + reference='RO-TEST-001', customer=self.customer + ) + + self.part = Part.objects.create(name='Test Part', description='Test Part') + self.stock_item = StockItem.objects.create(part=self.part, quantity=10) + + def _refresh(self, instance): + """Return a fresh copy of the instance from the database.""" + return instance.__class__.objects.get(pk=instance.pk) + + def test_updated_at_set_on_save(self): + """updated_at should be populated after the order is saved.""" + for instance in [self.po, self.so, self.ro]: + self.assertIsNotNone(self._refresh(instance).updated_at) + + def test_updated_at_changes_on_save(self): + """updated_at should advance when the order is saved again.""" + for instance in [self.po, self.so, self.ro]: + original = self._refresh(instance).updated_at + + instance.description = 'Updated description' + instance.save() + + refreshed = self._refresh(instance) + self.assertGreaterEqual(refreshed.updated_at, original) + + def test_updated_at_on_extra_line_add(self): + """updated_at should advance on the parent order when an extra line is added.""" + for instance, ExtraLine in [ + (self.po, PurchaseOrderExtraLine), + (self.so, SalesOrderExtraLine), + (self.ro, ReturnOrderExtraLine), + ]: + before = self._refresh(instance).updated_at + + ExtraLine.objects.create(order=instance, quantity=1) + + after = self._refresh(instance).updated_at + self.assertGreaterEqual(after, before) + + def test_updated_at_on_extra_line_update(self): + """updated_at should advance on the parent order when an extra line is updated.""" + for instance, ExtraLine in [ + (self.po, PurchaseOrderExtraLine), + (self.so, SalesOrderExtraLine), + (self.ro, ReturnOrderExtraLine), + ]: + line = ExtraLine.objects.create(order=instance, quantity=1) + + before = self._refresh(instance).updated_at + + line.quantity = 5 + line.save() + + after = self._refresh(instance).updated_at + self.assertGreaterEqual(after, before) + + def test_updated_at_on_extra_line_delete(self): + """updated_at should advance on the parent order when an extra line is deleted.""" + for instance, ExtraLine in [ + (self.po, PurchaseOrderExtraLine), + (self.so, SalesOrderExtraLine), + (self.ro, ReturnOrderExtraLine), + ]: + line = ExtraLine.objects.create(order=instance, quantity=1) + + before = self._refresh(instance).updated_at + + line.delete() + + after = self._refresh(instance).updated_at + self.assertGreaterEqual(after, before) + + def test_updated_at_on_line_item_add(self): + """updated_at should advance on the parent order when a regular line item is added.""" + before_po = self._refresh(self.po).updated_at + PurchaseOrderLineItem.objects.create(order=self.po, part=None, quantity=1) + self.assertGreaterEqual(self._refresh(self.po).updated_at, before_po) + + before_so = self._refresh(self.so).updated_at + SalesOrderLineItem.objects.create(order=self.so, part=None, quantity=1) + self.assertGreaterEqual(self._refresh(self.so).updated_at, before_so) + + before_ro = self._refresh(self.ro).updated_at + ReturnOrderLineItem.objects.create( + order=self.ro, item=self.stock_item, quantity=1 + ) + self.assertGreaterEqual(self._refresh(self.ro).updated_at, before_ro) + + def test_updated_at_on_line_item_update(self): + """updated_at should advance on the parent order when a regular line item is updated.""" + po_line = PurchaseOrderLineItem.objects.create( + order=self.po, part=None, quantity=1 + ) + so_line = SalesOrderLineItem.objects.create( + order=self.so, part=None, quantity=1 + ) + ro_line = ReturnOrderLineItem.objects.create( + order=self.ro, item=self.stock_item, quantity=1 + ) + + for instance, line in [ + (self.po, po_line), + (self.so, so_line), + (self.ro, ro_line), + ]: + before = self._refresh(instance).updated_at + line.quantity = 5 + line.save() + self.assertGreaterEqual(self._refresh(instance).updated_at, before) + + def test_updated_at_on_line_item_delete(self): + """updated_at should advance on the parent order when a regular line item is deleted.""" + po_line = PurchaseOrderLineItem.objects.create( + order=self.po, part=None, quantity=1 + ) + so_line = SalesOrderLineItem.objects.create( + order=self.so, part=None, quantity=1 + ) + ro_line = ReturnOrderLineItem.objects.create( + order=self.ro, item=self.stock_item, quantity=1 + ) + + for instance, line in [ + (self.po, po_line), + (self.so, so_line), + (self.ro, ro_line), + ]: + before = self._refresh(instance).updated_at + line.delete() + self.assertGreaterEqual(self._refresh(instance).updated_at, before) diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index db8eefd5e6..31169bf878 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -24,9 +24,7 @@ from InvenTree.api import ( from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration from InvenTree.filters import ( ORDER_FILTER, - ORDER_FILTER_ALIAS, SEARCH_ORDER_FILTER, - SEARCH_ORDER_FILTER_ALIAS, InvenTreeDateFilter, InvenTreeSearchFilter, NumberOrNullFilter, @@ -302,7 +300,7 @@ class CategoryTree(ListAPI): queryset = PartCategory.objects.all() serializer_class = part_serializers.CategoryTree - filter_backends = ORDER_FILTER_ALIAS + filter_backends = ORDER_FILTER ordering_fields = ['level', 'name', 'subcategories'] @@ -1080,7 +1078,7 @@ class PartList( filterset_class = PartFilter is_create = True - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_fields = [ 'name', @@ -1442,7 +1440,7 @@ class BomList( output_options = BomOutputOptions filterset_class = BomFilter - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER search_fields = [ 'reference', diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index bb27afe074..8102ac4641 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -22,6 +22,7 @@ from sql_util.utils import SubqueryCount import common.currency import common.filters +import common.models import common.serializers import company.models import InvenTree.helpers @@ -1656,8 +1657,20 @@ class BomItemSerializer( def validate_quantity(self, quantity): """Perform validation for the BomItem quantity field.""" - if quantity <= 0: - raise serializers.ValidationError(_('Quantity must be greater than zero')) + allow_zero_qty = common.models.InvenTreeSetting.get_setting( + 'PART_BOM_ALLOW_ZERO_QUANTITY', False + ) + + if allow_zero_qty: + if quantity < 0: + raise serializers.ValidationError( + _('Quantity must be greater than or equal to zero') + ) + else: + if quantity <= 0: + raise serializers.ValidationError( + _('Quantity must be greater than zero') + ) return quantity diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 9b2fd32a3b..bef906f6e5 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -37,9 +37,8 @@ from InvenTree.api import ( ) from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration from InvenTree.filters import ( - ORDER_FILTER_ALIAS, + ORDER_FILTER, SEARCH_ORDER_FILTER, - SEARCH_ORDER_FILTER_ALIAS, InvenTreeDateFilter, NumberOrNullFilter, ) @@ -455,7 +454,7 @@ class StockLocationTree(ListAPI): queryset = StockLocation.objects.all() serializer_class = StockSerializers.LocationTreeSerializer - filter_backends = ORDER_FILTER_ALIAS + filter_backends = ORDER_FILTER ordering_fields = ['level', 'name', 'sublocations'] @@ -1260,7 +1259,7 @@ class StockList( headers=self.get_success_headers(serializer.data), ) - filter_backends = SEARCH_ORDER_FILTER_ALIAS + filter_backends = SEARCH_ORDER_FILTER ordering_field_aliases = { 'part': 'part__name', diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 405f90a6b8..cbaea852f9 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -575,6 +575,81 @@ class StockItemListTest(StockAPITestCase): for ordering in ['part', 'location', 'stock', 'status', 'IPN', 'MPN', 'SKU']: self.run_ordering_test(self.list_url, ordering) + def test_pagination(self): + """Test that pagination boundaries are observed correctly. + + Ref: https://github.com/inventree/InvenTree/issues/11442 + """ + location = StockLocation.objects.first() + part = Part.objects.first() + + items = [] + + # Delete all existing stock item objects + for item in StockItem.objects.all(): + item.delete() + + for idx in range(1000): + items.append( + StockItem( + part=part, + location=location, + quantity=idx % 10, + level=0, + lft=0, + rght=0, + tree_id=0, + ) + ) + + if len(items) >= 100: + StockItem.objects.bulk_create(items) + items = [] + + self.assertEqual(StockItem.objects.count(), 1000) + + url = reverse('api-stock-list') + + # Keep track of the unique PKs we have seen in the results + unique_pks = set() + + for idx in range(0, 100, 10): + data = self.get(url, {'limit': 10, 'offset': idx}).data + self.assertEqual(data['count'], 1000) + self.assertEqual(len(data['results']), 10) + + for item in data['results']: + self.assertNotIn( + item['pk'], + unique_pks, + f'Duplicate PK {item["pk"]} found in paginated results @ page {idx // 10}', + ) + unique_pks.add(item['pk']) + + self.assertEqual( + len(unique_pks), 100, 'Expected to see 100 unique PKs in paginated results' + ) + + # Run same test again, with reverse ordering on part IPN + unique_pks = set() + + for idx in range(0, 100, 10): + data = self.get(url, {'limit': 10, 'offset': idx, 'ordering': '-IPN'}).data + self.assertEqual(data['count'], 1000) + self.assertEqual(len(data['results']), 10) + + for item in data['results']: + self.assertNotIn( + item['pk'], + unique_pks, + f'Duplicate PK {item["pk"]} found in paginated results @ page {idx // 10} with reverse ordering', + ) + unique_pks.add(item['pk']) + + self.assertEqual( + len(unique_pks), 100, 'Expected to see 100 unique PKs in paginated results' + ) + def test_top_level_filtering(self): """Test filtering against "top level" stock location.""" # No filters, should return *all* items diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index ac459aef18..d56202ad6d 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -101,16 +101,16 @@ blessed==1.30.0 \ # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -boto3==1.42.54 \ - --hash=sha256:71194e855bfc81a21872cbe29c41f52ffdbe67e0a184a52c13346ef00b328939 \ - --hash=sha256:fe3d8ec586c39a0c96327fd317c77ca601ec5f991e9ba7211cacae8db4c07a73 +boto3==1.42.58 \ + --hash=sha256:1bc5ff0b7a1a3f42b115481e269e1aada1d68bbfa80a989ac2882d51072907a3 \ + --hash=sha256:3a21b5bbc8bf8d6472a7ae7bdc77819b1f86f35d127f428f4603bed1b98122c0 # via # -c src/backend/requirements.txt # django-anymail # django-storages -botocore==1.42.54 \ - --hash=sha256:853a0822de66d060aeebafa07ca13a03799f7958313d1b29f8dc7e2e1be8f527 \ - --hash=sha256:ab203d4e57d22913c8386a695d048e003b7508a8a4a7a46c9ddf4ebd67a20b69 +botocore==1.42.58 \ + --hash=sha256:3098178f4404cf85c8997ebb7948b3f267cff1dd191b08fc4ebb614ac1013a20 \ + --hash=sha256:55224d6a91afae0997e8bee62d1ef1ae2dcbc6c210516939b32a774b0b35bec5 # via # -c src/backend/requirements.txt # boto3 @@ -219,9 +219,9 @@ brotli==1.2.0 \ # via # -c src/backend/requirements.txt # fonttools -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # -c src/backend/requirements.txt # requests @@ -504,9 +504,9 @@ defusedxml==0.7.1 \ # via # -c src/backend/requirements.txt # python3-openid -django==5.2.11 \ - --hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \ - --hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0 +django==5.2.12 \ + --hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \ + --hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -538,9 +538,9 @@ django==5.2.11 \ # djangorestframework # djangorestframework-simplejwt # drf-spectacular -django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \ - --hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \ - --hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15 +django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \ + --hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \ + --hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -877,68 +877,68 @@ googleapis-common-protos==1.72.0 \ # -c src/backend/requirements.txt # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -grpcio==1.78.1 \ - --hash=sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85 \ - --hash=sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca \ - --hash=sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862 \ - --hash=sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b \ - --hash=sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f \ - --hash=sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97 \ - --hash=sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0 \ - --hash=sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd \ - --hash=sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338 \ - --hash=sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72 \ - --hash=sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907 \ - --hash=sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba \ - --hash=sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd \ - --hash=sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966 \ - --hash=sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22 \ - --hash=sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11 \ - --hash=sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f \ - --hash=sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b \ - --hash=sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce \ - --hash=sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2 \ - --hash=sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b \ - --hash=sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70 \ - --hash=sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529 \ - --hash=sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9 \ - --hash=sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb \ - --hash=sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7 \ - --hash=sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512 \ - --hash=sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460 \ - --hash=sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0 \ - --hash=sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22 \ - --hash=sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74 \ - --hash=sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb \ - --hash=sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494 \ - --hash=sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04 \ - --hash=sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f \ - --hash=sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42 \ - --hash=sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1 \ - --hash=sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79 \ - --hash=sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541 \ - --hash=sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67 \ - --hash=sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba \ - --hash=sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465 \ - --hash=sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f \ - --hash=sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d \ - --hash=sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496 \ - --hash=sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e \ - --hash=sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89 \ - --hash=sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0 \ - --hash=sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068 \ - --hash=sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd \ - --hash=sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f \ - --hash=sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4 \ - --hash=sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a \ - --hash=sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1 \ - --hash=sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d \ - --hash=sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747 \ - --hash=sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668 \ - --hash=sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb \ - --hash=sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20 \ - --hash=sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1 \ - --hash=sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28 +grpcio==1.78.0 \ + --hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \ + --hash=sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d \ + --hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \ + --hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \ + --hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \ + --hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \ + --hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \ + --hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \ + --hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \ + --hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \ + --hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \ + --hash=sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec \ + --hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \ + --hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \ + --hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \ + --hash=sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb \ + --hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \ + --hash=sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df \ + --hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \ + --hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \ + --hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \ + --hash=sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf \ + --hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \ + --hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \ + --hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \ + --hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \ + --hash=sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5 \ + --hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \ + --hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \ + --hash=sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1 \ + --hash=sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5 \ + --hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \ + --hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \ + --hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \ + --hash=sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf \ + --hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \ + --hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \ + --hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \ + --hash=sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c \ + --hash=sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597 \ + --hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \ + --hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \ + --hash=sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813 \ + --hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \ + --hash=sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2 \ + --hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \ + --hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \ + --hash=sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a \ + --hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \ + --hash=sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5 \ + --hash=sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a \ + --hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \ + --hash=sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c \ + --hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \ + --hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \ + --hash=sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525 \ + --hash=sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de \ + --hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \ + --hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \ + --hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce \ + --hash=sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -949,9 +949,9 @@ gunicorn==25.1.0 \ # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -icalendar==7.0.1 \ - --hash=sha256:1f577af3e4d022a701aa98f9366dd524f0f1134b90c73c28493f06d5371901d5 \ - --hash=sha256:68a6295f31ed907885ae79f10336b53ba067e0a66075b6a956abbe5e89ac7b39 +icalendar==7.0.2 \ + --hash=sha256:ad31a5825b39522a30b073c6ced3ffcdf6c02cbb7dab69ba2e4de32ddbf77cc9 \ + --hash=sha256:de844ff5cde32f539bea7644e36d8494032a926b933bedb92621f2f239760806 # via # -c src/backend/requirements.txt # django-ical @@ -1669,9 +1669,9 @@ pynacl==1.6.2 \ # via # -c src/backend/requirements.txt # paramiko -pypdf==6.7.2 \ - --hash=sha256:331b63cd66f63138f152a700565b3e0cebdf4ec8bec3b7594b2522418782f1f3 \ - --hash=sha256:82a1a48de500ceea59a52a7d979f5095927ef802e4e4fac25ab862a73468acbb +pypdf==6.7.5 \ + --hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \ + --hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d # via # -c src/backend/requirements.txt # -r src/backend/requirements.in @@ -1899,9 +1899,9 @@ rapidfuzz==3.14.3 \ # via # -c src/backend/requirements.txt # -r src/backend/requirements.in -redis==7.2.0 \ - --hash=sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497 \ - --hash=sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26 +redis==7.2.1 \ + --hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \ + --hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5 # via # -c src/backend/requirements.txt # django-redis @@ -2164,9 +2164,9 @@ webencodings==0.5.1 \ # cssselect2 # tinycss2 # tinyhtml5 -whitenoise==6.11.0 \ - --hash=sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f \ - --hash=sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258 +whitenoise==6.12.0 \ + --hash=sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad \ + --hash=sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index 9c0d511a27..09f0138f8f 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -11,9 +11,9 @@ build==1.4.0 \ --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 # via pip-tools -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt @@ -401,9 +401,9 @@ distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -django==5.2.11 \ - --hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \ - --hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0 +django==5.2.12 \ + --hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \ + --hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt @@ -437,10 +437,12 @@ django-types==0.23.0 \ --hash=sha256:0727b13ae810c4b1f14eeac9872834ac928c99dc76584ea7c23afc4461e049dd \ --hash=sha256:f97fb746166fb15a5f40e470a1fd7a58226349aac9e0a9cb8ae81deb14d94fd0 # via -r src/backend/requirements-dev.in -filelock==3.21.2 \ - --hash=sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708 \ - --hash=sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560 - # via virtualenv +filelock==3.24.3 \ + --hash=sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa \ + --hash=sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d + # via + # python-discovery + # virtualenv gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ --hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce @@ -460,9 +462,9 @@ iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -isort==7.0.0 \ - --hash=sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 \ - --hash=sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187 +isort==8.0.0 \ + --hash=sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9 \ + --hash=sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c # via -r src/backend/requirements-dev.in markdown-it-py==4.0.0 \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ @@ -503,6 +505,7 @@ platformdirs==4.9.2 \ # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt + # python-discovery # virtualenv pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ @@ -555,10 +558,14 @@ pytest-codspeed==4.3.0 \ --hash=sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5 \ --hash=sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4 # via -r src/backend/requirements-dev.in -pytest-django==4.11.1 \ - --hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \ - --hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991 +pytest-django==4.12.0 \ + --hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \ + --hash=sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758 # via -r src/backend/requirements-dev.in +python-discovery==1.1.0 \ + --hash=sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268 \ + --hash=sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b + # via virtualenv pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -648,9 +655,9 @@ requests-mock==1.12.1 \ --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 # via -r src/backend/requirements-dev.in -rich==14.3.2 \ - --hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \ - --hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8 +rich==14.3.3 \ + --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ + --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b # via pytest-codspeed setuptools==82.0.0 \ --hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \ @@ -688,9 +695,9 @@ ty==0.0.1a21 \ --hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \ --hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a # via -r src/backend/requirements-dev.in -types-psycopg2==2.9.21.20251012 \ - --hash=sha256:4cdafd38927da0cfde49804f39ab85afd9c6e9c492800e42f1f0c1a1b0312935 \ - --hash=sha256:712bad5c423fe979e357edbf40a07ca40ef775d74043de72bd4544ca328cc57e +types-psycopg2==2.9.21.20260223 \ + --hash=sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02 \ + --hash=sha256:c6228ade72d813b0624f4c03feeb89471950ac27cd0506b5debed6f053086bc8 # via django-types types-pyyaml==6.0.12.20250915 \ --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ @@ -712,9 +719,9 @@ urllib3==2.6.3 \ # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt # requests -virtualenv==20.36.1 \ - --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ - --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba +virtualenv==21.1.0 \ + --hash=sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07 \ + --hash=sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44 # via pre-commit wheel==0.46.3 \ --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index f108783f37..d8fa6ba8cf 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -10,9 +10,9 @@ build==1.4.0 \ --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 # via pip-tools -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # -c src/backend/requirements.txt # requests @@ -231,99 +231,113 @@ click==8.3.1 \ --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 # via pip-tools -coverage[toml]==7.13.3 \ - --hash=sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1 \ - --hash=sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67 \ - --hash=sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395 \ - --hash=sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d \ - --hash=sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14 \ - --hash=sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8 \ - --hash=sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f \ - --hash=sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0 \ - --hash=sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c \ - --hash=sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b \ - --hash=sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051 \ - --hash=sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8 \ - --hash=sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea \ - --hash=sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f \ - --hash=sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba \ - --hash=sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3 \ - --hash=sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627 \ - --hash=sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10 \ - --hash=sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508 \ - --hash=sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75 \ - --hash=sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56 \ - --hash=sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3 \ - --hash=sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a \ - --hash=sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4 \ - --hash=sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab \ - --hash=sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8 \ - --hash=sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4 \ - --hash=sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f \ - --hash=sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be \ - --hash=sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024 \ - --hash=sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73 \ - --hash=sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43 \ - --hash=sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c \ - --hash=sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c \ - --hash=sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107 \ - --hash=sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e \ - --hash=sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0 \ - --hash=sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895 \ - --hash=sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43 \ - --hash=sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba \ - --hash=sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e \ - --hash=sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2 \ - --hash=sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f \ - --hash=sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4 \ - --hash=sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25 \ - --hash=sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6 \ - --hash=sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23 \ - --hash=sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7 \ - --hash=sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6 \ - --hash=sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910 \ - --hash=sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155 \ - --hash=sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b \ - --hash=sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9 \ - --hash=sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef \ - --hash=sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39 \ - --hash=sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0 \ - --hash=sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587 \ - --hash=sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00 \ - --hash=sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b \ - --hash=sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3 \ - --hash=sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e \ - --hash=sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a \ - --hash=sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34 \ - --hash=sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a \ - --hash=sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b \ - --hash=sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8 \ - --hash=sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222 \ - --hash=sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b \ - --hash=sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3 \ - --hash=sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9 \ - --hash=sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0 \ - --hash=sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301 \ - --hash=sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e \ - --hash=sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f \ - --hash=sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd \ - --hash=sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8 \ - --hash=sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96 \ - --hash=sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a \ - --hash=sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f \ - --hash=sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc \ - --hash=sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca \ - --hash=sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595 \ - --hash=sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba \ - --hash=sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e \ - --hash=sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a \ - --hash=sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1 \ - --hash=sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac \ - --hash=sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86 \ - --hash=sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad \ - --hash=sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4 \ - --hash=sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0 \ - --hash=sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb +coverage[toml]==7.13.4 \ + --hash=sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246 \ + --hash=sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459 \ + --hash=sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129 \ + --hash=sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6 \ + --hash=sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415 \ + --hash=sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf \ + --hash=sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80 \ + --hash=sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11 \ + --hash=sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0 \ + --hash=sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b \ + --hash=sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9 \ + --hash=sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b \ + --hash=sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f \ + --hash=sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505 \ + --hash=sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47 \ + --hash=sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55 \ + --hash=sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def \ + --hash=sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689 \ + --hash=sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012 \ + --hash=sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5 \ + --hash=sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3 \ + --hash=sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95 \ + --hash=sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9 \ + --hash=sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601 \ + --hash=sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997 \ + --hash=sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c \ + --hash=sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac \ + --hash=sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c \ + --hash=sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa \ + --hash=sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750 \ + --hash=sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3 \ + --hash=sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d \ + --hash=sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12 \ + --hash=sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a \ + --hash=sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932 \ + --hash=sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356 \ + --hash=sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92 \ + --hash=sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148 \ + --hash=sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39 \ + --hash=sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634 \ + --hash=sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6 \ + --hash=sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72 \ + --hash=sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98 \ + --hash=sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef \ + --hash=sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3 \ + --hash=sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9 \ + --hash=sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0 \ + --hash=sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a \ + --hash=sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9 \ + --hash=sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552 \ + --hash=sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc \ + --hash=sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f \ + --hash=sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525 \ + --hash=sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940 \ + --hash=sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a \ + --hash=sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23 \ + --hash=sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f \ + --hash=sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc \ + --hash=sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b \ + --hash=sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056 \ + --hash=sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7 \ + --hash=sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb \ + --hash=sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a \ + --hash=sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd \ + --hash=sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea \ + --hash=sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126 \ + --hash=sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299 \ + --hash=sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9 \ + --hash=sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b \ + --hash=sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00 \ + --hash=sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf \ + --hash=sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda \ + --hash=sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2 \ + --hash=sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5 \ + --hash=sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d \ + --hash=sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9 \ + --hash=sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9 \ + --hash=sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b \ + --hash=sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa \ + --hash=sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092 \ + --hash=sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58 \ + --hash=sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea \ + --hash=sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26 \ + --hash=sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea \ + --hash=sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9 \ + --hash=sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053 \ + --hash=sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f \ + --hash=sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0 \ + --hash=sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3 \ + --hash=sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256 \ + --hash=sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a \ + --hash=sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903 \ + --hash=sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91 \ + --hash=sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd \ + --hash=sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505 \ + --hash=sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7 \ + --hash=sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0 \ + --hash=sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2 \ + --hash=sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a \ + --hash=sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71 \ + --hash=sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985 \ + --hash=sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242 \ + --hash=sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d \ + --hash=sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af \ + --hash=sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c \ + --hash=sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0 # via -r src/backend/requirements-dev.in cryptography==46.0.5 \ --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ @@ -382,9 +396,9 @@ distlib==0.4.0 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -django==5.2.11 \ - --hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \ - --hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0 +django==5.2.12 \ + --hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \ + --hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb # via # -c src/backend/requirements.txt # django-silk @@ -417,9 +431,9 @@ django-types==0.23.0 \ --hash=sha256:0727b13ae810c4b1f14eeac9872834ac928c99dc76584ea7c23afc4461e049dd \ --hash=sha256:f97fb746166fb15a5f40e470a1fd7a58226349aac9e0a9cb8ae81deb14d94fd0 # via -r src/backend/requirements-dev.in -filelock==3.20.3 \ - --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ - --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 +filelock==3.24.3 \ + --hash=sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa \ + --hash=sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d # via virtualenv gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ @@ -439,9 +453,9 @@ iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -isort==7.0.0 \ - --hash=sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 \ - --hash=sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187 +isort==8.0.0 \ + --hash=sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9 \ + --hash=sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c # via -r src/backend/requirements-dev.in markdown-it-py==4.0.0 \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ @@ -471,9 +485,9 @@ pip==26.0.1 \ --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 # via pip-tools -pip-tools==7.5.2 \ - --hash=sha256:2d64d72da6a044da1110257d333960563d7a4743637e8617dd2610ae7b82d60f \ - --hash=sha256:2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce +pip-tools==7.5.3 \ + --hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \ + --hash=sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41 # via -r src/backend/requirements-dev.in platformdirs==4.9.2 \ --hash=sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd \ @@ -513,27 +527,27 @@ pytest==9.0.2 \ # via # pytest-codspeed # pytest-django -pytest-codspeed==4.2.0 \ - --hash=sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7 \ - --hash=sha256:0881a736285f33b9a8894da8fe8e1775aa1a4310226abe5d1f0329228efb680c \ - --hash=sha256:238e17abe8f08d8747fa6c7acff34fefd3c40f17a56a7847ca13dc8d6e8c6009 \ - --hash=sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8 \ - --hash=sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b \ - --hash=sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe \ - --hash=sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37 \ - --hash=sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b \ - --hash=sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8 \ - --hash=sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5 \ - --hash=sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f \ - --hash=sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39 \ - --hash=sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3 \ - --hash=sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca \ - --hash=sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830 \ - --hash=sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0 +pytest-codspeed==4.3.0 \ + --hash=sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a \ + --hash=sha256:10d19b48c9dc35b227a7ac863d4b3c58132256c2ba1aae3220aaddbf6f3f5f9a \ + --hash=sha256:34f2fd8497456eefbd325673f677ea80d93bb1bc08a578c1fa43a09cec3d1879 \ + --hash=sha256:5230d9d65f39063a313ed1820df775166227ec5c20a1122968f85653d5efee48 \ + --hash=sha256:527a3a02eaa3e4d4583adc4ba2327eef79628f3e1c682a4b959439551a72588e \ + --hash=sha256:619120775e92a3f43fb4ff4c256a251b1554c904d95e2154a382484283f0388a \ + --hash=sha256:6ac31ae566bf047e91c79b6d12d9a31efedad556ff9258294d6ecebeacb92fa4 \ + --hash=sha256:878aad5e4bb7b401ad8d82f3af5186030cd2bd0d0446782e10dabb9db8827466 \ + --hash=sha256:9858c2a6e1f391d5696757e7b6e9484749a7376c46f8b4dd9aebf093479a9667 \ + --hash=sha256:a2f5bb6d8898bea7db45e3c8b916ee48e36905b929477bb511b79c5a3ccacda4 \ + --hash=sha256:b2acecc4126658abebc683b38121adec405a46e18a619d49d6154c6e60c5deb2 \ + --hash=sha256:bec30f4fc9c4973143cd80f0d33fa780e9fa3e01e4dbe8cedf229e72f1212c62 \ + --hash=sha256:dbeff1eb2f2e36df088658b556fa993e6937bf64ffb07406de4db16fd2b26874 \ + --hash=sha256:df0d1f6ea594f29b745c634d66d5f5f1caa1c3abd2af82fea49d656038e8fc77 \ + --hash=sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5 \ + --hash=sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4 # via -r src/backend/requirements-dev.in -pytest-django==4.11.1 \ - --hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \ - --hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991 +pytest-django==4.12.0 \ + --hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \ + --hash=sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758 # via -r src/backend/requirements-dev.in pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -622,9 +636,9 @@ requests-mock==1.12.1 \ --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 # via -r src/backend/requirements-dev.in -rich==14.3.2 \ - --hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \ - --hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8 +rich==14.3.3 \ + --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ + --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b # via pytest-codspeed setuptools==82.0.0 \ --hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \ @@ -731,9 +745,9 @@ urllib3==2.6.3 \ # via # -c src/backend/requirements.txt # requests -virtualenv==20.36.1 \ - --hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \ - --hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba +virtualenv==20.38.0 \ + --hash=sha256:94f39b1abaea5185bf7ea5a46702b56f1d0c9aa2f41a6c2b8b0af4ddc74c10a7 \ + --hash=sha256:d6e78e5889de3a4742df2d3d44e779366325a90cf356f15621fddace82431794 # via pre-commit wheel==0.46.3 \ --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 7b5b028ca8..e059b7be4c 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -95,15 +95,15 @@ blessed==1.30.0 \ --hash=sha256:4061a9f10dd22798716c2548ba36385af6a29d856c897f367c6ccc927e0b3a5a \ --hash=sha256:4d547019d7b40fc5420ea2ba2bc180fdccc31d6715298e2b49ffa7b020d44667 # via -r src/backend/requirements.in -boto3==1.42.54 \ - --hash=sha256:71194e855bfc81a21872cbe29c41f52ffdbe67e0a184a52c13346ef00b328939 \ - --hash=sha256:fe3d8ec586c39a0c96327fd317c77ca601ec5f991e9ba7211cacae8db4c07a73 +boto3==1.42.58 \ + --hash=sha256:1bc5ff0b7a1a3f42b115481e269e1aada1d68bbfa80a989ac2882d51072907a3 \ + --hash=sha256:3a21b5bbc8bf8d6472a7ae7bdc77819b1f86f35d127f428f4603bed1b98122c0 # via # django-anymail # django-storages -botocore==1.42.54 \ - --hash=sha256:853a0822de66d060aeebafa07ca13a03799f7958313d1b29f8dc7e2e1be8f527 \ - --hash=sha256:ab203d4e57d22913c8386a695d048e003b7508a8a4a7a46c9ddf4ebd67a20b69 +botocore==1.42.58 \ + --hash=sha256:3098178f4404cf85c8997ebb7948b3f267cff1dd191b08fc4ebb614ac1013a20 \ + --hash=sha256:55224d6a91afae0997e8bee62d1ef1ae2dcbc6c210516939b32a774b0b35bec5 # via # boto3 # s3transfer @@ -209,9 +209,9 @@ brotli==1.2.0 \ --hash=sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361 \ --hash=sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d # via fonttools -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 # via # requests # sentry-sdk @@ -485,9 +485,9 @@ defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 # via python3-openid -django==5.2.11 \ - --hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \ - --hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0 +django==5.2.12 \ + --hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \ + --hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb # via # -r src/backend/requirements.in # django-allauth @@ -518,9 +518,9 @@ django==5.2.11 \ # djangorestframework # djangorestframework-simplejwt # drf-spectacular -django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \ - --hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \ - --hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15 +django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \ + --hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \ + --hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d # via -r src/backend/requirements.in django-anymail[amazon-ses, postal]==14.0 \ --hash=sha256:98e5575bcff231952a92117b7d0a4dd37fab582333a343de302d415a31dc72b1 \ @@ -773,68 +773,68 @@ googleapis-common-protos==1.72.0 \ # via # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-exporter-otlp-proto-http -grpcio==1.78.1 \ - --hash=sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85 \ - --hash=sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca \ - --hash=sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862 \ - --hash=sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b \ - --hash=sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f \ - --hash=sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97 \ - --hash=sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0 \ - --hash=sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd \ - --hash=sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338 \ - --hash=sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72 \ - --hash=sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907 \ - --hash=sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba \ - --hash=sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd \ - --hash=sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966 \ - --hash=sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22 \ - --hash=sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11 \ - --hash=sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f \ - --hash=sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b \ - --hash=sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce \ - --hash=sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2 \ - --hash=sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b \ - --hash=sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70 \ - --hash=sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529 \ - --hash=sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9 \ - --hash=sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb \ - --hash=sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7 \ - --hash=sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512 \ - --hash=sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460 \ - --hash=sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0 \ - --hash=sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22 \ - --hash=sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74 \ - --hash=sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb \ - --hash=sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494 \ - --hash=sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04 \ - --hash=sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f \ - --hash=sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42 \ - --hash=sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1 \ - --hash=sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79 \ - --hash=sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541 \ - --hash=sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67 \ - --hash=sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba \ - --hash=sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465 \ - --hash=sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f \ - --hash=sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d \ - --hash=sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496 \ - --hash=sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e \ - --hash=sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89 \ - --hash=sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0 \ - --hash=sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068 \ - --hash=sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd \ - --hash=sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f \ - --hash=sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4 \ - --hash=sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a \ - --hash=sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1 \ - --hash=sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d \ - --hash=sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747 \ - --hash=sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668 \ - --hash=sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb \ - --hash=sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20 \ - --hash=sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1 \ - --hash=sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28 +grpcio==1.78.0 \ + --hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \ + --hash=sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d \ + --hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \ + --hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \ + --hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \ + --hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \ + --hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \ + --hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \ + --hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \ + --hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \ + --hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \ + --hash=sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec \ + --hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \ + --hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \ + --hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \ + --hash=sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb \ + --hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \ + --hash=sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df \ + --hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \ + --hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \ + --hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \ + --hash=sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf \ + --hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \ + --hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \ + --hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \ + --hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \ + --hash=sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5 \ + --hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \ + --hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \ + --hash=sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1 \ + --hash=sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5 \ + --hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \ + --hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \ + --hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \ + --hash=sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf \ + --hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \ + --hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \ + --hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \ + --hash=sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c \ + --hash=sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597 \ + --hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \ + --hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \ + --hash=sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813 \ + --hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \ + --hash=sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2 \ + --hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \ + --hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \ + --hash=sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a \ + --hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \ + --hash=sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5 \ + --hash=sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a \ + --hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \ + --hash=sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c \ + --hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \ + --hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \ + --hash=sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525 \ + --hash=sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de \ + --hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \ + --hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \ + --hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce \ + --hash=sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7 # via # -r src/backend/requirements.in # opentelemetry-exporter-otlp-proto-grpc @@ -842,9 +842,9 @@ gunicorn==25.1.0 \ --hash=sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616 \ --hash=sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b # via -r src/backend/requirements.in -icalendar==7.0.1 \ - --hash=sha256:1f577af3e4d022a701aa98f9366dd524f0f1134b90c73c28493f06d5371901d5 \ - --hash=sha256:68a6295f31ed907885ae79f10336b53ba067e0a66075b6a956abbe5e89ac7b39 +icalendar==7.0.2 \ + --hash=sha256:ad31a5825b39522a30b073c6ced3ffcdf6c02cbb7dab69ba2e4de32ddbf77cc9 \ + --hash=sha256:de844ff5cde32f539bea7644e36d8494032a926b933bedb92621f2f239760806 # via django-ical idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ @@ -1477,9 +1477,9 @@ pynacl==1.6.2 \ --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 # via paramiko -pypdf==6.7.2 \ - --hash=sha256:331b63cd66f63138f152a700565b3e0cebdf4ec8bec3b7594b2522418782f1f3 \ - --hash=sha256:82a1a48de500ceea59a52a7d979f5095927ef802e4e4fac25ab862a73468acbb +pypdf==6.7.5 \ + --hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \ + --hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d # via -r src/backend/requirements.in pyphen==0.17.2 \ --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ @@ -1686,9 +1686,9 @@ rapidfuzz==3.14.3 \ --hash=sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb \ --hash=sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9 # via -r src/backend/requirements.in -redis==7.2.0 \ - --hash=sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497 \ - --hash=sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26 +redis==7.2.1 \ + --hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \ + --hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5 # via django-redis referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ @@ -1922,9 +1922,9 @@ webencodings==0.5.1 \ # cssselect2 # tinycss2 # tinyhtml5 -whitenoise==6.11.0 \ - --hash=sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f \ - --hash=sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258 +whitenoise==6.12.0 \ + --hash=sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad \ + --hash=sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2 # via -r src/backend/requirements.in wrapt==1.17.3 \ --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ diff --git a/src/frontend/CHANGELOG.md b/src/frontend/CHANGELOG.md index deec3c3251..97db95266d 100644 --- a/src/frontend/CHANGELOG.md +++ b/src/frontend/CHANGELOG.md @@ -2,6 +2,10 @@ This file contains historical changelog information for the InvenTree UI components library. +### 0.8.0 - March 2026 + +Exposes the `monitorDataOutput` hook, which allows plugins to monitor the output of a long-running task and display notifications when the task is complete. This is useful for plugins that need to perform long-running tasks and want to provide feedback to the user when the task is complete. + ### 0.7.0 - October 2025 Exposes stock adjustment forms to plugins, allowing plugins to adjust stock adjustments using the common InvenTree UI form components. diff --git a/src/frontend/lib/hooks/MonitorDataOutput.tsx b/src/frontend/lib/hooks/MonitorDataOutput.tsx new file mode 100644 index 0000000000..a03654cd69 --- /dev/null +++ b/src/frontend/lib/hooks/MonitorDataOutput.tsx @@ -0,0 +1,122 @@ +import { t } from '@lingui/core/macro'; +import { useDocumentVisibility } from '@mantine/hooks'; +import { notifications, showNotification } from '@mantine/notifications'; +import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import type { AxiosInstance } from 'axios'; +import { useEffect, useState } from 'react'; +import { ProgressBar } from '../components/ProgressBar'; +import { ApiEndpoints } from '../enums/ApiEndpoints'; +import { apiUrl } from '../functions/Api'; + +/** + * Hook for monitoring a data output process running on the server + */ +export default function monitorDataOutput({ + api, + title, + hostname, + id +}: { + api: AxiosInstance; + title: string; + hostname?: string; + id?: number; +}) { + const visibility = useDocumentVisibility(); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!!id) { + setLoading(true); + showNotification({ + id: `data-output-${id}`, + title: title, + loading: true, + autoClose: false, + withCloseButton: false, + message: + }); + } else setLoading(false); + }, [id, title]); + + useQuery({ + enabled: !!id && loading && visibility === 'visible', + refetchInterval: 500, + queryKey: ['data-output', id, title], + queryFn: () => + api + .get(apiUrl(ApiEndpoints.data_output, id)) + .then((response) => { + const data = response?.data ?? {}; + + if (!!data.errors || !!data.error) { + setLoading(false); + + const error: string = + data?.error ?? data?.errors?.error ?? t`Process failed`; + + notifications.update({ + id: `data-output-${id}`, + loading: false, + icon: , + autoClose: 2500, + title: title, + message: error, + color: 'red' + }); + } else if (data.complete) { + setLoading(false); + notifications.update({ + id: `data-output-${id}`, + loading: false, + autoClose: 2500, + title: title, + message: t`Process completed successfully`, + color: 'green', + icon: + }); + + if (data.output) { + const url = data.output; + const base = hostname ?? window.location.hostname; + + const downloadUrl = new URL(url, base); + + window.open(downloadUrl.toString(), '_blank'); + } + } else { + notifications.update({ + id: `data-output-${id}`, + loading: true, + autoClose: false, + withCloseButton: false, + message: ( + 0} + animated + /> + ) + }); + } + + return data; + }) + .catch(() => { + setLoading(false); + notifications.update({ + id: `data-output-${id}`, + loading: false, + autoClose: 2500, + title: title, + message: t`Process failed`, + color: 'red' + }); + return {}; + }) + }); +} diff --git a/src/frontend/lib/index.ts b/src/frontend/lib/index.ts index 17e7e20f36..625e65c436 100644 --- a/src/frontend/lib/index.ts +++ b/src/frontend/lib/index.ts @@ -71,3 +71,6 @@ export { RowCancelAction, RowActions } from './components/RowActions'; + +// Shared hooks +export { default as monitorDataOutput } from './hooks/MonitorDataOutput'; diff --git a/src/frontend/lib/types/Filters.tsx b/src/frontend/lib/types/Filters.tsx index 1ef44a3a84..53d5a563ca 100644 --- a/src/frontend/lib/types/Filters.tsx +++ b/src/frontend/lib/types/Filters.tsx @@ -39,7 +39,7 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api'; */ export type TableFilter = { name: string; - label: string; + label?: string; description?: string; type?: TableFilterType; choices?: TableFilterChoice[]; diff --git a/src/frontend/lib/types/Tables.tsx b/src/frontend/lib/types/Tables.tsx index ba0675e285..99f02ce06d 100644 --- a/src/frontend/lib/types/Tables.tsx +++ b/src/frontend/lib/types/Tables.tsx @@ -99,6 +99,8 @@ export type TableState = { * @param cellsStyle - The style of the cells in the column * @param extra - Extra data to pass to the render function * @param noContext - Disable context menu for this column + * @param copyable - Enable copy button on hover (uses accessor to get value, or custom function) + * @param copyAccessor - Custom accessor path for copy value (defaults to column accessor) */ export type TableColumnProps = { accessor?: string; @@ -123,6 +125,8 @@ export type TableColumnProps = { extra?: any; noContext?: boolean; style?: MantineStyleProp; + copyable?: boolean | ((record: T) => string); + copyAccessor?: string; }; /** diff --git a/src/frontend/package.json b/src/frontend/package.json index 1e6d57d0b3..4c283164f9 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@inventreedb/ui", "description": "UI components for the InvenTree project", - "version": "0.7.0", + "version": "0.8.0", "private": false, "type": "module", "license": "MIT", diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index b9e283daa9..a43b4b3099 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/core/macro'; import { ActionIcon, + type ActionIconVariant, Button, type DefaultMantineColor, type FloatingPosition, @@ -22,7 +23,8 @@ export function CopyButton({ tooltipPosition, content, size, - color = 'gray' + color = 'gray', + variant = 'transparent' }: Readonly<{ value: any; label?: string; @@ -32,9 +34,15 @@ export function CopyButton({ content?: JSX.Element; size?: MantineSize; color?: DefaultMantineColor; + variant?: ActionIconVariant; }>) { const ButtonComponent = label ? Button : ActionIcon; + // Disable the copy button if we are not in a secure context, as the Clipboard API is not available + if (!window.isSecureContext) { + return null; + } + return ( {({ copied, copy }) => ( @@ -46,8 +54,12 @@ export function CopyButton({ { + e.stopPropagation(); + e.preventDefault(); + copy(); + }} + variant={copied ? 'transparent' : (variant ?? 'transparent')} size={size ?? 'sm'} > {copied ? ( diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx index d93195d725..50f416a175 100644 --- a/src/frontend/src/components/calendar/Calendar.tsx +++ b/src/frontend/src/components/calendar/Calendar.tsx @@ -29,6 +29,10 @@ import { } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; +import { + defaultLocale, + getPriorityLocale +} from '../../contexts/LanguageContext'; import type { CalendarState } from '../../hooks/UseCalendar'; import { useLocalState } from '../../states/LocalState'; import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer'; @@ -60,7 +64,19 @@ export default function Calendar({ const [locale] = useLocalState(useShallow((s) => [s.language])); // Ensure underscore is replaced with dash - const calendarLocale = useMemo(() => locale.replace('_', '-'), [locale]); + const calendarLocale = useMemo(() => { + let _locale: string | null = locale; + + if (!_locale) { + _locale = getPriorityLocale(); + } + + _locale = _locale || defaultLocale; + + _locale = _locale.replace('_', '-'); + + return _locale; + }, [locale]); const selectMonth = useCallback( (date: DateValue) => { diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 655af719c6..2e61e7377a 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -54,10 +54,16 @@ export type DetailsField = { type BadgeType = 'owner' | 'user' | 'group'; type ValueFormatterReturn = string | number | null | React.ReactNode; -type StringDetailField = { - type: 'string' | 'text' | 'date'; - unit?: boolean; -}; +type StringDetailField = + | { + type: 'string' | 'text'; + unit?: boolean; + } + | { + type: 'date'; + unit?: boolean; + showTime?: boolean; + }; type NumberDetailField = { type: 'number'; @@ -260,7 +266,13 @@ function NameBadge({ } function DateValue(props: Readonly) { - return {formatDate(props.field_value?.toString())}; + return ( + + {formatDate(props.field_value?.toString(), { + showTime: props.field_data?.showTime + })} + + ); } // Return a formatted "number" value, with optional unit diff --git a/src/frontend/src/components/items/LanguageSelect.tsx b/src/frontend/src/components/items/LanguageSelect.tsx index 6ef3cdfc0a..3650603670 100644 --- a/src/frontend/src/components/items/LanguageSelect.tsx +++ b/src/frontend/src/components/items/LanguageSelect.tsx @@ -1,8 +1,12 @@ import { Select } from '@mantine/core'; import { useEffect, useState } from 'react'; +import { t } from '@lingui/core/macro'; import { useShallow } from 'zustand/react/shallow'; -import { getSupportedLanguages } from '../../contexts/LanguageContext'; +import { + activateLocale, + getSupportedLanguages +} from '../../contexts/LanguageContext'; import { useLocalState } from '../../states/LocalState'; export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) { @@ -28,13 +32,21 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) { })); setLangOptions(newLangOptions); setValue(locale); + activateLocale(locale); // Ensure the locale is activated on component load }, [locale]); return (