diff --git a/.github/workflows/check_translations.yaml b/.github/workflows/check_translations.yaml index 5fabe38106..edbdc6a57c 100644 --- a/.github/workflows/check_translations.yaml +++ b/.github/workflows/check_translations.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 62c645427a..124fbcbfd2 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -39,7 +39,7 @@ jobs: docker: ${{ steps.filter.outputs.docker }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1 @@ -67,7 +67,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Test Docker Image @@ -129,7 +129,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Run Migration Tests @@ -153,7 +153,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Set Up Python ${{ env.python_version }} diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 4e6e85ced1..97ddf2a6fc 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -45,7 +45,7 @@ jobs: force: ${{ steps.force.outputs.force }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1 @@ -68,7 +68,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -122,7 +122,7 @@ jobs: VITE_COVERAGE: false steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -201,7 +201,7 @@ jobs: VITE_COVERAGE: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -269,7 +269,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false @@ -300,7 +300,7 @@ jobs: - name: Upload coverage reports to Codecov if: ${{ !cancelled() && github.ref == 'refs/heads/master' }} - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # pin@v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} slug: inventree/InvenTree diff --git a/.github/workflows/import_export.yaml b/.github/workflows/import_export.yaml index 6cf00b0408..1f0599f0b5 100644 --- a/.github/workflows/import_export.yaml +++ b/.github/workflows/import_export.yaml @@ -48,7 +48,7 @@ jobs: outputs: server: ${{ steps.filter.outputs.server }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1 @@ -76,7 +76,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 7737d2b7a3..a4d2c3e1f2 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -44,7 +44,7 @@ jobs: submit-performance: ${{ steps.runner-perf.outputs.submit-performance }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1 @@ -108,7 +108,7 @@ jobs: if: needs.paths-filter.outputs.cicd == 'true' || needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Set up Python ${{ env.python_version }} @@ -130,7 +130,7 @@ jobs: if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -152,7 +152,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Set up Python ${{ env.python_version }} @@ -190,7 +190,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -222,7 +222,7 @@ jobs: echo "Downloaded api.yaml" - name: Running OpenAPI Spec diff action id: breaking_changes - uses: oasdiff/oasdiff-action/diff@f30668f65075c93440bd59ce2de73ce9e78751f4 # pin@main + uses: oasdiff/oasdiff-action/diff@3530478ec30f84adedbfeb28f0d9527a290f50a9 # pin@main with: base: "api.yaml" revision: "src/backend/InvenTree/schema.yml" @@ -275,7 +275,7 @@ jobs: version: ${{ needs.schema.outputs.version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 name: Checkout Code with: repository: inventree/schema @@ -332,7 +332,7 @@ jobs: node_version: '>=24' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -390,7 +390,7 @@ jobs: python_version: ${{ matrix.python_version }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -415,7 +415,7 @@ jobs: path: .coverage retention-days: 14 - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # pin@v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} @@ -441,7 +441,7 @@ jobs: INVENTREE_AUTO_UPDATE: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -492,7 +492,7 @@ jobs: - 6379:6379 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -541,7 +541,7 @@ jobs: - 3306:3306 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -584,7 +584,7 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -597,7 +597,7 @@ jobs: - name: Run Tests run: invoke dev.test --check --migrations --report --coverage --translations - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # pin@v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} @@ -618,7 +618,7 @@ jobs: INVENTREE_PLUGINS_ENABLED: false steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false name: Checkout Code @@ -674,7 +674,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Run zizmor 🌈 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8d44edfcb8..3dc2176956 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Version Check @@ -43,7 +43,7 @@ jobs: contents: write attestations: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -109,7 +109,7 @@ jobs: INVENTREE_DEBUG: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup @@ -149,7 +149,7 @@ jobs: - ubuntu:24.04 - debian:12 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index ba06306c45..5e74fd9529 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index d619040b90..3307459c02 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3 with: persist-credentials: false - name: Environment Setup diff --git a/contrib/container/Dockerfile b/contrib/container/Dockerfile index fd7b4e1995..0427dd15ed 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-03-20 -FROM python:3.14.5-slim-trixie@sha256:c845af9399020c7e562969a13689e929074a10fd057acd1b1fad06a2fb068e97 AS inventree_base +# Base image last bumped 2026-06-16 +FROM python:3.14.6-slim-trixie@sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061 AS inventree_base # Build arguments for this image ARG commit_tag="" diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index 87edf69474..79b080bb1d 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -15,7 +15,7 @@ from django.views.generic.base import RedirectView import structlog from django_q.models import OrmQ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema -from rest_framework import serializers, viewsets +from rest_framework import permissions, serializers, viewsets from rest_framework.generics import GenericAPIView from rest_framework.request import clone_request from rest_framework.response import Response @@ -356,7 +356,10 @@ class InfoView(APIView): class NotFoundView(APIView): """Simple JSON view when accessing an invalid API view.""" - permission_classes = [InvenTree.permissions.AllowAnyOrReadScope] + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.AllowAnyOrReadScope, + ] def not_found(self, request): """Return a 404 error.""" diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 2019005f28..24104985de 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,16 +1,22 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 506 +INVENTREE_API_VERSION = 508 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ -v506 -> 2026-06-16 : https://github.com/inventree/InvenTree/pull/11971 +v508 -> 2026-06-16 : https://github.com/inventree/InvenTree/pull/11971 - Removes direct "notes" field from any models which previously supported markdown notes - Adds a generic "Note" model which can be attached to any model type via a generic foreign key relationship - Allow multiple notes to be attached to a single object, and for notes to be created / edited / deleted via the API +v507 -> 2026-06-16 : https://github.com/inventree/InvenTree/pull/12180 + - Adds "lookup_field" parameter to the DataImportSessionSerializer, which allows for more flexible lookup of related objects during data import operations + +v506 -> 2026-06-15 : https://github.com/inventree/InvenTree/pull/12168 + - Reduce permissions scope for a number of API endpoints, to improve security and ensure that users only have access to the data they need + v505 -> 2026-06-15 : https://github.com/inventree/InvenTree/pull/12165 - Allow parameters to be specified against the PartCategory model diff --git a/src/backend/InvenTree/InvenTree/permissions.py b/src/backend/InvenTree/InvenTree/permissions.py index 47af7dabcc..060a698c83 100644 --- a/src/backend/InvenTree/InvenTree/permissions.py +++ b/src/backend/InvenTree/InvenTree/permissions.py @@ -415,7 +415,6 @@ class GlobalSettingsPermissions(OASTokenMixin, permissions.BasePermission): """Check that the requesting user is 'admin'.""" try: user = request.user - if request.method in permissions.SAFE_METHODS: return True # Any other methods require staff access permissions diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index 5475edd03a..028e305926 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -612,6 +612,7 @@ class GeneralApiTests(InvenTreeAPITestCase): response = self.get( url, headers={'Authorization': f'Token {token}'}, max_query_count=20 ) + self.assertIsNotNone(data.get('active_plugins')) self.assertGreater(len(response.json()['database']), 4) data = response.json() diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 571f2b1e7c..8b46d7ca5c 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -3,7 +3,8 @@ from __future__ import annotations from django.contrib.auth.models import User -from django.db.models import F, Q +from django.db.models import F, OuterRef, Q, Subquery, Sum +from django.db.models.functions import Coalesce from django.urls import include, path from django.utils.translation import gettext_lazy as _ @@ -520,8 +521,22 @@ class BuildLineFilter(FilterSet): - The quantity available for each BuildLine (including variants and substitutes) - The quantity allocated for each BuildLine """ - flt = Q( - quantity__lte=F('allocated') + allocated_subquery = ( + BuildItem.objects + .filter(build_line=OuterRef('pk')) + .values('build_line') + .annotate(total=Sum('quantity')) + .values('total') + ) + + queryset = queryset.alias( + allocated_quantity=Coalesce(Subquery(allocated_subquery), 0) + ) + + # A query filter construct to determine the total quantity available for this BuildLine, + # taking into account any stock which is already allocated or consumed + available = ( + F('allocated_quantity') + F('consumed') + F('available_stock') + F('available_substitute_stock') @@ -529,8 +544,9 @@ class BuildLineFilter(FilterSet): ) if str2bool(value): - return queryset.filter(flt) - return queryset.exclude(flt) + return queryset.filter(quantity__lte=available) + + return queryset.filter(quantity__gt=available) on_order = rest_filters.BooleanFilter(label=_('On Order'), method='filter_on_order') diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py index 47e55895c3..6c3bf0916e 100644 --- a/src/backend/InvenTree/build/models.py +++ b/src/backend/InvenTree/build/models.py @@ -965,7 +965,7 @@ class Build( # Remove the build output from the database # This is a special case where serialized stock can be deleted, - # independedent of the global setting which normally prevents deletion of serialized stock items + # independent of the global setting which normally prevents deletion of serialized stock items output.delete(ignore_serial_check=True) @transaction.atomic diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 6050774e80..0833aeb45d 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -11,7 +11,7 @@ from build.models import Build, BuildItem, BuildLine from build.status_codes import BuildStatus from common.settings import set_global_setting from InvenTree.unit_test import InvenTreeAPITestCase -from part.models import BomItem, Part +from part.models import BomItem, BomItemSubstitute, Part from stock.models import StockItem, StockLocation, StockSortOrder from stock.status_codes import StockStatus @@ -1625,6 +1625,175 @@ class BuildLineTests(BuildAPITest): self.assertEqual(n_t + n_f, BuildLine.objects.count()) + def test_filter_available_allocated_consumed_mixed(self): + """Filter BuildLine objects with mixed allocated / consumed / stock states.""" + assembly = Part.objects.create( + name='Available Filter Assembly', + description='Assembly for advanced available filter tests', + assembly=True, + ) + + components = [ + Part.objects.create( + name=f'Available Filter Component {idx}', + description=f'Component {idx}', + component=True, + ) + for idx in range(4) + ] + + # The assembly uses 10x of each component + for component in components: + BomItem.objects.create(part=assembly, sub_part=component, quantity=10) + + # Create a new Build, requiring 100x of each component + build = Build.objects.create( + part=assembly, + reference='BO-9999', + quantity=10, + title='Available Filter Mixed', + ) + + lines = list(build.build_lines.order_by('pk')) + self.assertEqual(len(lines), 4) + + # Build line quantity is 100 for each line (10 * build quantity 10) + for line in lines: + self.assertEqual(line.quantity, 100) + + # Stock allocation baseline for each component + # Note: Quantity values will be updated later + stock_items = [ + StockItem.objects.create(part=component, quantity=1) + for component in components + ] + + # Line 0: AVAILABLE = True + # allocated = 30 + # consumed = 0 + # available = 70 + BuildItem.objects.create( + build_line=lines[0], stock_item=stock_items[0], quantity=30 + ) + + stock_items[0].quantity = 30 + 70 + stock_items[0].save() + + # Line 1: allocated 20 + consumed 30 + available stock 50 => available (100) + BuildItem.objects.create( + build_line=lines[1], stock_item=stock_items[1], quantity=20 + ) + lines[1].consumed = 30 + lines[1].save() + stock_items[1].quantity = 20 + 50 + stock_items[1].save() + + # Line 2: allocated 0 + consumed 10 + available stock 50 => not available (60) + lines[2].consumed = 10 + lines[2].save() + stock_items[2].quantity = 50 + stock_items[2].save() + + # Line 3: allocated 40 + consumed 0 + available stock 20 => not available (60) + BuildItem.objects.create( + build_line=lines[3], stock_item=stock_items[3], quantity=40 + ) + stock_items[3].quantity = 40 + 20 + stock_items[3].save() + + url = reverse('api-build-line-list') + + response_true = self.get(url, {'build': build.pk, 'available': True}) + + response_false = self.get(url, {'build': build.pk, 'available': False}) + + true_ids = {item['pk'] for item in response_true.data} + false_ids = {item['pk'] for item in response_false.data} + + self.assertSetEqual(true_ids, {lines[0].pk, lines[1].pk}) + self.assertSetEqual(false_ids, {lines[2].pk, lines[3].pk}) + self.assertSetEqual(true_ids | false_ids, {line.pk for line in lines}) + + def test_filter_available_substitute_and_variant_stock(self): + """Filter BuildLine objects where availability comes from substitute or variant stock.""" + assembly = Part.objects.create( + name='Available Filter Sub/Var Assembly', + description='Assembly for substitute and variant availability tests', + assembly=True, + ) + + # Substitute path: line should pass via substitute stock + sub_master_ok = Part.objects.create(name='Sub Master OK', component=True) + sub_alt_ok = Part.objects.create(name='Sub Alt OK', component=True) + + # Substitute path: line should fail (insufficient substitute stock) + sub_master_low = Part.objects.create(name='Sub Master Low', component=True) + sub_alt_low = Part.objects.create(name='Sub Alt Low', component=True) + + # Variant path: line should pass via variant stock + var_parent_ok = Part.objects.create( + name='Variant Parent OK', component=True, is_template=True + ) + var_child_ok = Part.objects.create( + name='Variant Child OK', component=True, variant_of=var_parent_ok + ) + + # Variant path: line should fail (insufficient variant stock) + var_parent_low = Part.objects.create( + name='Variant Parent Low', component=True, is_template=True + ) + var_child_low = Part.objects.create( + name='Variant Child Low', component=True, variant_of=var_parent_low + ) + + bom_sub_ok = BomItem.objects.create( + part=assembly, sub_part=sub_master_ok, quantity=10, allow_variants=False + ) + bom_sub_low = BomItem.objects.create( + part=assembly, sub_part=sub_master_low, quantity=10, allow_variants=False + ) + bom_var_ok = BomItem.objects.create( + part=assembly, sub_part=var_parent_ok, quantity=10, allow_variants=True + ) + bom_var_low = BomItem.objects.create( + part=assembly, sub_part=var_parent_low, quantity=10, allow_variants=True + ) + + BomItemSubstitute.objects.create(bom_item=bom_sub_ok, part=sub_alt_ok) + BomItemSubstitute.objects.create(bom_item=bom_sub_low, part=sub_alt_low) + + # Build quantity 10 => each line requires 100 units + build = Build.objects.create( + part=assembly, reference='BO-0987', quantity=10, title='Available Sub/Var' + ) + + lines = list(build.build_lines.order_by('pk')) + self.assertEqual(len(lines), 4) + + # Keep master parts at zero stock so only substitute/variant paths contribute + StockItem.objects.create(part=sub_alt_ok, quantity=100) + StockItem.objects.create(part=sub_alt_low, quantity=40) + StockItem.objects.create(part=var_child_ok, quantity=100) + StockItem.objects.create(part=var_child_low, quantity=40) + + url = reverse('api-build-line-list') + + response_true = self.get(url, {'build': build.pk, 'available': True}) + + response_false = self.get(url, {'build': build.pk, 'available': False}) + + pk_by_bom = {line.bom_item_id: line.pk for line in lines} + + expected_true = {pk_by_bom[bom_sub_ok.pk], pk_by_bom[bom_var_ok.pk]} + expected_false = {pk_by_bom[bom_sub_low.pk], pk_by_bom[bom_var_low.pk]} + + true_ids = {item['pk'] for item in response_true.data} + false_ids = {item['pk'] for item in response_false.data} + + self.assertSetEqual(true_ids, expected_true) + self.assertSetEqual(false_ids, expected_false) + self.assertSetEqual(true_ids | false_ids, {line.pk for line in lines}) + def test_output_options(self): """Test output options for the BuildLine endpoint.""" self.run_output_test( diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index c3b6d24752..1d1fd92113 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -1481,6 +1481,7 @@ class ObservabilityEndSerializer(serializers.Serializer): class ObservabilityEnd(CreateAPI): """Endpoint for observability tools.""" + # Note: This endpoint can be called anonymously, as it needs to function before the user is authenticated (e.g. during login) permission_classes = [AllowAnyOrReadScope] serializer_class = ObservabilityEndSerializer diff --git a/src/backend/InvenTree/importer/api.py b/src/backend/InvenTree/importer/api.py index c64f71c654..fd8c6024fd 100644 --- a/src/backend/InvenTree/importer/api.py +++ b/src/backend/InvenTree/importer/api.py @@ -115,6 +115,10 @@ class DataImportSessionAcceptFields(APIView): """Accept the field mapping for a DataImportSession.""" session = get_object_or_404(importer.models.DataImportSession, pk=pk) + # Check session ownership + if not request.user.is_staff and session.user != request.user: + raise PermissionDenied() + # Check that the user has permission to accept the field mapping if model_class := session.model_class: if not check_user_permission(request.user, model_class, 'change'): @@ -137,17 +141,45 @@ class DataImportSessionAcceptRows(DataImporterPermissionMixin, CreateAPI): ctx = super().get_serializer_context() try: - ctx['session'] = importer.models.DataImportSession.objects.get( + session = importer.models.DataImportSession.objects.get( pk=self.kwargs.get('pk', None) ) - except Exception: - pass + except importer.models.DataImportSession.DoesNotExist: + session = None + + if session: + user = self.request.user + if not user.is_staff and session.user != user: + raise PermissionDenied() + ctx['session'] = session ctx['request'] = self.request return ctx -class DataImportColumnMappingList(DataImporterPermissionMixin, ListAPI): +class DataImportSessionChildMixin(DataImporterPermissionMixin): + """Mixin for DataImportRow and DataImportColumnMap views. + + Ensures users can only access objects that belong to an import session they own. + Staff users retain access to all objects. + """ + + def get_queryset(self): + """Return only objects whose session belongs to the requesting user.""" + queryset = super().get_queryset() + + try: + user = self.request.user + except AttributeError: + raise PermissionDenied('User information is not available') + + if user.is_staff: + return queryset + + return queryset.filter(session__user=user) + + +class DataImportColumnMappingList(DataImportSessionChildMixin, ListAPI): """API endpoint for accessing a list of DataImportColumnMap objects.""" queryset = importer.models.DataImportColumnMap.objects.all() @@ -158,14 +190,14 @@ class DataImportColumnMappingList(DataImporterPermissionMixin, ListAPI): filterset_fields = ['session'] -class DataImportColumnMappingDetail(DataImporterPermissionMixin, RetrieveUpdateAPI): +class DataImportColumnMappingDetail(DataImportSessionChildMixin, RetrieveUpdateAPI): """Detail endpoint for a single DataImportColumnMap object.""" queryset = importer.models.DataImportColumnMap.objects.all() serializer_class = importer.serializers.DataImportColumnMapSerializer -class DataImportRowList(DataImporterPermissionMixin, BulkDeleteMixin, ListAPI): +class DataImportRowList(DataImportSessionChildMixin, BulkDeleteMixin, ListAPI): """API endpoint for accessing a list of DataImportRow objects.""" queryset = importer.models.DataImportRow.objects.all() @@ -180,7 +212,7 @@ class DataImportRowList(DataImporterPermissionMixin, BulkDeleteMixin, ListAPI): ordering = 'row_index' -class DataImportRowDetail(DataImporterPermissionMixin, RetrieveUpdateDestroyAPI): +class DataImportRowDetail(DataImportSessionChildMixin, RetrieveUpdateDestroyAPI): """Detail endpoint for a single DataImportRow object.""" queryset = importer.models.DataImportRow.objects.all() diff --git a/src/backend/InvenTree/importer/migrations/0006_dataimportcolumnmap_lookup_field.py b/src/backend/InvenTree/importer/migrations/0006_dataimportcolumnmap_lookup_field.py new file mode 100644 index 0000000000..a6bda2f824 --- /dev/null +++ b/src/backend/InvenTree/importer/migrations/0006_dataimportcolumnmap_lookup_field.py @@ -0,0 +1,24 @@ +# Generated migration + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("importer", "0005_dataimportsession_update_records"), + ] + + operations = [ + migrations.AddField( + model_name="dataimportcolumnmap", + name="lookup_field", + field=models.CharField( + blank=True, + null=True, + max_length=100, + verbose_name="Lookup Field", + help_text="Database field to use for foreign-key lookup. Leave blank for automatic lookup.", + ), + ), + ] diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py index 8dae1376ab..042e8df43b 100644 --- a/src/backend/InvenTree/importer/models.py +++ b/src/backend/InvenTree/importer/models.py @@ -152,6 +152,36 @@ class DataImportSession(models.Model): return supported_models().get(self.model_type, None) + def get_lookup_fields_for_field(self, field_name: str) -> list: + """Return the valid lookup fields for a given related (FK) field. + + Returns a list of field names that can be used as a lookup key, + consisting of 'pk' plus any fields defined in IMPORT_ID_FIELDS on the related model. + """ + model = self.get_related_model(field_name) + + if not model: + return ['pk'] + + id_fields = ['pk'] + + if custom_fields := getattr(model, 'IMPORT_ID_FIELDS', None): + id_fields += custom_fields + + return id_fields + + @property + def field_lookup_mapping(self) -> dict: + """Return a dict of field -> lookup_field mappings for this import session. + + Only entries where lookup_field is explicitly set are included. + """ + return { + mapping.field: mapping.lookup_field + for mapping in self.column_mappings.all() + if mapping.lookup_field + } + def get_related_model(self, field_name: str) -> Optional[models.Model]: """Return the related model for a given field name. @@ -344,14 +374,21 @@ class DataImportSession(models.Model): self.save() def check_complete(self) -> bool: - """Check if the import session is complete.""" + """Check if the import session is complete. + + When all rows have been accepted, the rows and column mappings are + deleted as they are no longer needed. The session itself is retained + as an audit record. + """ if self.completed_row_count < self.row_count: return False - # Update the status of this session if self.status != DataImportStatusCode.COMPLETE.value: self.status = DataImportStatusCode.COMPLETE.value self.save() + # Clear staging data now that all rows have been imported + self.rows.all().delete() + self.column_mappings.all().delete() return True @@ -401,6 +438,11 @@ class DataImportSession(models.Model): if field.get('read_only', False): continue + if field.get('type') == 'related field': + field['lookup_fields'] = self.get_lookup_fields_for_field( + field_name + ) + fields[field_name] = field # Cache the available fields against this instance @@ -488,6 +530,22 @@ class DataImportColumnMap(models.Model): if field_def.get('read_only', False): raise DjangoValidationError({'field': _('Selected field is read-only')}) + if self.lookup_field: + if field_def.get('type') != 'related field': + raise DjangoValidationError({ + 'lookup_field': _( + 'Lookup field can only be set for related (foreign-key) fields' + ) + }) + + valid_lookup_fields = self.session.get_lookup_fields_for_field(self.field) + if self.lookup_field not in valid_lookup_fields: + raise DjangoValidationError({ + 'lookup_field': _( + 'Invalid lookup field. Valid options are: {options}' + ).format(options=', '.join(valid_lookup_fields)) + }) + session = models.ForeignKey( DataImportSession, on_delete=models.CASCADE, @@ -499,6 +557,16 @@ class DataImportColumnMap(models.Model): column = models.CharField(blank=True, max_length=100, verbose_name=_('Column')) + lookup_field = models.CharField( + blank=True, + null=True, + max_length=100, + verbose_name=_('Lookup Field'), + help_text=_( + 'Database field to use for foreign-key lookup. Leave blank for automatic lookup.' + ), + ) + @property def available_fields(self): """Return a list of available fields for this import session. @@ -631,9 +699,12 @@ class DataImportRow(models.Model): default_values = self.default_values data = {} + extract_errors = {} self.related_field_map = {} + field_lookup_mapping = self.session.field_lookup_mapping + # We have mapped column (file) to field (serializer) already for field, col in field_mapping.items(): # Data override (force value and skip any further checks) @@ -661,7 +732,13 @@ class DataImportRow(models.Model): elif field_type == 'date': value = self.convert_date_field(value) elif field_type == 'related field': - value = self.lookup_related_field(field, value) + try: + value = self.lookup_related_field( + field, value, lookup_field=field_lookup_mapping.get(field) + ) + except DjangoValidationError as exc: + extract_errors[field] = exc.message + continue # Use the default value, if provided if value is None and field in default_values: @@ -703,6 +780,9 @@ class DataImportRow(models.Model): self.data = data + if extract_errors: + self.errors = extract_errors + if commit: self.save() @@ -726,7 +806,9 @@ class DataImportRow(models.Model): # If none of the formats matched, return the original value return value - def lookup_related_field(self, field_name: str, value: str) -> Optional[int]: + def lookup_related_field( + self, field_name: str, value: str, lookup_field: Optional[str] = None + ) -> Optional[int]: """Try to perform lookup against a related field. - This is used to convert a human-readable value (e.g. a supplier name) into a database reference (e.g. supplier ID). @@ -735,6 +817,7 @@ class DataImportRow(models.Model): Arguments: field_name: The name of the field to perform the lookup against value: The value to be looked up + lookup_field: If provided, only query this specific model field (skips auto-lookup) Returns: A primary key value @@ -758,21 +841,35 @@ class DataImportRow(models.Model): 'session': f'No related model found for field: {field_name}' }) - valid_items = set() - base_filters = ( self.session.field_filters.get(field_name, {}) if self.session.field_filters else {} ) - # First priority is the PK (primary key) field + if lookup_field and type(lookup_field) is str: + # A specific lookup field has been chosen by the user — query only that field + try: + queryset = model.objects.filter(**{lookup_field: value}, **base_filters) + except ValueError: + return value + + results = list(queryset[:2]) + + if len(results) == 1: + return results[0].pk + + # Zero or multiple results — return raw value and let serializer report the error + return value + + # Auto-lookup: try pk first, then any model-defined IMPORT_ID_FIELDS id_fields = ['pk'] if custom_id_fields := getattr(model, 'IMPORT_ID_FIELDS', None): id_fields += custom_id_fields - # Iterate through the provided list - if any of the values match, we can perform the lookup + valid_items = set() + for id_field in id_fields: try: queryset = model.objects.filter(**{id_field: value}, **base_filters) @@ -782,15 +879,19 @@ class DataImportRow(models.Model): # Evaluate at most two results to determine if there is exactly one match results = list(queryset[:2]) if len(results) == 1: - # We have a single match against this field valid_items.add(results[0].pk) if len(valid_items) == 1: - # We found a single valid match against the related model - return this value return valid_items.pop() - # We found either zero or multiple values matching against the related model - # Return the original value and let the serializer validation handle any errors against this field + if len(valid_items) > 1: + raise DjangoValidationError( + _( + 'Multiple matches found for value - please ensure the value is unique, or select a specific lookup field' + ) + ) + + # No match found - return the original value and let the serializer validation handle it return value def serializer_data(self): @@ -837,6 +938,10 @@ class DataImportRow(models.Model): # Row has already been completed return True + if self.errors: + # Errors were set during data extraction (e.g. ambiguous FK lookup) + return False + if self.session.update_records: # Extract the ID field from the data instance_id = self.data.get(self.session.ID_FIELD_LABEL, None) diff --git a/src/backend/InvenTree/importer/serializers.py b/src/backend/InvenTree/importer/serializers.py index 2e428b7e9b..2da60e0e32 100644 --- a/src/backend/InvenTree/importer/serializers.py +++ b/src/backend/InvenTree/importer/serializers.py @@ -23,7 +23,15 @@ class DataImportColumnMapSerializer(InvenTreeModelSerializer): """Meta class options for the serializer.""" model = importer.models.DataImportColumnMap - fields = ['pk', 'session', 'column', 'field', 'label', 'description'] + fields = [ + 'pk', + 'session', + 'column', + 'field', + 'label', + 'description', + 'lookup_field', + ] read_only_fields = ['field', 'session'] label = serializers.CharField(read_only=True) diff --git a/src/backend/InvenTree/importer/tests.py b/src/backend/InvenTree/importer/tests.py index babc9d4f29..6b3693f349 100644 --- a/src/backend/InvenTree/importer/tests.py +++ b/src/backend/InvenTree/importer/tests.py @@ -2,10 +2,11 @@ import os +from django.contrib.auth.models import User from django.core.files.base import ContentFile from django.urls import reverse -from importer.models import DataImportRow, DataImportSession +from importer.models import DataImportColumnMap, DataImportRow, DataImportSession from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase, InvenTreeTestCase @@ -58,14 +59,20 @@ class ImporterTest(ImporterMixin, InvenTreeTestCase): self.assertEqual(session.rows.count(), 12) # Check that some data has been imported - for row in session.rows.all(): + rows = list(session.rows.all()) + self.assertEqual(len(rows), 12) + + for row in rows: self.assertIsNotNone(row.data.get('name', None)) self.assertTrue(row.valid) row.validate(commit=True) self.assertTrue(row.complete) - self.assertEqual(session.completed_row_count, 12) + # All rows accepted: rows and mappings are cleared, session is retained + session.refresh_from_db() + self.assertEqual(session.rows.count(), 0) + self.assertEqual(session.column_mappings.count(), 0) # Check that the new companies have been created self.assertEqual(n + 12, Company.objects.count()) @@ -73,6 +80,72 @@ class ImporterTest(ImporterMixin, InvenTreeTestCase): def test_field_defaults(self): """Test default field values.""" + def test_lookup_field_ambiguous_match(self): + """Test the behavior of lookup_related_field for ambiguous and pinned matches.""" + from django.core.exceptions import ValidationError as DjangoValidationError + + from part.models import Part, PartCategory + + category = PartCategory.objects.create( + name='Test Category', description='Test category' + ) + + # Two parts which collide under different IMPORT_ID_FIELDS ('IPN' and 'name') + part_a = Part.objects.create( + category=category, name='Widget', description='desc', IPN='AMBIG-001' + ) + Part.objects.create( + category=category, name='AMBIG-001', description='desc', IPN='WIDGET-002' + ) + + data_file = self.helper_file('companies.csv') + session = DataImportSession.objects.create( + data_file=data_file, model_type='stockitem' + ) + + row = DataImportRow(session=session) + row.related_field_map = {} + + # Auto-lookup (no pinned lookup field) raises, as the value matches two different parts + with self.assertRaises(DjangoValidationError): + row.lookup_related_field('part', 'AMBIG-001') + + # Pinning the lookup field to 'IPN' resolves the match unambiguously + result = row.lookup_related_field('part', 'AMBIG-001', lookup_field='IPN') + self.assertEqual(result, part_a.pk) + + # Pinning the lookup field to 'name' resolves to the *other* part + result = row.lookup_related_field('part', 'AMBIG-001', lookup_field='name') + self.assertNotEqual(result, part_a.pk) + + def test_lookup_field_validation(self): + """Test that DataImportColumnMap.clean() validates the lookup_field value.""" + from django.core.exceptions import ValidationError as DjangoValidationError + + data_file = self.helper_file('companies.csv') + session = DataImportSession.objects.create( + data_file=data_file, model_type='stockitem' + ) + + part_mapping = session.column_mappings.get(field='part') + quantity_mapping = session.column_mappings.get(field='quantity') + + # Valid lookup field for a related (FK) field + part_mapping.lookup_field = 'IPN' + part_mapping.save() + part_mapping.refresh_from_db() + self.assertEqual(part_mapping.lookup_field, 'IPN') + + # Invalid lookup field (not a valid IMPORT_ID_FIELDS option) + part_mapping.lookup_field = 'not_a_real_field' + with self.assertRaises(DjangoValidationError): + part_mapping.save() + + # lookup_field cannot be set against a non-related field + quantity_mapping.lookup_field = 'IPN' + with self.assertRaises(DjangoValidationError): + quantity_mapping.save() + class ImportAPITest(ImporterMixin, InvenTreeAPITestCase): """End-to-end tests for the importer API.""" @@ -174,6 +247,58 @@ class ImportAPITest(ImporterMixin, InvenTreeAPITestCase): # Check that there are new database records self.assertEqual(PartCategory.objects.count(), N + 4) + def test_column_mapping_lookup_field(self): + """Test that the 'lookup_field' option can be specified via the column-mapping API.""" + f = self.helper_file('companies.csv') + + session = DataImportSession.objects.create( + data_file=f, model_type='stockitem', user=self.user + ) + + self.assignRole('stock.change') + + # available_fields should expose the valid lookup field options for the 'part' FK field + session_detail = self.get( + reverse('api-import-session-detail', kwargs={'pk': session.pk}) + ).data + part_field_info = session_detail['available_fields']['part'] + self.assertIn('lookup_fields', part_field_info) + self.assertIn('IPN', part_field_info['lookup_fields']) + self.assertIn('name', part_field_info['lookup_fields']) + self.assertIn('pk', part_field_info['lookup_fields']) + + part_mapping = session.column_mappings.get(field='part') + quantity_mapping = session.column_mappings.get(field='quantity') + + mapping_url = reverse( + 'api-importer-mapping-detail', kwargs={'pk': part_mapping.pk} + ) + + # Initially, no lookup field is set (defaults to 'auto') + data = self.get(mapping_url).data + self.assertIn(data['lookup_field'], [None, '']) + + # Set a valid lookup field + data = self.patch(mapping_url, {'lookup_field': 'IPN'}, expected_code=200).data + self.assertEqual(data['lookup_field'], 'IPN') + + # Confirm it persists + data = self.get(mapping_url).data + self.assertEqual(data['lookup_field'], 'IPN') + + # An invalid lookup field is rejected + self.patch(mapping_url, {'lookup_field': 'not_a_real_field'}, expected_code=400) + + # Clear the lookup field (revert to 'auto') + data = self.patch(mapping_url, {'lookup_field': None}, expected_code=200).data + self.assertIn(data['lookup_field'], [None, '']) + + # lookup_field cannot be set against a non-related field + quantity_url = reverse( + 'api-importer-mapping-detail', kwargs={'pk': quantity_mapping.pk} + ) + self.patch(quantity_url, {'lookup_field': 'IPN'}, expected_code=400) + def test_session_list(self): """Test API endpoint which details the list of import sessions.""" url = reverse('api-importer-session-list') @@ -204,6 +329,191 @@ class ImportAPITest(ImporterMixin, InvenTreeAPITestCase): for session in response.data: self.assertEqual(session['user'], self.user.pk) + def test_accept_fields_ownership(self): + """Test that accept_fields rejects requests for sessions owned by another user.""" + other_user = User.objects.create_user( + username='other_accept', password='password' + ) + + f = self.helper_file('companies.csv') + session = DataImportSession.objects.create( + data_file=f, model_type='company', user=other_user + ) + + url = reverse('api-import-session-accept-fields', kwargs={'pk': session.pk}) + + # Non-owner, non-staff should be denied + self.user.is_staff = False + self.user.save() + self.post(url, expected_code=403) + + # Staff should be allowed (subject to model permission) + # Company is part of the purchase_order ruleset + self.user.is_staff = True + self.user.save() + self.assignRole('purchase_order.change') + self.post(url, expected_code=200) + + def test_accept_rows_ownership(self): + """Test that accept_rows rejects requests for sessions owned by another user.""" + other_user = User.objects.create_user( + username='other_accept_rows', password='password' + ) + + f = self.helper_file('companies.csv') + session = DataImportSession.objects.create( + data_file=f, model_type='company', user=other_user + ) + session.extract_columns() + + url = reverse('api-import-session-accept-rows', kwargs={'pk': session.pk}) + + self.user.is_staff = False + self.user.save() + self.post(url, {'rows': []}, expected_code=403) + + # Staff can reach the endpoint (rows list is empty so validation rejects with 400, not 403) + self.user.is_staff = True + self.user.save() + self.post(url, {'rows': []}, expected_code=400) + + def test_session_cleanup_on_complete(self): + """Test that a completed import session deletes itself and all associated data.""" + url = reverse('api-importer-session-list') + data_file = self.helper_file('part_categories.csv') + + data = self.post( + url, + {'model_type': 'partcategory', 'data_file': data_file}, + format='multipart', + ).data + + session_id = data['pk'] + session_pk = session_id + + self.assignRole('part_category.add') + self.post( + reverse('api-import-session-accept-fields', kwargs={'pk': session_id}), + expected_code=200, + ) + + rows = self.get( + reverse('api-importer-row-list'), data={'session': session_id} + ).data + row_ids = [r['pk'] for r in rows] + self.assertGreater(len(row_ids), 0) + + # Confirm rows and mappings exist before acceptance + self.assertTrue(DataImportRow.objects.filter(session_id=session_pk).exists()) + self.assertTrue( + DataImportColumnMap.objects.filter(session_id=session_pk).exists() + ) + + # Accept all rows — this should trigger cleanup of rows and mappings + self.post( + reverse('api-import-session-accept-rows', kwargs={'pk': session_id}), + {'rows': row_ids}, + ) + + # Rows and column mappings must be cleared + self.assertFalse(DataImportRow.objects.filter(session_id=session_pk).exists()) + self.assertFalse( + DataImportColumnMap.objects.filter(session_id=session_pk).exists() + ) + + # Session itself is retained as an audit record with COMPLETE status + from importer.models import DataImportSession + from importer.status_codes import DataImportStatusCode + + session_obj = DataImportSession.objects.get(pk=session_pk) + self.assertEqual(session_obj.status, DataImportStatusCode.COMPLETE.value) + + detail = self.get( + reverse('api-import-session-detail', kwargs={'pk': session_id}), + expected_code=200, + ).data + self.assertEqual(detail['row_count'], 0) + self.assertEqual(detail['completed_row_count'], 0) + + def test_row_and_mapping_ownership(self): + """Test that DataImportRow and DataImportColumnMap endpoints filter by session ownership.""" + f = self.helper_file('companies.csv') + + other_user = User.objects.create_user( + username='other_importer', password='password' + ) + + # Session owned by self.user + session_mine = DataImportSession.objects.create( + data_file=f, model_type='company', user=self.user + ) + session_mine.extract_columns() + + # Session owned by another user + f2 = self.helper_file('companies.csv') + session_other = DataImportSession.objects.create( + data_file=f2, model_type='company', user=other_user + ) + session_other.extract_columns() + + row_list_url = reverse('api-importer-row-list') + mapping_list_url = reverse('api-importer-mapping-list') + + # Non-staff: should only see rows/mappings from own session + self.user.is_staff = False + self.user.save() + + rows = self.get(row_list_url).data + for row in rows: + self.assertEqual(row['session'], session_mine.pk) + + mappings = self.get(mapping_list_url).data + for mapping in mappings: + self.assertEqual(mapping['session'], session_mine.pk) + + # Detail endpoint: own session's row/mapping should be accessible + own_row = DataImportRow.objects.filter(session=session_mine).first() + other_row = DataImportRow.objects.filter(session=session_other).first() + + if own_row: + self.get( + reverse('api-importer-row-detail', kwargs={'pk': own_row.pk}), + expected_code=200, + ) + if other_row: + self.get( + reverse('api-importer-row-detail', kwargs={'pk': other_row.pk}), + expected_code=404, + ) + + own_mapping = DataImportColumnMap.objects.filter(session=session_mine).first() + other_mapping = DataImportColumnMap.objects.filter( + session=session_other + ).first() + + if own_mapping: + self.get( + reverse('api-importer-mapping-detail', kwargs={'pk': own_mapping.pk}), + expected_code=200, + ) + if other_mapping: + self.get( + reverse('api-importer-mapping-detail', kwargs={'pk': other_mapping.pk}), + expected_code=404, + ) + + # Staff user: should see rows/mappings from all sessions + self.user.is_staff = True + self.user.save() + + all_row_pks = set(DataImportRow.objects.values_list('pk', flat=True)) + response_rows = self.get(row_list_url).data + self.assertEqual({r['pk'] for r in response_rows}, all_row_pks) + + all_mapping_pks = set(DataImportColumnMap.objects.values_list('pk', flat=True)) + response_mappings = self.get(mapping_list_url).data + self.assertEqual({m['pk'] for m in response_mappings}, all_mapping_pks) + class AdminTest(ImporterMixin, AdminTestCase): """Tests for the admin interface integration.""" diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py index 400f0004df..c8967f5da1 100644 --- a/src/backend/InvenTree/plugin/api.py +++ b/src/backend/InvenTree/plugin/api.py @@ -10,7 +10,7 @@ import django_filters.rest_framework.filters as rest_filters from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.utils import extend_schema -from rest_framework import status +from rest_framework import permissions, status from rest_framework.exceptions import NotFound from rest_framework.response import Response from rest_framework.views import APIView @@ -174,7 +174,10 @@ class PluginDetail(RetrieveDestroyAPI): queryset = PluginConfig.objects.all() serializer_class = PluginSerializers.PluginConfigSerializer - permission_classes = [InvenTree.permissions.IsSuperuserOrReadOnlyOrScope] + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.IsSuperuserOrReadOnlyOrScope, + ] lookup_field = 'key' lookup_url_kwarg = 'plugin' @@ -201,6 +204,7 @@ class PluginAdminDetail(RetrieveAPI): queryset = PluginConfig.objects.all() serializer_class = PluginSerializers.PluginAdminDetailSerializer + permission_classes = [InvenTree.permissions.IsAdminOrAdminScope] lookup_field = 'key' lookup_url_kwarg = 'plugin' @@ -292,7 +296,10 @@ class PluginSettingList(ListAPI): queryset = PluginSetting.objects.all() serializer_class = PluginSerializers.PluginSettingSerializer - permission_classes = [InvenTree.permissions.GlobalSettingsPermissions] + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.GlobalSettingsPermissions, + ] filter_backends = [DjangoFilterBackend] @@ -365,7 +372,10 @@ class PluginAllSettingList(APIView): - GET: return all settings for a plugin config """ - permission_classes = [InvenTree.permissions.GlobalSettingsPermissions] + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.GlobalSettingsPermissions, + ] @extend_schema( responses={200: PluginSerializers.PluginSettingSerializer(many=True)} @@ -393,6 +403,11 @@ class PluginSettingDetail(RetrieveUpdateAPI): queryset = PluginSetting.objects.all() serializer_class = PluginSerializers.PluginSettingSerializer + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.GlobalSettingsPermissions, + ] + def get_object(self): """Lookup the plugin setting object, based on the URL. @@ -415,9 +430,6 @@ class PluginSettingDetail(RetrieveUpdateAPI): setting_key, plugin=plugin.plugin_config() ) - # Staff permission required - permission_classes = [InvenTree.permissions.GlobalSettingsPermissions] - class PluginUserSettingList(APIView): """List endpoint for all user settings for a specific plugin. diff --git a/src/backend/InvenTree/plugin/builtin/exporter/stocktake_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/stocktake_exporter.py index f2ffe8c66a..6503d8f571 100644 --- a/src/backend/InvenTree/plugin/builtin/exporter/stocktake_exporter.py +++ b/src/backend/InvenTree/plugin/builtin/exporter/stocktake_exporter.py @@ -29,7 +29,13 @@ class PartStocktakeExportOptionsSerializer(serializers.Serializer): export_include_variant_items = serializers.BooleanField( default=False, label=_('Include Variant Items'), - help_text=_('Include part variant stock in pricing calculations'), + help_text=_('Include part variant stock in stocktake data'), + ) + + export_exclude_zero_stock_entries = serializers.BooleanField( + default=False, + label=_('Exclude Zero Stock Entries'), + help_text=_('Exclude parts with zero stock from the exported dataset'), ) @@ -43,7 +49,7 @@ class PartStocktakeExporter(DataExportMixin, InvenTreePlugin): SLUG = 'inventree-stocktake-exporter' TITLE = _('Part Stocktake Exporter') DESCRIPTION = _('Exporter for part stocktake data') - VERSION = '1.0.0' + VERSION = '1.1.0' AUTHOR = _('InvenTree contributors') ExportOptionsSerializer = PartStocktakeExportOptionsSerializer @@ -78,6 +84,9 @@ class PartStocktakeExporter(DataExportMixin, InvenTreePlugin): 'pk', 'name', 'IPN', + 'active', + 'component', + 'assembly', 'description', 'category', 'allocated_to_build_orders', @@ -124,26 +133,36 @@ class PartStocktakeExporter(DataExportMixin, InvenTreePlugin): export_pricing_data = context.get('export_pricing_data', True) include_external_items = context.get('export_include_external_items', False) include_variant_items = context.get('export_include_variant_items', False) + exclude_zero_stock = context.get('export_exclude_zero_stock_entries', False) data = super().export_data( queryset, serializer_class, headers, context, output, **kwargs ) - if export_pricing_data: - for row in data: - quantity = Decimal(row.get('total_in_stock', 0)) + output_data = [] - if not include_external_items: - quantity -= Decimal(row.get('external_stock', 0)) + for row in data: + quantity = Decimal(row.get('total_in_stock', 0)) - if not include_variant_items: - quantity -= Decimal(row.get('variant_stock', 0)) + if not include_external_items: + quantity -= Decimal(row.get('external_stock', 0)) - if quantity < 0: - quantity = Decimal(0) + if not include_variant_items: + quantity -= Decimal(row.get('variant_stock', 0)) - pricing_min = row.get('pricing_min', None) - pricing_max = row.get('pricing_max', None) + if quantity < 0: + quantity = Decimal(0) + + if exclude_zero_stock: + continue + + if export_pricing_data: + pricing_min = row.get('pricing_min', None) or row.get( + 'pricing_max', None + ) + pricing_max = row.get('pricing_max', None) or row.get( + 'pricing_min', None + ) if pricing_min is not None: pricing_min = Decimal(pricing_min) @@ -157,4 +176,6 @@ class PartStocktakeExporter(DataExportMixin, InvenTreePlugin): pricing_max * quantity, rounding=10 ) - return data + output_data.append(row) + + return output_data diff --git a/src/backend/InvenTree/plugin/builtin/exporter/test_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/test_exporter.py index a22ecf2a63..cf1e9c9af8 100644 --- a/src/backend/InvenTree/plugin/builtin/exporter/test_exporter.py +++ b/src/backend/InvenTree/plugin/builtin/exporter/test_exporter.py @@ -37,7 +37,7 @@ class StocktakeExporterTest(InvenTreeAPITestCase): 'Minimum Unit Cost', 'Maximum Total Cost', ], - excluded_cols=['Active', 'External Stock', 'Variant Stock'], + excluded_cols=['External Stock', 'Variant Stock'], ) # Now, with additional parameters specific to the plugin @@ -60,7 +60,6 @@ class StocktakeExporterTest(InvenTreeAPITestCase): 'External Stock', 'Variant Stock', ], - excluded_cols=['Active'], ) # Finally, exclude pricing data entirely diff --git a/src/backend/InvenTree/plugin/test_api.py b/src/backend/InvenTree/plugin/test_api.py index 9e22f40355..8b1c543dd2 100644 --- a/src/backend/InvenTree/plugin/test_api.py +++ b/src/backend/InvenTree/plugin/test_api.py @@ -816,3 +816,48 @@ class PluginLockedSettingsTest(PluginMixin, InvenTreeAPITestCase): self.assertFalse( response.data['read_only'], msg=f'{key} should not be read_only' ) + + +class PluginUnauthenticatedAccessTest(PluginMixin, InvenTreeAPITestCase): + """Ensure plugin API endpoints reject unauthenticated requests. + + Tests the four endpoints hardened on the permissions-fix branch: + - PluginDetail (api-plugin-detail) + - PluginSettingList (api-plugin-setting-list) + - PluginAllSettingList (api-plugin-settings) + - PluginSettingDetail (api-plugin-setting-detail) + """ + + superuser = True + PLUGIN_SLUG = 'sample' + SETTING_KEY = 'API_KEY' + + def setUp(self): + """Activate sample plugin, then log out to simulate an anonymous client.""" + super().setUp() + from plugin.registry import registry + + registry.set_plugin_state(self.PLUGIN_SLUG, True) + self.client.logout() + + def test_plugin_detail_unauthenticated(self): + """GET /api/plugins// must return 401 for unauthenticated users.""" + url = reverse('api-plugin-detail', kwargs={'plugin': self.PLUGIN_SLUG}) + self.get(url, expected_code=401) + + def test_plugin_setting_list_unauthenticated(self): + """GET /api/plugins/settings/ must return 401 for unauthenticated users.""" + self.get(reverse('api-plugin-setting-list'), expected_code=401) + + def test_plugin_all_settings_unauthenticated(self): + """GET /api/plugins//settings/ must return 401 for unauthenticated users.""" + url = reverse('api-plugin-settings', kwargs={'plugin': self.PLUGIN_SLUG}) + self.get(url, expected_code=401) + + def test_plugin_setting_detail_unauthenticated(self): + """GET /api/plugins//settings// must return 401 for unauthenticated users.""" + url = reverse( + 'api-plugin-setting-detail', + kwargs={'plugin': self.PLUGIN_SLUG, 'key': self.SETTING_KEY}, + ) + self.get(url, expected_code=401) diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index ff512493dc..055cc4a088 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -449,56 +449,56 @@ charset-normalizer==3.4.7 \ # via # -c src/backend/requirements.txt # requests -cryptography==48.0.0 \ - --hash=sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13 \ - --hash=sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6 \ - --hash=sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8 \ - --hash=sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25 \ - --hash=sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c \ - --hash=sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832 \ - --hash=sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12 \ - --hash=sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c \ - --hash=sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7 \ - --hash=sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c \ - --hash=sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec \ - --hash=sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5 \ - --hash=sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355 \ - --hash=sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c \ - --hash=sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741 \ - --hash=sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86 \ - --hash=sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321 \ - --hash=sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a \ - --hash=sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7 \ - --hash=sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920 \ - --hash=sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e \ - --hash=sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff \ - --hash=sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd \ - --hash=sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3 \ - --hash=sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f \ - --hash=sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602 \ - --hash=sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855 \ - --hash=sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18 \ - --hash=sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a \ - --hash=sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336 \ - --hash=sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239 \ - --hash=sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74 \ - --hash=sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a \ - --hash=sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c \ - --hash=sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4 \ - --hash=sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c \ - --hash=sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f \ - --hash=sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4 \ - --hash=sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db \ - --hash=sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166 \ - --hash=sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5 \ - --hash=sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f \ - --hash=sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae \ - --hash=sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20 \ - --hash=sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a \ - --hash=sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057 \ - --hash=sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb \ - --hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \ - --hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b +cryptography==48.0.1 \ + --hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \ + --hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \ + --hash=sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471 \ + --hash=sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f \ + --hash=sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1 \ + --hash=sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225 \ + --hash=sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92 \ + --hash=sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6 \ + --hash=sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24 \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f \ + --hash=sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72 \ + --hash=sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6 \ + --hash=sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8 \ + --hash=sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a \ + --hash=sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429 \ + --hash=sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1 \ + --hash=sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd \ + --hash=sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9 \ + --hash=sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265 \ + --hash=sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a \ + --hash=sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475 \ + --hash=sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d \ + --hash=sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3 \ + --hash=sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c \ + --hash=sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1 \ + --hash=sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac \ + --hash=sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6 \ + --hash=sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2 \ + --hash=sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08 \ + --hash=sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c \ + --hash=sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b \ + --hash=sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401 \ + --hash=sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158 \ + --hash=sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8 \ + --hash=sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9 \ + --hash=sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411 \ + --hash=sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4 \ + --hash=sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991 \ + --hash=sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17 \ + --hash=sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242 \ + --hash=sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691 \ + --hash=sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 \ + --hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46 # 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 073cd8122f..1bd491ec1f 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -354,56 +354,56 @@ coverage[toml]==7.14.1 \ --hash=sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253 \ --hash=sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c # via -r src/backend/requirements-dev.in -cryptography==48.0.0 \ - --hash=sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13 \ - --hash=sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6 \ - --hash=sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8 \ - --hash=sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25 \ - --hash=sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c \ - --hash=sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832 \ - --hash=sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12 \ - --hash=sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c \ - --hash=sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7 \ - --hash=sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c \ - --hash=sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec \ - --hash=sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5 \ - --hash=sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355 \ - --hash=sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c \ - --hash=sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741 \ - --hash=sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86 \ - --hash=sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321 \ - --hash=sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a \ - --hash=sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7 \ - --hash=sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920 \ - --hash=sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e \ - --hash=sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff \ - --hash=sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd \ - --hash=sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3 \ - --hash=sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f \ - --hash=sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602 \ - --hash=sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855 \ - --hash=sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18 \ - --hash=sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a \ - --hash=sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336 \ - --hash=sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239 \ - --hash=sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74 \ - --hash=sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a \ - --hash=sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c \ - --hash=sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4 \ - --hash=sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c \ - --hash=sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f \ - --hash=sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4 \ - --hash=sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db \ - --hash=sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166 \ - --hash=sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5 \ - --hash=sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f \ - --hash=sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae \ - --hash=sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20 \ - --hash=sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a \ - --hash=sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057 \ - --hash=sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb \ - --hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \ - --hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b +cryptography==48.0.1 \ + --hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \ + --hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \ + --hash=sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471 \ + --hash=sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f \ + --hash=sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1 \ + --hash=sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225 \ + --hash=sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92 \ + --hash=sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6 \ + --hash=sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24 \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f \ + --hash=sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72 \ + --hash=sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6 \ + --hash=sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8 \ + --hash=sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a \ + --hash=sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429 \ + --hash=sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1 \ + --hash=sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd \ + --hash=sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9 \ + --hash=sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265 \ + --hash=sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a \ + --hash=sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475 \ + --hash=sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d \ + --hash=sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3 \ + --hash=sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c \ + --hash=sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1 \ + --hash=sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac \ + --hash=sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6 \ + --hash=sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2 \ + --hash=sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08 \ + --hash=sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c \ + --hash=sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b \ + --hash=sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401 \ + --hash=sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158 \ + --hash=sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8 \ + --hash=sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9 \ + --hash=sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411 \ + --hash=sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4 \ + --hash=sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991 \ + --hash=sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17 \ + --hash=sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242 \ + --hash=sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691 \ + --hash=sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 \ + --hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46 # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index a0be6c4215..1f01208c0b 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -350,56 +350,56 @@ coverage[toml]==7.13.5 \ --hash=sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0 \ --hash=sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f # via -r src/backend/requirements-dev.in -cryptography==48.0.0 \ - --hash=sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13 \ - --hash=sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6 \ - --hash=sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8 \ - --hash=sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25 \ - --hash=sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c \ - --hash=sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832 \ - --hash=sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12 \ - --hash=sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c \ - --hash=sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7 \ - --hash=sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c \ - --hash=sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec \ - --hash=sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5 \ - --hash=sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355 \ - --hash=sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c \ - --hash=sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741 \ - --hash=sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86 \ - --hash=sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321 \ - --hash=sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a \ - --hash=sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7 \ - --hash=sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920 \ - --hash=sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e \ - --hash=sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff \ - --hash=sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd \ - --hash=sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3 \ - --hash=sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f \ - --hash=sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602 \ - --hash=sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855 \ - --hash=sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18 \ - --hash=sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a \ - --hash=sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336 \ - --hash=sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239 \ - --hash=sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74 \ - --hash=sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a \ - --hash=sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c \ - --hash=sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4 \ - --hash=sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c \ - --hash=sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f \ - --hash=sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4 \ - --hash=sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db \ - --hash=sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166 \ - --hash=sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5 \ - --hash=sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f \ - --hash=sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae \ - --hash=sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20 \ - --hash=sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a \ - --hash=sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057 \ - --hash=sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb \ - --hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \ - --hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b +cryptography==48.0.1 \ + --hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \ + --hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \ + --hash=sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471 \ + --hash=sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f \ + --hash=sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1 \ + --hash=sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225 \ + --hash=sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92 \ + --hash=sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6 \ + --hash=sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24 \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f \ + --hash=sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72 \ + --hash=sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6 \ + --hash=sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8 \ + --hash=sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a \ + --hash=sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429 \ + --hash=sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1 \ + --hash=sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd \ + --hash=sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9 \ + --hash=sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265 \ + --hash=sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a \ + --hash=sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475 \ + --hash=sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d \ + --hash=sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3 \ + --hash=sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c \ + --hash=sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1 \ + --hash=sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac \ + --hash=sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6 \ + --hash=sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2 \ + --hash=sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08 \ + --hash=sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c \ + --hash=sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b \ + --hash=sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401 \ + --hash=sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158 \ + --hash=sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8 \ + --hash=sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9 \ + --hash=sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411 \ + --hash=sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4 \ + --hash=sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991 \ + --hash=sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17 \ + --hash=sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242 \ + --hash=sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691 \ + --hash=sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 \ + --hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46 # via # -c src/backend/requirements.txt # pdfminer-six @@ -476,9 +476,9 @@ pdfminer-six==20260107 \ --hash=sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9 \ --hash=sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602 # via -r src/backend/requirements-dev.in -pip==26.1.1 \ - --hash=sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb \ - --hash=sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78 +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via pip-tools pip-tools==7.5.3 \ --hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \ diff --git a/src/backend/requirements.in b/src/backend/requirements.in index 6fc4fc3e57..2c279968f9 100644 --- a/src/backend/requirements.in +++ b/src/backend/requirements.in @@ -1,7 +1,7 @@ # Please keep this list sorted - if you pin a version provide a reason django<6.0 # Django package blessed # CLI for Q Monitor -cryptography>=48.0.0 # Core cryptographic functionality +cryptography>=48.0.1 # Core cryptographic functionality django-anymail[amazon_ses,postal] # Email backend for various providers django-allauth[mfa,socialaccount,saml,openid,headless] # SSO for external providers via OpenID django-cleanup # Automated deletion of old / unused uploaded files diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 34f64e8def..d524f45453 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -435,56 +435,56 @@ charset-normalizer==3.4.7 \ --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 # via requests -cryptography==48.0.0 \ - --hash=sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13 \ - --hash=sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6 \ - --hash=sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8 \ - --hash=sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25 \ - --hash=sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c \ - --hash=sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832 \ - --hash=sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12 \ - --hash=sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c \ - --hash=sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7 \ - --hash=sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c \ - --hash=sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec \ - --hash=sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5 \ - --hash=sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355 \ - --hash=sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c \ - --hash=sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741 \ - --hash=sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86 \ - --hash=sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321 \ - --hash=sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a \ - --hash=sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7 \ - --hash=sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920 \ - --hash=sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e \ - --hash=sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff \ - --hash=sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd \ - --hash=sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3 \ - --hash=sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f \ - --hash=sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602 \ - --hash=sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855 \ - --hash=sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18 \ - --hash=sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a \ - --hash=sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336 \ - --hash=sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239 \ - --hash=sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74 \ - --hash=sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a \ - --hash=sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c \ - --hash=sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4 \ - --hash=sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c \ - --hash=sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f \ - --hash=sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4 \ - --hash=sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db \ - --hash=sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166 \ - --hash=sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5 \ - --hash=sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f \ - --hash=sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae \ - --hash=sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20 \ - --hash=sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a \ - --hash=sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057 \ - --hash=sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb \ - --hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \ - --hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b +cryptography==48.0.1 \ + --hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \ + --hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \ + --hash=sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471 \ + --hash=sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f \ + --hash=sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1 \ + --hash=sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225 \ + --hash=sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92 \ + --hash=sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6 \ + --hash=sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24 \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f \ + --hash=sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72 \ + --hash=sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6 \ + --hash=sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8 \ + --hash=sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a \ + --hash=sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429 \ + --hash=sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1 \ + --hash=sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd \ + --hash=sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9 \ + --hash=sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265 \ + --hash=sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a \ + --hash=sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475 \ + --hash=sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d \ + --hash=sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3 \ + --hash=sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c \ + --hash=sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1 \ + --hash=sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac \ + --hash=sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6 \ + --hash=sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2 \ + --hash=sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08 \ + --hash=sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c \ + --hash=sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b \ + --hash=sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401 \ + --hash=sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158 \ + --hash=sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8 \ + --hash=sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9 \ + --hash=sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411 \ + --hash=sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4 \ + --hash=sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991 \ + --hash=sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17 \ + --hash=sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242 \ + --hash=sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691 \ + --hash=sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 \ + --hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46 # via # -r src/backend/requirements.in # django-anymail diff --git a/src/frontend/package.json b/src/frontend/package.json index 11faa0b3e6..184727b270 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -49,16 +49,16 @@ "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", - "@emotion/react": "^11.13.3", "@floating-ui/dom": "^1.0.0", - "@fortawesome/fontawesome-svg-core": "^7.0.0", - "@fortawesome/free-regular-svg-icons": "^7.0.0", - "@fortawesome/free-solid-svg-icons": "^7.0.0", - "@fortawesome/react-fontawesome": "^3.0.1", - "@fullcalendar/core": "^6.1.15", - "@fullcalendar/daygrid": "^6.1.15", - "@fullcalendar/interaction": "^6.1.15", - "@fullcalendar/react": "^6.1.15", + "@emotion/react": "^11.14.0", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-regular-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.3.1", + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/react": "^6.1.20", "@github/webauthn-json": "^2.1.1", "@lingui/core": "^5.9.2", "@lingui/react": "^5.9.2", @@ -76,9 +76,6 @@ "@mantine/utils": "^6.0.22", "@mantine/vanilla-extract": "^9.2.1", "@messageformat/date-skeleton": "^1.1.0", - "@sentry/react": "^10.43.0", - "@tabler/icons-react": "^3.17.0", - "@tanstack/react-query": "^5.56.2", "@tiptap/core": "^3.23.6", "@tiptap/extension-image": "^3.23.6", "@tiptap/extension-link": "^3.23.6", @@ -86,68 +83,71 @@ "@tiptap/pm": "^3.23.6", "@tiptap/react": "^3.23.6", "@tiptap/starter-kit": "^3.23.6", + "@sentry/react": "^10.57.0", + "@tabler/icons-react": "^3.44.0", + "@tanstack/react-query": "^5.101.0", "@uiw/codemirror-theme-vscode": "^4.25.8", "@uiw/react-codemirror": "^4.25.8", "@uiw/react-split": "^5.9.4", - "@vanilla-extract/css": "^1.18.0", - "axios": "^1.13.6", + "@vanilla-extract/css": "^1.20.1", + "axios": "^1.17.0", "clsx": "^2.1.1", "codemirror": "^6.0.2", - "dayjs": "^1.11.13", - "dompurify": "^3.2.4", - "embla-carousel": "^8.5.2", - "embla-carousel-react": "^8.5.2", - "fuse.js": "^7.0.0", + "dayjs": "^1.11.21", + "dompurify": "^3.4.8", + "embla-carousel": "^8.6.0", + "embla-carousel-react": "^8.6.0", + "fuse.js": "^7.4.2", "html5-qrcode": "^2.3.8", "mantine-contextmenu": "^9.2.1", - "mantine-datatable": "^9.2.0", + "mantine-datatable": "^9.2.2", "qrcode": "^1.5.4", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-grid-layout": "1.4.4", - "react-hook-form": "^7.62.0", - "react-is": "^19.2.4", - "react-router-dom": "^6.26.2", - "react-select": "^5.9.0", - "react-window": "1.8.11", - "recharts": "^3.1.2", - "styled-components": "^6.1.14", "tiptap-extension-resizable-image": "^2.1.0", - "undici": "^6.24.0", - "zustand": "^5.0.8" + "react-hook-form": "^7.78.0", + "react-is": "^19.2.7", + "react-router-dom": "^6.30.4", + "react-select": "^5.10.2", + "react-window": "1.8.11", + "recharts": "^3.8.1", + "styled-components": "^6.4.2", + "undici": "^8.4.1", + "zustand": "^5.0.14" }, "devDependencies": { - "@babel/core": "^7.29.0", - "@babel/preset-react": "^7.28.5", - "@babel/preset-typescript": "^7.28.5", - "@babel/runtime": "^7.28.6", - "@codecov/vite-plugin": "^1.9.1", + "@babel/core": "^7.29.7", + "@babel/preset-react": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", + "@babel/runtime": "^7.29.7", + "@codecov/vite-plugin": "^2.0.1", "@flakiness/playwright": "^1.13.0", - "@lingui/babel-plugin-lingui-macro": "^5.9.2", - "@lingui/cli": "^5.9.2", - "@lingui/macro": "^5.9.2", - "@playwright/test": "^1.16.0", - "@types/node": "^25.5.0", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.2.14", + "@lingui/babel-plugin-lingui-macro": "^5.9.5", + "@lingui/cli": "^5.9.5", + "@lingui/macro": "^5.9.5", + "@playwright/test": "^1.60.0", + "@types/node": "^25.9.2", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/react-grid-layout": "^1.3.5", "@types/react-router-dom": "^5.3.3", "@types/react-window": "^1.8.8", - "@vanilla-extract/vite-plugin": "^5.1.4", + "@vanilla-extract/vite-plugin": "^5.2.2", "@vitejs/plugin-react": "^5.2.0", "babel-plugin-macros": "^3.1.0", "nyc": "^18.0.0", - "otpauth": "^9.4.1", + "otpauth": "^9.5.1", "path": "^0.12.7", - "rollup": "^4.59.0", - "rollup-plugin-license": "^3.7.0", + "rollup": "^4.61.1", + "rollup-plugin-license": "^3.7.1", "typescript": "^5.9.3", "vite": "^6.4.2", "vite-plugin-babel-macros": "^1.0.6", - "vite-plugin-dts": "^4.5.4", + "vite-plugin-dts": "^5.0.2", "vite-plugin-externals": "^0.6.2", - "vite-plugin-istanbul": "^8.0.0" + "vite-plugin-istanbul": "^9.0.1" }, "overrides": { }, diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index adffd6063e..85e128a1fd 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -68,7 +68,7 @@ export function ApiFormField({ : definition.value ); } - }, [definition.value]); + }, [definition.value, definition.field_type]); const fieldDefinition: ApiFormFieldType = useMemo(() => { return { diff --git a/src/frontend/src/components/importer/ImporterColumnSelector.tsx b/src/frontend/src/components/importer/ImporterColumnSelector.tsx index 83b420e812..f4a64bd272 100644 --- a/src/frontend/src/components/importer/ImporterColumnSelector.tsx +++ b/src/frontend/src/components/importer/ImporterColumnSelector.tsx @@ -179,6 +179,55 @@ function ImporterDefaultField({ ); } +function ImporterLookupFieldSelector({ + column, + session +}: Readonly<{ column: any; session: ImportSessionState }>) { + const api = useApi(); + + const fieldDef = session.availableFields[column.field]; + const lookupFields: string[] = fieldDef?.lookup_fields ?? []; + + const [selected, setSelected] = useState(column.lookup_field ?? ''); + + useEffect(() => { + setSelected(column.lookup_field ?? ''); + }, [column.lookup_field]); + + if (lookupFields.length === 0) { + return null; + } + + const options = [ + { value: '', label: t`Auto` }, + ...lookupFields.map((f: string) => ({ value: f, label: f })) + ]; + + const onChange = useCallback( + (value: string | null) => { + const next = value ?? ''; + api + .patch( + apiUrl(ApiEndpoints.import_session_column_mapping_list, column.pk), + { lookup_field: next || null } + ) + .then(() => setSelected(next)) + .catch(() => {}); + }, + [column.pk] + ); + + return ( +