]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Merge branch 'main' into dev
authorshamoon <4887959+shamoon@users.noreply.github.com>
Mon, 16 Feb 2026 17:01:19 +0000 (09:01 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Mon, 16 Feb 2026 17:01:19 +0000 (09:01 -0800)
1  2 
docs/advanced_usage.md
pyproject.toml
src-ui/package.json
src/documents/permissions.py
src/documents/serialisers.py
src/documents/templating/filepath.py
src/documents/tests/test_api_objects.py
src/documents/tests/test_file_handling.py
src/documents/views.py
uv.lock

Simple merge
diff --cc pyproject.toml
Simple merge
Simple merge
index a47762c46570dfa94e7ab724cf1d13a4cdafc420,813136a3dd78dbe47dec47145231179711643fe3..de6fff1fb01a8f087267cf9796f08f4b683804d5
@@@ -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,
index 5fd1597720779739f273383aae56cebe0826e49e,bec1254c8a6af5568bb577c23eee7c1b98c00819..5a2cd6c8a5b713d9e8f98e9d9db691ef6f47d85c
@@@ -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
Simple merge
index 1d4be14fe4b7042ba609ab5bee84cf8bb7a88c48,18648365587a5c123da48522b694049e0f761e89..9b6f96ce10fb13eaf68e2015f1157f7da1e30f9f
@@@ -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:
              self.assertEqual(len(capture.output), 1)
              self.assertEqual(
                  capture.output[0],
-                 "WARNING:paperless.templating:Template attempted restricted operation: <bound method Model.save of <Document: 2020-06-25 Does Matter>> 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
index c634c007e3d62eb36c86ec6e7a190c98584cf3b6,2ce12c330177121c68df48490eb68e4ab68ccdfb..5d78d1b48b73a919dd424f68ec7070cffb775275
@@@ -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 1b8af1b5b468048e7e0421d8535d4aa737b31ee7,f595c3dac080b10f2a2deca9722910a7a45175a2..07f521e194e71042adab88b823ac3632900eac39
+++ 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'" },