✨(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:
committed by
Anthony LC
parent
7ff4bc457f
commit
1d0386d9b5
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user