(backend) add API endpoint to create children for a document

We add a POST method to the existing children endpoint.
This commit is contained in:
Samuel Paccoud - DINUM
2024-12-18 08:44:12 +01:00
committed by Anthony LC
parent 7ff4bc457f
commit 1d0386d9b5
6 changed files with 310 additions and 18 deletions

View File

@@ -8,7 +8,8 @@ from rest_framework import permissions
from core.models import DocumentAccess, RoleChoices
ACTION_FOR_METHOD_TO_PERMISSION = {
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"}
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"},
"children": {"GET": "children_list", "POST": "children_create"},
}

View File

@@ -12,6 +12,7 @@ from django.contrib.postgres.search import TrigramSimilarity
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.db import models as db
from django.db import transaction
from django.db.models import (
Exists,
F,
@@ -493,11 +494,38 @@ class DocumentViewSet(
detail=True,
methods=["get", "post"],
serializer_class=serializers.ListDocumentSerializer,
url_path="children",
)
# pylint: disable=unused-argument
def children(self, request, pk, *args, **kwargs):
"""Custom action to retrieve children of a document"""
def children(self, request, *args, **kwargs):
"""Handle listing and creating children of a document"""
document = self.get_object()
if request.method == "POST":
# Create a child document
serializer = serializers.DocumentSerializer(
data=request.data, context=self.get_serializer_context()
)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
child_document = document.add_child(
creator=request.user,
**serializer.validated_data,
)
models.DocumentAccess.objects.create(
document=child_document,
user=request.user,
role=models.RoleChoices.OWNER,
)
# Set the created instance to the serializer
serializer.instance = child_document
headers = self.get_success_headers(serializer.data)
return drf.response.Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
# GET: List children
queryset = document.get_children()
queryset = self.annotate_queryset(queryset)

View File

@@ -584,7 +584,8 @@ class Document(MP_Node, BaseModel):
"ai_transform": can_update,
"ai_translate": can_update,
"attachment_upload": can_update,
"children": can_get,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"destroy": RoleChoices.OWNER in roles,
"favorite": can_get and user.is_authenticated,

View File

@@ -0,0 +1,249 @@
"""
Tests for Documents API endpoint in impress's core app: create
"""
import random
from uuid import uuid4
import pytest
from rest_framework.test import APIClient
from core import factories
from core.models import Document, LinkReachChoices, LinkRoleChoices
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize("depth", [1, 2, 3])
@pytest.mark.parametrize("role", LinkRoleChoices.values)
@pytest.mark.parametrize("reach", LinkReachChoices.values)
def test_api_documents_children_create_anonymous(reach, role, depth):
"""Anonymous users should not be allowed to create children documents."""
for i in range(depth):
if i == 0:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
else:
document = factories.DocumentFactory(parent=document)
response = APIClient().post(
f"/api/v1.0/documents/{document.id!s}/children/",
{
"title": "my document",
},
)
assert response.status_code == 401
assert Document.objects.count() == depth
@pytest.mark.parametrize("depth", [1, 2, 3])
@pytest.mark.parametrize(
"reach,role",
[
["restricted", "editor"],
["restricted", "reader"],
["public", "reader"],
["authenticated", "reader"],
],
)
def test_api_documents_children_create_authenticated_forbidden(reach, role, depth):
"""
Authenticated users with no write access on a document should not be allowed
to create a nested document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
for i in range(depth):
if i == 0:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
else:
document = factories.DocumentFactory(parent=document, link_role="reader")
response = client.post(
f"/api/v1.0/documents/{document.id!s}/children/",
{
"title": "my document",
},
)
assert response.status_code == 403
assert Document.objects.count() == depth
@pytest.mark.parametrize("depth", [1, 2, 3])
@pytest.mark.parametrize(
"reach,role",
[
["public", "editor"],
["authenticated", "editor"],
],
)
def test_api_documents_children_create_authenticated_success(reach, role, depth):
"""
Authenticated users with write access on a document should be able
to create a nested document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
for i in range(depth):
if i == 0:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
else:
document = factories.DocumentFactory(parent=document, link_role="reader")
response = client.post(
f"/api/v1.0/documents/{document.id!s}/children/",
{
"title": "my child",
},
)
assert response.status_code == 201
child = Document.objects.get(id=response.json()["id"])
assert child.title == "my child"
assert child.link_reach == "restricted"
assert child.accesses.filter(role="owner", user=user).exists()
@pytest.mark.parametrize("depth", [1, 2, 3])
def test_api_documents_children_create_related_forbidden(depth):
"""
Authenticated users with a specific read access on a document should not be allowed
to create a nested document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
for i in range(depth):
if i == 0:
document = factories.DocumentFactory(link_reach="restricted")
factories.UserDocumentAccessFactory(
user=user, document=document, role="reader"
)
else:
document = factories.DocumentFactory(
parent=document, link_reach="restricted"
)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/children/",
{
"title": "my document",
},
)
assert response.status_code == 403
assert Document.objects.count() == depth
@pytest.mark.parametrize("depth", [1, 2, 3])
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
def test_api_documents_children_create_related_success(role, depth):
"""
Authenticated users with a specific write access on a document should be
able to create a nested document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
for i in range(depth):
if i == 0:
document = factories.DocumentFactory(link_reach="restricted")
factories.UserDocumentAccessFactory(user=user, document=document, role=role)
else:
document = factories.DocumentFactory(
parent=document, link_reach="restricted"
)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/children/",
{
"title": "my child",
},
)
assert response.status_code == 201
child = Document.objects.get(id=response.json()["id"])
assert child.title == "my child"
assert child.link_reach == "restricted"
assert child.accesses.filter(role="owner", user=user).exists()
def test_api_documents_children_create_authenticated_title_null():
"""It should be possible to create several nested documents with a null title."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(
title=None, link_reach="authenticated", link_role="editor"
)
factories.DocumentFactory(title=None, parent=parent)
response = client.post(
f"/api/v1.0/documents/{parent.id!s}/children/", {}, format="json"
)
assert response.status_code == 201
assert Document.objects.filter(title__isnull=True).count() == 3
def test_api_documents_children_create_force_id_success():
"""It should be possible to force the document ID when creating a nested document."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
access = factories.UserDocumentAccessFactory(user=user, role="editor")
forced_id = uuid4()
response = client.post(
f"/api/v1.0/documents/{access.document.id!s}/children/",
{
"id": str(forced_id),
"title": "my document",
},
format="json",
)
assert response.status_code == 201
assert Document.objects.count() == 2
assert response.json()["id"] == str(forced_id)
def test_api_documents_children_create_force_id_existing():
"""
It should not be possible to use the ID of an existing document when forcing ID on creation.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
access = factories.UserDocumentAccessFactory(user=user, role="editor")
document = factories.DocumentFactory()
response = client.post(
f"/api/v1.0/documents/{access.document.id!s}/children/",
{
"id": str(document.id),
"title": "my document",
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"id": ["A document with this ID already exists. You cannot override it."]
}

View File

@@ -27,7 +27,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"children": True,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
# Anonymous user can't favorite a document even with read access
@@ -78,7 +79,8 @@ def test_api_documents_retrieve_anonymous_public_parent():
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"attachment_upload": grand_parent.link_role == "editor",
"children": True,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
# Anonymous user can't favorite a document even with read access
@@ -163,7 +165,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"children": True,
"children_create": document.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,
@@ -221,7 +224,8 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"attachment_upload": grand_parent.link_role == "editor",
"children": True,
"children_create": grand_parent.link_role == "editor",
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,
@@ -386,7 +390,8 @@ def test_api_documents_retrieve_authenticated_related_parent():
"ai_transform": access.role != "reader",
"ai_translate": access.role != "reader",
"attachment_upload": access.role != "reader",
"children": True,
"children_create": access.role != "reader",
"children_list": True,
"collaboration_auth": True,
"destroy": access.role == "owner",
"favorite": True,

View File

@@ -109,7 +109,8 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"children": False,
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"destroy": False,
"favorite": False,
@@ -147,7 +148,8 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"children": True,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": is_authenticated,
@@ -185,7 +187,8 @@ def test_models_documents_get_abilities_editor(is_authenticated, reach):
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"children": True,
"children_create": is_authenticated,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": is_authenticated,
@@ -212,7 +215,8 @@ def test_models_documents_get_abilities_owner():
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"children": True,
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"destroy": True,
"favorite": True,
@@ -238,7 +242,8 @@ def test_models_documents_get_abilities_administrator():
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"children": True,
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,
@@ -267,7 +272,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"children": True,
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,
@@ -298,7 +304,8 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"children": True,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,
@@ -330,7 +337,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"children": True,
"children_create": False,
"children_list": True,
"collaboration_auth": True,
"destroy": False,
"favorite": True,