Skip to content

Commit 2e04b63

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 eec419b commit 2e04b63

File tree

5 files changed

+115
-10
lines changed

5 files changed

+115
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to
3434
- 🐛(minio) fix user permission error with Minio and Windows #1264
3535
- 🐛(frontend) fix export when quote block and inline code #1319
3636
- 🐛(frontend) fix base64 font #1324
37+
- 🐛(backend) allow editor to delete subpages #1296
3738

3839
## [3.5.0] - 2025-07-31
3940

src/backend/core/api/viewsets.py

Lines changed: 25 additions & 7 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,19 +845,27 @@ 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

848854
ancestors = (
849-
(current_document.get_ancestors() | self.queryset.filter(pk=pk))
855+
(
856+
current_document.get_ancestors()
857+
| self.queryset.select_related(None).filter(pk=pk)
858+
)
850859
.filter(ancestors_deleted_at__isnull=True)
851860
.order_by("path")
852861
)
853862

854863
# Get the highest readable ancestor
855864
highest_readable = (
856-
ancestors.readable_per_se(request.user).only("depth", "path").first()
865+
ancestors.select_related(None)
866+
.readable_per_se(request.user)
867+
.only("depth", "path")
868+
.first()
857869
)
858870
if highest_readable is None:
859871
raise (
@@ -881,7 +893,12 @@ def tree(self, request, pk, *args, **kwargs):
881893

882894
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
883895

884-
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
896+
queryset = (
897+
ancestors.select_related("creator").filter(
898+
depth__gte=highest_readable.depth
899+
)
900+
| children
901+
)
885902
queryset = queryset.order_by("path")
886903
queryset = queryset.annotate_user_roles(user)
887904
queryset = queryset.annotate_is_favorite(user)
@@ -1283,7 +1300,8 @@ def media_auth(self, request, *args, **kwargs):
12831300
)
12841301

12851302
attachments_documents = (
1286-
self.queryset.filter(attachments__contains=[key])
1303+
self.queryset.select_related(None)
1304+
.filter(attachments__contains=[key])
12871305
.only("path")
12881306
.order_by("path")
12891307
)

src/backend/core/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,12 @@ def get_abilities(self, user):
753753
can_update = (
754754
is_owner_or_admin or role == RoleChoices.EDITOR
755755
) and not is_deleted
756+
can_create_children = can_update and user.is_authenticated
757+
can_destroy = (
758+
is_owner
759+
if self.is_root()
760+
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
761+
)
756762

757763
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
758764
ai_access = any(
@@ -775,11 +781,11 @@ def get_abilities(self, user):
775781
"media_check": can_get,
776782
"can_edit": can_update,
777783
"children_list": can_get,
778-
"children_create": can_update and user.is_authenticated,
784+
"children_create": can_create_children,
779785
"collaboration_auth": can_get,
780786
"cors_proxy": can_get,
781787
"descendants": can_get,
782-
"destroy": is_owner,
788+
"destroy": can_destroy,
783789
"duplicate": can_get and user.is_authenticated,
784790
"favorite": can_get and user.is_authenticated,
785791
"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 in ["administrator", "owner"],
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)