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/CHANGELOG.md b/CHANGELOG.md index 3156935ee4..2abba4e75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#12165](https://github.com/inventree/InvenTree/pull/12165) adds support for parameters against the PartCategory model - [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns. - [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API) - [#12079](https://github.com/inventree/InvenTree/pull/12079) adds the ability to save filter groups for table and calendar views in the user interface. This allows users to save and reuse commonly used filter configurations, improving the usability and efficiency of the interface. 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 a137a1571a..8c33402798 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,20 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 504 +INVENTREE_API_VERSION = 507 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +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 + v504 -> 2026-06-13 : https://github.com/inventree/InvenTree/pull/12139 - Adjustments to the SelectionList and SelectionListEntry API endpoints to support more efficient queries and data retrieval 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/common/api.py b/src/backend/InvenTree/common/api.py index 964d9e7a77..59c11addfb 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -1405,6 +1405,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 17490b9283..bc0fb147b5 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/part/models.py b/src/backend/InvenTree/part/models.py index 9ffe3d42e9..876b5e521b 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -68,6 +68,7 @@ logger = structlog.get_logger('inventree') class PartCategory( InvenTree.models.PluginValidationMixin, + InvenTree.models.InvenTreeParameterMixin, InvenTree.models.MetadataMixin, InvenTree.models.PathStringMixin, InvenTree.models.InvenTreeTree, diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index ccbce76492..d0ce0d43fa 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -103,6 +103,8 @@ class CategorySerializer( 'structural', 'icon', 'parent_default_location', + # Optional fields + 'parameters', ] read_only_fields = ['level', 'pathstring'] @@ -176,6 +178,8 @@ class CategorySerializer( parent_default_location = serializers.IntegerField(read_only=True, allow_null=True) + parameters = common.filters.enable_parameters_filter() + class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for PartCategory tree.""" 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 edbeda1b63..10e1d887c8 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -49,15 +49,15 @@ "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.40.0", - "@emotion/react": "^11.13.3", - "@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", @@ -73,72 +73,72 @@ "@mantine/spotlight": "^9.2.1", "@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", + "@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", - "easymde": "^2.20.0", - "embla-carousel": "^8.5.2", - "embla-carousel-react": "^8.5.2", - "fuse.js": "^7.0.0", + "dayjs": "^1.11.21", + "dompurify": "^3.4.8", + "easymde": "^2.21.0", + "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-hook-form": "^7.78.0", + "react-is": "^19.2.7", + "react-router-dom": "^6.30.4", + "react-select": "^5.10.2", "react-simplemde-editor": "^5.2.0", "react-window": "1.8.11", - "recharts": "^3.1.2", - "styled-components": "^6.1.14", - "undici": "^6.24.0", - "zustand": "^5.0.8" + "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" }, "resolutions": { "undici": "^6.24.0", 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 ( +