🐛(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
This commit is contained in:
@@ -34,6 +34,7 @@ and this project adheres to
|
|||||||
- 🐛(minio) fix user permission error with Minio and Windows #1264
|
- 🐛(minio) fix user permission error with Minio and Windows #1264
|
||||||
- 🐛(frontend) fix export when quote block and inline code #1319
|
- 🐛(frontend) fix export when quote block and inline code #1319
|
||||||
- 🐛(frontend) fix base64 font #1324
|
- 🐛(frontend) fix base64 font #1324
|
||||||
|
- 🐛(backend) allow editor to delete subpages #1296
|
||||||
|
|
||||||
## [3.5.0] - 2025-07-31
|
## [3.5.0] - 2025-07-31
|
||||||
|
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ class DocumentViewSet(
|
|||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.DocumentPermission,
|
permissions.DocumentPermission,
|
||||||
]
|
]
|
||||||
queryset = models.Document.objects.all()
|
queryset = models.Document.objects.select_related("creator").all()
|
||||||
serializer_class = serializers.DocumentSerializer
|
serializer_class = serializers.DocumentSerializer
|
||||||
ai_translate_serializer_class = serializers.AITranslateSerializer
|
ai_translate_serializer_class = serializers.AITranslateSerializer
|
||||||
children_serializer_class = serializers.ListDocumentSerializer
|
children_serializer_class = serializers.ListDocumentSerializer
|
||||||
@@ -787,7 +787,11 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# GET: List children
|
# GET: List children
|
||||||
queryset = document.get_children().filter(ancestors_deleted_at__isnull=True)
|
queryset = (
|
||||||
|
document.get_children()
|
||||||
|
.select_related("creator")
|
||||||
|
.filter(ancestors_deleted_at__isnull=True)
|
||||||
|
)
|
||||||
queryset = self.filter_queryset(queryset)
|
queryset = self.filter_queryset(queryset)
|
||||||
|
|
||||||
filterset = DocumentFilter(request.GET, queryset=queryset)
|
filterset = DocumentFilter(request.GET, queryset=queryset)
|
||||||
@@ -841,19 +845,27 @@ class DocumentViewSet(
|
|||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_document = self.queryset.only("depth", "path").get(pk=pk)
|
current_document = (
|
||||||
|
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
|
||||||
|
)
|
||||||
except models.Document.DoesNotExist as excpt:
|
except models.Document.DoesNotExist as excpt:
|
||||||
raise drf.exceptions.NotFound() from excpt
|
raise drf.exceptions.NotFound() from excpt
|
||||||
|
|
||||||
ancestors = (
|
ancestors = (
|
||||||
(current_document.get_ancestors() | self.queryset.filter(pk=pk))
|
(
|
||||||
|
current_document.get_ancestors()
|
||||||
|
| self.queryset.select_related(None).filter(pk=pk)
|
||||||
|
)
|
||||||
.filter(ancestors_deleted_at__isnull=True)
|
.filter(ancestors_deleted_at__isnull=True)
|
||||||
.order_by("path")
|
.order_by("path")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the highest readable ancestor
|
# Get the highest readable ancestor
|
||||||
highest_readable = (
|
highest_readable = (
|
||||||
ancestors.readable_per_se(request.user).only("depth", "path").first()
|
ancestors.select_related(None)
|
||||||
|
.readable_per_se(request.user)
|
||||||
|
.only("depth", "path")
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
if highest_readable is None:
|
if highest_readable is None:
|
||||||
raise (
|
raise (
|
||||||
@@ -881,7 +893,12 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
|
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
|
||||||
|
|
||||||
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
|
queryset = (
|
||||||
|
ancestors.select_related("creator").filter(
|
||||||
|
depth__gte=highest_readable.depth
|
||||||
|
)
|
||||||
|
| children
|
||||||
|
)
|
||||||
queryset = queryset.order_by("path")
|
queryset = queryset.order_by("path")
|
||||||
queryset = queryset.annotate_user_roles(user)
|
queryset = queryset.annotate_user_roles(user)
|
||||||
queryset = queryset.annotate_is_favorite(user)
|
queryset = queryset.annotate_is_favorite(user)
|
||||||
@@ -1283,7 +1300,8 @@ class DocumentViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
attachments_documents = (
|
attachments_documents = (
|
||||||
self.queryset.filter(attachments__contains=[key])
|
self.queryset.select_related(None)
|
||||||
|
.filter(attachments__contains=[key])
|
||||||
.only("path")
|
.only("path")
|
||||||
.order_by("path")
|
.order_by("path")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -753,6 +753,12 @@ class Document(MP_Node, BaseModel):
|
|||||||
can_update = (
|
can_update = (
|
||||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||||
) and not is_deleted
|
) and not is_deleted
|
||||||
|
can_create_children = can_update and user.is_authenticated
|
||||||
|
can_destroy = (
|
||||||
|
is_owner
|
||||||
|
if self.is_root()
|
||||||
|
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
|
||||||
|
)
|
||||||
|
|
||||||
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
||||||
ai_access = any(
|
ai_access = any(
|
||||||
@@ -775,11 +781,11 @@ class Document(MP_Node, BaseModel):
|
|||||||
"media_check": can_get,
|
"media_check": can_get,
|
||||||
"can_edit": can_update,
|
"can_edit": can_update,
|
||||||
"children_list": can_get,
|
"children_list": can_get,
|
||||||
"children_create": can_update and user.is_authenticated,
|
"children_create": can_create_children,
|
||||||
"collaboration_auth": can_get,
|
"collaboration_auth": can_get,
|
||||||
"cors_proxy": can_get,
|
"cors_proxy": can_get,
|
||||||
"descendants": can_get,
|
"descendants": can_get,
|
||||||
"destroy": is_owner,
|
"destroy": can_destroy,
|
||||||
"duplicate": can_get and user.is_authenticated,
|
"duplicate": can_get and user.is_authenticated,
|
||||||
"favorite": can_get and user.is_authenticated,
|
"favorite": can_get and user.is_authenticated,
|
||||||
"link_configuration": is_owner_or_admin,
|
"link_configuration": is_owner_or_admin,
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
|||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"destroy": access.role == "owner",
|
"destroy": access.role in ["administrator", "owner"],
|
||||||
"duplicate": True,
|
"duplicate": True,
|
||||||
"favorite": True,
|
"favorite": True,
|
||||||
"invite_owner": access.role == "owner",
|
"invite_owner": access.role == "owner",
|
||||||
|
|||||||
@@ -593,6 +593,86 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_authenticated, is_creator,role,link_reach,link_role,can_destroy",
|
||||||
|
[
|
||||||
|
(True, False, "owner", "restricted", "editor", True),
|
||||||
|
(True, True, "owner", "restricted", "editor", True),
|
||||||
|
(True, False, "owner", "restricted", "reader", True),
|
||||||
|
(True, True, "owner", "restricted", "reader", True),
|
||||||
|
(True, False, "owner", "authenticated", "editor", True),
|
||||||
|
(True, True, "owner", "authenticated", "editor", True),
|
||||||
|
(True, False, "owner", "authenticated", "reader", True),
|
||||||
|
(True, True, "owner", "authenticated", "reader", True),
|
||||||
|
(True, False, "owner", "public", "editor", True),
|
||||||
|
(True, True, "owner", "public", "editor", True),
|
||||||
|
(True, False, "owner", "public", "reader", True),
|
||||||
|
(True, True, "owner", "public", "reader", True),
|
||||||
|
(True, False, "administrator", "restricted", "editor", True),
|
||||||
|
(True, True, "administrator", "restricted", "editor", True),
|
||||||
|
(True, False, "administrator", "restricted", "reader", True),
|
||||||
|
(True, True, "administrator", "restricted", "reader", True),
|
||||||
|
(True, False, "administrator", "authenticated", "editor", True),
|
||||||
|
(True, True, "administrator", "authenticated", "editor", True),
|
||||||
|
(True, False, "administrator", "authenticated", "reader", True),
|
||||||
|
(True, True, "administrator", "authenticated", "reader", True),
|
||||||
|
(True, False, "administrator", "public", "editor", True),
|
||||||
|
(True, True, "administrator", "public", "editor", True),
|
||||||
|
(True, False, "administrator", "public", "reader", True),
|
||||||
|
(True, True, "administrator", "public", "reader", True),
|
||||||
|
(True, False, "editor", "restricted", "editor", False),
|
||||||
|
(True, True, "editor", "restricted", "editor", True),
|
||||||
|
(True, False, "editor", "restricted", "reader", False),
|
||||||
|
(True, True, "editor", "restricted", "reader", True),
|
||||||
|
(True, False, "editor", "authenticated", "editor", False),
|
||||||
|
(True, True, "editor", "authenticated", "editor", True),
|
||||||
|
(True, False, "editor", "authenticated", "reader", False),
|
||||||
|
(True, True, "editor", "authenticated", "reader", True),
|
||||||
|
(True, False, "editor", "public", "editor", False),
|
||||||
|
(True, True, "editor", "public", "editor", True),
|
||||||
|
(True, False, "editor", "public", "reader", False),
|
||||||
|
(True, True, "editor", "public", "reader", True),
|
||||||
|
(True, False, "reader", "restricted", "editor", False),
|
||||||
|
(True, False, "reader", "restricted", "reader", False),
|
||||||
|
(True, False, "reader", "authenticated", "editor", False),
|
||||||
|
(True, True, "reader", "authenticated", "editor", True),
|
||||||
|
(True, False, "reader", "authenticated", "reader", False),
|
||||||
|
(True, False, "reader", "public", "editor", False),
|
||||||
|
(True, True, "reader", "public", "editor", True),
|
||||||
|
(True, False, "reader", "public", "reader", False),
|
||||||
|
(False, False, None, "restricted", "editor", False),
|
||||||
|
(False, False, None, "restricted", "reader", False),
|
||||||
|
(False, False, None, "authenticated", "editor", False),
|
||||||
|
(False, False, None, "authenticated", "reader", False),
|
||||||
|
(False, False, None, "public", "editor", False),
|
||||||
|
(False, False, None, "public", "reader", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||||
|
def test_models_documents_get_abilities_children_destroy( # noqa: PLR0913
|
||||||
|
is_authenticated,
|
||||||
|
is_creator,
|
||||||
|
role,
|
||||||
|
link_reach,
|
||||||
|
link_role,
|
||||||
|
can_destroy,
|
||||||
|
):
|
||||||
|
"""For a sub document, if a user can create children, he can destroy it."""
|
||||||
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
|
parent = factories.DocumentFactory(link_reach=link_reach, link_role=link_role)
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach=link_reach,
|
||||||
|
link_role=link_role,
|
||||||
|
parent=parent,
|
||||||
|
creator=user if is_creator else None,
|
||||||
|
)
|
||||||
|
if is_authenticated:
|
||||||
|
factories.UserDocumentAccessFactory(document=parent, user=user, role=role)
|
||||||
|
|
||||||
|
abilities = document.get_abilities(user)
|
||||||
|
assert abilities["destroy"] is can_destroy
|
||||||
|
|
||||||
|
|
||||||
@override_settings(AI_ALLOW_REACH_FROM="public")
|
@override_settings(AI_ALLOW_REACH_FROM="public")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_authenticated,reach",
|
"is_authenticated,reach",
|
||||||
|
|||||||
Reference in New Issue
Block a user