Skip to content

Commit c478359

Browse files
committed
🐛(backend) allow creator to delete subpages
An editor who created a subpages should be allowed to delete it. We change the abilities to be coherent between the creation and the deletion. Fixes #1193
1 parent 586825a commit c478359

File tree

5 files changed

+107
-8
lines changed

5 files changed

+107
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ and this project adheres to
2929

3030
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
3131
- 🐛(minio) fix user permission error with Minio and Windows #1264
32+
- 🐛(backend) allow editor to delete subpages #1296
3233

3334
## [3.5.0] - 2025-07-31
3435

src/backend/core/api/viewsets.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ class DocumentViewSet(
360360
permission_classes = [
361361
permissions.DocumentPermission,
362362
]
363-
queryset = models.Document.objects.all()
363+
queryset = models.Document.objects.select_related("creator").all()
364364
serializer_class = serializers.DocumentSerializer
365365
ai_translate_serializer_class = serializers.AITranslateSerializer
366366
children_serializer_class = serializers.ListDocumentSerializer
@@ -787,7 +787,11 @@ def children(self, request, *args, **kwargs):
787787
)
788788

789789
# GET: List children
790-
queryset = document.get_children().filter(ancestors_deleted_at__isnull=True)
790+
queryset = (
791+
document.get_children()
792+
.select_related("creator")
793+
.filter(ancestors_deleted_at__isnull=True)
794+
)
791795
queryset = self.filter_queryset(queryset)
792796

793797
filterset = DocumentFilter(request.GET, queryset=queryset)
@@ -841,7 +845,9 @@ def tree(self, request, pk, *args, **kwargs):
841845
user = self.request.user
842846

843847
try:
844-
current_document = self.queryset.only("depth", "path").get(pk=pk)
848+
current_document = (
849+
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
850+
)
845851
except models.Document.DoesNotExist as excpt:
846852
raise drf.exceptions.NotFound() from excpt
847853

@@ -881,7 +887,12 @@ def tree(self, request, pk, *args, **kwargs):
881887

882888
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
883889

884-
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
890+
queryset = (
891+
ancestors.select_related("creator").filter(
892+
depth__gte=highest_readable.depth
893+
)
894+
| children
895+
)
885896
queryset = queryset.order_by("path")
886897
queryset = queryset.annotate_user_roles(user)
887898
queryset = queryset.annotate_is_favorite(user)
@@ -1283,7 +1294,8 @@ def media_auth(self, request, *args, **kwargs):
12831294
)
12841295

12851296
attachments_documents = (
1286-
self.queryset.filter(attachments__contains=[key])
1297+
self.queryset.select_related(None)
1298+
.filter(attachments__contains=[key])
12871299
.only("path")
12881300
.order_by("path")
12891301
)

src/backend/core/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,12 @@ def get_abilities(self, user):
762762
can_update = (
763763
is_owner_or_admin or role == RoleChoices.EDITOR
764764
) and not is_deleted
765+
can_create_children = can_update and user.is_authenticated
766+
can_destroy = (
767+
is_owner
768+
if self.is_root()
769+
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
770+
)
765771

766772
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
767773
ai_access = any(
@@ -784,11 +790,11 @@ def get_abilities(self, user):
784790
"media_check": can_get,
785791
"can_edit": can_update,
786792
"children_list": can_get,
787-
"children_create": can_update and user.is_authenticated,
793+
"children_create": can_create_children,
788794
"collaboration_auth": can_get,
789795
"cors_proxy": can_get,
790796
"descendants": can_get,
791-
"destroy": is_owner,
797+
"destroy": can_destroy,
792798
"duplicate": can_get and user.is_authenticated,
793799
"favorite": can_get and user.is_authenticated,
794800
"link_configuration": is_owner_or_admin,

src/backend/core/tests/documents/test_api_documents_retrieve.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
494494
"collaboration_auth": True,
495495
"descendants": True,
496496
"cors_proxy": True,
497-
"destroy": access.role == "owner",
497+
"destroy": access.role != "reader",
498498
"duplicate": True,
499499
"favorite": True,
500500
"invite_owner": access.role == "owner",

src/backend/core/tests/test_models_documents.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,86 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
593593
}
594594

595595

596+
@pytest.mark.parametrize(
597+
"is_authenticated, is_creator,role,link_reach,link_role,can_destroy",
598+
[
599+
(True, False, "owner", "restricted", "editor", True),
600+
(True, True, "owner", "restricted", "editor", True),
601+
(True, False, "owner", "restricted", "reader", True),
602+
(True, True, "owner", "restricted", "reader", True),
603+
(True, False, "owner", "authenticated", "editor", True),
604+
(True, True, "owner", "authenticated", "editor", True),
605+
(True, False, "owner", "authenticated", "reader", True),
606+
(True, True, "owner", "authenticated", "reader", True),
607+
(True, False, "owner", "public", "editor", True),
608+
(True, True, "owner", "public", "editor", True),
609+
(True, False, "owner", "public", "reader", True),
610+
(True, True, "owner", "public", "reader", True),
611+
(True, False, "administrator", "restricted", "editor", True),
612+
(True, True, "administrator", "restricted", "editor", True),
613+
(True, False, "administrator", "restricted", "reader", True),
614+
(True, True, "administrator", "restricted", "reader", True),
615+
(True, False, "administrator", "authenticated", "editor", True),
616+
(True, True, "administrator", "authenticated", "editor", True),
617+
(True, False, "administrator", "authenticated", "reader", True),
618+
(True, True, "administrator", "authenticated", "reader", True),
619+
(True, False, "administrator", "public", "editor", True),
620+
(True, True, "administrator", "public", "editor", True),
621+
(True, False, "administrator", "public", "reader", True),
622+
(True, True, "administrator", "public", "reader", True),
623+
(True, False, "editor", "restricted", "editor", False),
624+
(True, True, "editor", "restricted", "editor", True),
625+
(True, False, "editor", "restricted", "reader", False),
626+
(True, True, "editor", "restricted", "reader", True),
627+
(True, False, "editor", "authenticated", "editor", False),
628+
(True, True, "editor", "authenticated", "editor", True),
629+
(True, False, "editor", "authenticated", "reader", False),
630+
(True, True, "editor", "authenticated", "reader", True),
631+
(True, False, "editor", "public", "editor", False),
632+
(True, True, "editor", "public", "editor", True),
633+
(True, False, "editor", "public", "reader", False),
634+
(True, True, "editor", "public", "reader", True),
635+
(True, False, "reader", "restricted", "editor", False),
636+
(True, False, "reader", "restricted", "reader", False),
637+
(True, False, "reader", "authenticated", "editor", False),
638+
(True, True, "reader", "authenticated", "editor", True),
639+
(True, False, "reader", "authenticated", "reader", False),
640+
(True, False, "reader", "public", "editor", False),
641+
(True, True, "reader", "public", "editor", True),
642+
(True, False, "reader", "public", "reader", False),
643+
(False, False, None, "restricted", "editor", False),
644+
(False, False, None, "restricted", "reader", False),
645+
(False, False, None, "authenticated", "editor", False),
646+
(False, False, None, "authenticated", "reader", False),
647+
(False, False, None, "public", "editor", False),
648+
(False, False, None, "public", "reader", False),
649+
],
650+
)
651+
# pylint: disable=too-many-arguments, too-many-positional-arguments
652+
def test_models_documents_get_abilities_children_destroy( # noqa: PLR0913
653+
is_authenticated,
654+
is_creator,
655+
role,
656+
link_reach,
657+
link_role,
658+
can_destroy,
659+
):
660+
"""For a sub document, if a user can create children, he can destroy it."""
661+
user = factories.UserFactory() if is_authenticated else AnonymousUser()
662+
parent = factories.DocumentFactory(link_reach=link_reach, link_role=link_role)
663+
document = factories.DocumentFactory(
664+
link_reach=link_reach,
665+
link_role=link_role,
666+
parent=parent,
667+
creator=user if is_creator else None,
668+
)
669+
if is_authenticated:
670+
factories.UserDocumentAccessFactory(document=parent, user=user, role=role)
671+
672+
abilities = document.get_abilities(user)
673+
assert abilities["destroy"] is can_destroy
674+
675+
596676
@override_settings(AI_ALLOW_REACH_FROM="public")
597677
@pytest.mark.parametrize(
598678
"is_authenticated,reach",

0 commit comments

Comments
 (0)