]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Fix: retrieve document_count for tag children (#11125)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 22 Oct 2025 18:13:15 +0000 (11:13 -0700)
committerGitHub <noreply@github.com>
Wed, 22 Oct 2025 18:13:15 +0000 (11:13 -0700)
src/documents/permissions.py
src/documents/serialisers.py
src/documents/tests/test_tag_hierarchy.py
src/documents/views.py

index 797d92ed93757602450a5a562b60e7e47fffcb4c..a2b165fd8f73c042334243a10e82c4e45e9b24c7 100644 (file)
@@ -2,6 +2,7 @@ from django.contrib.auth.models import Group
 from django.contrib.auth.models import Permission
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
 from django.db.models import QuerySet
 from guardian.core import ObjectPermissionChecker
 from guardian.models import GroupObjectPermission
@@ -12,6 +13,8 @@ from guardian.shortcuts import remove_perm
 from rest_framework.permissions import BasePermission
 from rest_framework.permissions import DjangoObjectPermissions
 
+from documents.models import Document
+
 
 class PaperlessObjectPermissions(DjangoObjectPermissions):
     """
@@ -125,6 +128,25 @@ def set_permissions_for_object(permissions: list[str], object, *, merge: bool =
                         )
 
 
+def get_document_count_filter_for_user(user):
+    """
+    Return the Q object used to filter document counts for the given user.
+    """
+
+    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):
+        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),
+    )
+
+
 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)
index 18d5700101d11fceae459fd0aad59a7164153315..25207bdfa6bb6fb52f8f737da1ce72de8e6ab8b8 100644 (file)
@@ -20,6 +20,7 @@ from django.core.validators import EmailValidator
 from django.core.validators import MaxLengthValidator
 from django.core.validators import RegexValidator
 from django.core.validators import integer_validator
+from django.db.models import Count
 from django.utils.crypto import get_random_string
 from django.utils.dateparse import parse_datetime
 from django.utils.text import slugify
@@ -65,6 +66,7 @@ from documents.models import WorkflowActionEmail
 from documents.models import WorkflowActionWebhook
 from documents.models import WorkflowTrigger
 from documents.parsers import is_mime_type_supported
+from documents.permissions import get_document_count_filter_for_user
 from documents.permissions import get_groups_with_only_permission
 from documents.permissions import set_permissions_for_object
 from documents.templating.filepath import validate_filepath_template_and_render
@@ -572,8 +574,16 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
         ),
     )
     def get_children(self, obj):
+        filter_q = self.context.get("document_count_filter")
+        if filter_q is None:
+            request = self.context.get("request")
+            user = getattr(request, "user", None) if request else None
+            filter_q = get_document_count_filter_for_user(user)
+            self.context["document_count_filter"] = filter_q
         serializer = TagSerializer(
-            obj.get_children(),
+            obj.get_children_queryset()
+            .select_related("owner")
+            .annotate(document_count=Count("documents", filter=filter_q)),
             many=True,
             context=self.context,
         )
index 73fe8178680a35423f7141313988d6b6efc3d0c2..708e3e534d6d8276993025f4ec29738baf8abddc 100644 (file)
@@ -9,6 +9,7 @@ from documents.models import Tag
 from documents.models import Workflow
 from documents.models import WorkflowAction
 from documents.models import WorkflowTrigger
+from documents.serialisers import TagSerializer
 from documents.signals.handlers import run_workflows
 
 
@@ -121,6 +122,31 @@ class TestTagHierarchy(APITestCase):
         tags = set(self.document.tags.values_list("pk", flat=True))
         assert tags == {self.parent.pk, orphan.pk}
 
+    def test_child_document_count_included_when_parent_paginated(self):
+        self.document.tags.add(self.child)
+
+        response = self.client.get(
+            "/api/tags/",
+            {"page_size": 1, "ordering": "-name"},
+        )
+
+        assert response.status_code == 200
+        assert response.data["results"][0]["id"] == self.parent.pk
+
+        children = response.data["results"][0]["children"]
+        assert len(children) == 1
+
+        child_entry = children[0]
+        assert child_entry["id"] == self.child.pk
+        assert child_entry["document_count"] == 1
+
+    def test_tag_serializer_populates_document_filter_context(self):
+        context = {}
+
+        serializer = TagSerializer(self.parent, context=context)
+        assert serializer.data  # triggers serialization
+        assert "document_count_filter" in context
+
     def test_cannot_set_parent_to_self(self):
         tag = Tag.objects.create(name="Selfie")
         resp = self.client.patch(
index 4168eba38ceb34a0b48fb263eb211c78bba31f38..696311a7ab947be76a01ecdf25ef53e22af4edb2 100644 (file)
@@ -141,6 +141,7 @@ from documents.permissions import AcknowledgeTasksPermissions
 from documents.permissions import PaperlessAdminPermissions
 from documents.permissions import PaperlessNotePermissions
 from documents.permissions import PaperlessObjectPermissions
+from documents.permissions import get_document_count_filter_for_user
 from documents.permissions import get_objects_for_user_owner_aware
 from documents.permissions import has_perms_owner_aware
 from documents.permissions import set_permissions_for_object
@@ -364,21 +365,13 @@ class PermissionsAwareDocumentCountMixin(BulkPermissionMixin, PassUserMixin):
     Mixin to add document count to queryset, permissions-aware if needed
     """
 
+    def get_document_count_filter(self):
+        request = getattr(self, "request", None)
+        user = getattr(request, "user", None) if request else None
+        return get_document_count_filter_for_user(user)
+
     def get_queryset(self):
-        filter = (
-            Q(documents__deleted_at__isnull=True)
-            if self.request.user is None or self.request.user.is_superuser
-            else (
-                Q(
-                    documents__deleted_at__isnull=True,
-                    documents__id__in=get_objects_for_user_owner_aware(
-                        self.request.user,
-                        "documents.view_document",
-                        Document,
-                    ).values_list("id", flat=True),
-                )
-            )
-        )
+        filter = self.get_document_count_filter()
         return (
             super()
             .get_queryset()
@@ -447,6 +440,11 @@ class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
     filterset_class = TagFilterSet
     ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count")
 
+    def get_serializer_context(self):
+        context = super().get_serializer_context()
+        context["document_count_filter"] = self.get_document_count_filter()
+        return context
+
     def perform_update(self, serializer):
         old_parent = self.get_object().get_parent()
         tag = serializer.save()