From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:01:19 +0000 (-0800) Subject: Merge branch 'main' into dev X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=be4e29a19c876d7908840d99c6d0a4625c8fa08c;p=thirdparty%2Fpaperless-ngx.git Merge branch 'main' into dev --- be4e29a19c876d7908840d99c6d0a4625c8fa08c diff --cc src/documents/permissions.py index a47762c465,813136a3dd..de6fff1fb0 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@@ -137,41 -180,57 +185,73 @@@ def _permitted_document_ids(user) def get_document_count_filter_for_user(user): """ Return the Q object used to filter document counts for the given user. + + The filter is expressed as an ``id__in`` against a small subquery of permitted + document IDs to keep the generated SQL simple and avoid large OR clauses. """ - if user is None or not getattr(user, "is_authenticated", False): - return Q(documents__deleted_at__isnull=True, documents__owner__isnull=True) if getattr(user, "is_superuser", False): + # Superuser: no permission filtering needed return Q(documents__deleted_at__isnull=True) - return Q( - documents__deleted_at__isnull=True, - documents__id__in=get_objects_for_user_owner_aware( - user, - "documents.view_document", - Document, - ).values_list("id", flat=True), + + permitted_ids = _permitted_document_ids(user) + return Q(documents__id__in=permitted_ids) + + + def annotate_document_count_for_related_queryset( + queryset, + through_model, + related_object_field: str, + target_field: str = "document_id", + user=None, + ): + """ + Annotate a queryset with permissions-aware document counts using a subquery + against a relation table. + + Args: + queryset: base queryset to annotate (must contain pk) + through_model: model representing the relation (e.g., Document.tags.through + or CustomFieldInstance) + source_field: field on the relation pointing back to queryset pk + target_field: field on the relation pointing to Document id + user: the user for whom to filter permitted document ids + """ + + permitted_ids = _permitted_document_ids(user) + counts = ( + through_model.objects.filter( + **{ + related_object_field: OuterRef("pk"), + f"{target_field}__in": permitted_ids, + }, + ) + .values(related_object_field) + .annotate(c=Count(target_field)) + .values("c") ) + return queryset.annotate(document_count=Coalesce(Subquery(counts[:1]), 0)) -def get_objects_for_user_owner_aware(user, perms, Model) -> QuerySet: - objects_owned = Model.objects.filter(owner=user) - objects_unowned = Model.objects.filter(owner__isnull=True) +def get_objects_for_user_owner_aware( + user, + perms, + Model, + *, + include_deleted=False, +) -> QuerySet: + """ + Returns objects the user owns, are unowned, or has explicit perms. + When include_deleted is True, soft-deleted items are also included. + """ + manager = ( + Model.global_objects + if include_deleted and hasattr(Model, "global_objects") + else Model.objects + ) + + objects_owned = manager.filter(owner=user) + objects_unowned = manager.filter(owner__isnull=True) objects_with_perms = get_objects_for_user( user=user, perms=perms, diff --cc src/documents/serialisers.py index 5fd1597720,bec1254c8a..5a2cd6c8a5 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@@ -4,9 -4,9 +4,10 @@@ import loggin import math import re from datetime import datetime +from datetime import timedelta from decimal import Decimal from typing import TYPE_CHECKING + from typing import Any from typing import Literal import magic @@@ -720,7 -714,10 +721,7 @@@ class StoragePathField(serializers.Prim class CustomFieldSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs): - # Ignore args passed by permissions mixin - kwargs.pop("user", None) - kwargs.pop("full_perms", None) context = kwargs.get("context") self.api_version = int( context.get("request").version diff --cc src/documents/tests/test_file_handling.py index 1d4be14fe4,1864836558..9b6f96ce10 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@@ -1361,10 -1379,10 +1361,10 @@@ class TestFilenameGeneration(Directorie @override_settings( FILENAME_FORMAT="{{created}}/{{ document.save() }}", ) - def test_template_with_security(self): + def test_template_with_security(self) -> None: """ GIVEN: - - Filename format with one or more undefined variables + - Filename format with an unavailable document attribute WHEN: - Filepath for a document with this format is called THEN: @@@ -1390,10 -1408,10 +1390,10 @@@ self.assertEqual(len(capture.output), 1) self.assertEqual( capture.output[0], - "WARNING:paperless.templating:Template attempted restricted operation: > is not safely callable", + "ERROR:paperless.templating:Template variable error: 'dict object' has no attribute 'save'", ) - def test_template_with_custom_fields(self): + def test_template_with_custom_fields(self) -> None: """ GIVEN: - Filename format which accesses custom field data diff --cc src/documents/views.py index c634c007e3,2ce12c3301..5d78d1b48b --- a/src/documents/views.py +++ b/src/documents/views.py @@@ -32,9 -32,9 +32,8 @@@ from django.db.models import Coun from django.db.models import IntegerField from django.db.models import Max from django.db.models import Model - from django.db.models import Q from django.db.models import Sum from django.db.models import When -from django.db.models.functions import Length from django.db.models.functions import Lower from django.db.models.manager import Manager from django.http import FileResponse diff --cc uv.lock index 1b8af1b5b4,f595c3dac0..07f521e194 --- a/uv.lock +++ b/uv.lock @@@ -3019,10 -1991,9 +3019,10 @@@ wheels = [[package]] name = "paperless-ngx" - version = "2.20.6" + version = "2.20.7" source = { virtual = "." } dependencies = [ + { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },