From d786e9703e58310edb16eff4e0728b94852e891c Mon Sep 17 00:00:00 2001 From: Sylvain Boissel Date: Tue, 24 Mar 2026 15:09:42 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20do=20not=20filter=20del?= =?UTF-8?q?eted=20docs=20for=20the=20owner=20in=20the=20tree=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the bug described in #1254 (Delete document request conflict) In the tree request, deleted documents for which the user is the owner are not filtered anymore. --- CHANGELOG.md | 4 ++ src/backend/core/api/viewsets.py | 15 ++++- .../documents/test_api_documents_tree.py | 55 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0ddb080..9f4d7a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to - 💄(frontend) improve comments highlights #1961 +### Fixed + +- 🐛(backend) do not filter deleted documents for the owner in the tree #2121 + ## [v4.8.3] - 2026-03-23 ### Changed diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 3296e9f0..a4e38786 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1209,7 +1209,20 @@ class DocumentViewSet( path__startswith=ancestor.path, depth=ancestor.depth + 1 ) - children = self.queryset.filter(children_clause, deleted_at__isnull=True) + # Include deleted documents owned by the user, if any. + if request.user.is_authenticated: + owned_document_ids = models.DocumentAccess.objects.filter( + user=user, + role=models.RoleChoices.OWNER, + document__deleted_at__isnull=False, + ).values("document_id") + + children = self.queryset.filter( + children_clause, + db.Q(deleted_at__isnull=True) | db.Q(id__in=owned_document_ids), + ) + else: + children = self.queryset.filter(children_clause, deleted_at__isnull=True) queryset = ( ancestors.select_related("creator").filter( diff --git a/src/backend/core/tests/documents/test_api_documents_tree.py b/src/backend/core/tests/documents/test_api_documents_tree.py index c86eebc1..d7819b71 100644 --- a/src/backend/core/tests/documents/test_api_documents_tree.py +++ b/src/backend/core/tests/documents/test_api_documents_tree.py @@ -1258,3 +1258,58 @@ def test_api_documents_tree_list_deleted_document_owner(django_assert_num_querie assert content["children"][0][ "deleted_at" ] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z") + + +def test_api_documents_tree_deleted_child_visible_to_owner(): + """ + A deleted child document should appear in the tree for users who are its direct owner, + even when they are not the owner of the parent. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public") + owned_child = factories.DocumentFactory(parent=parent, users=[(user, "owner")]) + other_child = factories.DocumentFactory(parent=parent) + + owned_child.soft_delete() + owned_child.refresh_from_db() + + response = client.get(f"/api/v1.0/documents/{parent.id!s}/tree/") + assert response.status_code == 200 + + content = response.json() + child_ids = [c["id"] for c in content["children"]] + assert str(owned_child.id) in child_ids + assert str(other_child.id) in child_ids + + owned_child_data = next( + c for c in content["children"] if c["id"] == str(owned_child.id) + ) + assert owned_child_data["deleted_at"] == owned_child.deleted_at.isoformat().replace( + "+00:00", "Z" + ) + + +def test_api_documents_tree_deleted_child_hidden_from_non_owner(): + """ + A deleted child document should not appear in the tree for users who are not its owner. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public") + deleted_child = factories.DocumentFactory(parent=parent) + visible_child = factories.DocumentFactory(parent=parent) + + deleted_child.soft_delete() + + response = client.get(f"/api/v1.0/documents/{parent.id!s}/tree/") + assert response.status_code == 200 + + content = response.json() + child_ids = [c["id"] for c in content["children"]] + assert str(deleted_child.id) not in child_ids + assert str(visible_child.id) in child_ids