feat: add perm param to permitted_document_ids for change/delete checks

Widens permitted_document_ids(user, *, include_deleted=False) to
permitted_document_ids(user, *, perm="view_document", include_deleted=False)
so Stage 2 callers can check change_document/delete_document permissions
instead of the hardcoded view_document codename. Default is unchanged for
every existing call site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GRp4kf1mdn9ruv81zWAmh2
This commit is contained in:
stumpylog
2026-07-27 21:36:38 -07:00
co-authored by Claude Sonnet 5
parent d4fa852373
commit 09c60ea667
2 changed files with 56 additions and 7 deletions
+12 -7
View File
@@ -163,13 +163,18 @@ def set_permissions_for_object(
)
def permitted_document_ids(user, *, include_deleted: bool = False):
def permitted_document_ids(
user,
*,
perm: str = "view_document",
include_deleted: bool = False,
):
"""
Return a queryset of document IDs the user may view. By default limited
to non-deleted documents; pass ``include_deleted=True`` for callers that
need to check permission on soft-deleted documents (e.g. trash restore).
This intentionally avoids ``get_objects_for_user`` to keep the subquery
small and index-friendly.
Return a queryset of document IDs the user has ``perm`` on (default
``"view_document"``). By default limited to non-deleted documents; pass
``include_deleted=True`` for callers that need to check permission on
soft-deleted documents (e.g. trash restore). This intentionally avoids
``get_objects_for_user`` to keep the subquery small and index-friendly.
"""
manager = Document.global_objects if include_deleted else Document.objects
@@ -185,7 +190,7 @@ def permitted_document_ids(user, *, include_deleted: bool = False):
document_ct = ContentType.objects.get_for_model(Document)
perm_filter = {
"permission__codename": "view_document",
"permission__codename": perm,
"permission__content_type": document_ct,
}
@@ -212,3 +212,47 @@ class TestDuplicateDocumentsPermissionBoundary:
result_stranger = _get_viewable_duplicates(original, stranger)
assert {d.pk for d in result_stranger} == {dup_visible.pk}
@pytest.mark.django_db
class TestPermittedDocumentIdsArbitraryPermission:
def test_change_document_permission_is_distinct_from_view(self):
owner = User.objects.create_user(username="owner")
viewer_only = User.objects.create_user(username="viewer")
editor = User.objects.create_user(username="editor")
doc = DocumentFactory(owner=owner)
assign_perm("view_document", viewer_only, doc)
assign_perm("change_document", editor, doc)
assign_perm("view_document", editor, doc)
assert_visible_document_ids(
permitted_document_ids(editor, perm="change_document"),
expected_visible=[doc.pk],
expected_hidden=[],
)
assert_visible_document_ids(
permitted_document_ids(viewer_only, perm="change_document"),
expected_visible=[],
expected_hidden=[doc.pk],
)
def test_delete_permission_with_include_deleted_for_trash_restore(self):
owner = User.objects.create_user(username="owner")
stranger = User.objects.create_user(username="mallory")
doc = DocumentFactory(owner=owner)
doc.delete()
assert_visible_document_ids(
permitted_document_ids(owner, perm="delete_document", include_deleted=True),
expected_visible=[doc.pk],
expected_hidden=[],
)
assert_visible_document_ids(
permitted_document_ids(
stranger,
perm="delete_document",
include_deleted=True,
),
expected_visible=[],
expected_hidden=[doc.pk],
)