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,
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
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
@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
[[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'" },