✨(backend) allow the duplication of subpages
Adds a new with_descendants parameter to the doc duplication API. The logic of the duplicate() method has been moved to a new internal _duplicate_document() method to allow for recursion. Adds unit tests for the new feature.
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to
|
||||
- ✨(frontend) Can print a doc #1832
|
||||
- ✨(backend) manage reconciliation requests for user accounts #1878
|
||||
- 👷(CI) add GHCR workflow for forked repo testing #1851
|
||||
- ✨(backend) allow the duplication of subpages #1893
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -591,10 +591,13 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
|
||||
class DocumentDuplicationSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for duplicating a document.
|
||||
Allows specifying whether to keep access permissions.
|
||||
Allows specifying whether to keep access permissions,
|
||||
and whether to duplicate descendant documents as well
|
||||
(deep copy) or not (shallow copy).
|
||||
"""
|
||||
|
||||
with_accesses = serializers.BooleanField(default=False)
|
||||
with_descendants = serializers.BooleanField(default=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
|
||||
@@ -1208,11 +1208,7 @@ class DocumentViewSet(
|
||||
@transaction.atomic
|
||||
def duplicate(self, request, *args, **kwargs):
|
||||
"""
|
||||
Duplicate a document and store the links to attached files in the duplicated
|
||||
document to allow cross-access.
|
||||
|
||||
Optionally duplicates accesses if `with_accesses` is set to true
|
||||
in the payload.
|
||||
Duplicate a document, alongside its descendants if requested.
|
||||
"""
|
||||
# Get document while checking permissions
|
||||
document_to_duplicate = self.get_object()
|
||||
@@ -1221,8 +1217,43 @@ class DocumentViewSet(
|
||||
data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = request.user
|
||||
|
||||
duplicated_document = self._duplicate_document(
|
||||
document_to_duplicate=document_to_duplicate,
|
||||
serializer=serializer,
|
||||
user=user,
|
||||
)
|
||||
|
||||
return drf_response.Response(
|
||||
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def _duplicate_document(
|
||||
self,
|
||||
document_to_duplicate,
|
||||
serializer,
|
||||
user,
|
||||
new_parent=None,
|
||||
):
|
||||
"""
|
||||
Duplicate a document and store the links to attached files in the duplicated
|
||||
document to allow cross-access.
|
||||
|
||||
Optionally duplicates accesses if `with_accesses` is set to true
|
||||
in the payload.
|
||||
|
||||
Optionally duplicates sub-documents if `with_descendants` is set to true in
|
||||
the payload. In this case, the whole subtree of the document will be duplicated,
|
||||
and the links to attached files will be stored in all duplicated documents.
|
||||
|
||||
The `with_accesses` option will also be applied to all duplicated documents
|
||||
if `with_descendants` is set to true.
|
||||
"""
|
||||
with_accesses = serializer.validated_data.get("with_accesses", False)
|
||||
user_role = document_to_duplicate.get_role(request.user)
|
||||
with_descendants = serializer.validated_data.get("with_descendants", False)
|
||||
|
||||
user_role = document_to_duplicate.get_role(user)
|
||||
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
|
||||
|
||||
base64_yjs_content = document_to_duplicate.content
|
||||
@@ -1241,11 +1272,41 @@ class DocumentViewSet(
|
||||
extracted_attachments & set(document_to_duplicate.attachments)
|
||||
)
|
||||
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
|
||||
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
|
||||
# If parent_duplicate is provided we must add the duplicated document as a child
|
||||
if new_parent is not None:
|
||||
duplicated_document = new_parent.add_child(
|
||||
title=title,
|
||||
content=base64_yjs_content,
|
||||
attachments=attachments,
|
||||
duplicated_from=document_to_duplicate,
|
||||
creator=user,
|
||||
**link_kwargs,
|
||||
)
|
||||
|
||||
# Handle access duplication for this child
|
||||
if with_accesses and is_owner_or_admin:
|
||||
original_accesses = models.DocumentAccess.objects.filter(
|
||||
document=document_to_duplicate
|
||||
).exclude(user=user)
|
||||
|
||||
accesses_to_create = [
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user_id=access.user_id,
|
||||
team=access.team,
|
||||
role=access.role,
|
||||
)
|
||||
for access in original_accesses
|
||||
]
|
||||
|
||||
if accesses_to_create:
|
||||
models.DocumentAccess.objects.bulk_create(accesses_to_create)
|
||||
|
||||
elif not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
|
||||
user_role
|
||||
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
|
||||
duplicated_document = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
creator=user,
|
||||
title=title,
|
||||
content=base64_yjs_content,
|
||||
attachments=attachments,
|
||||
@@ -1254,55 +1315,63 @@ class DocumentViewSet(
|
||||
)
|
||||
models.DocumentAccess.objects.create(
|
||||
document=duplicated_document,
|
||||
user=self.request.user,
|
||||
user=user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
return drf_response.Response(
|
||||
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
|
||||
else:
|
||||
duplicated_document = document_to_duplicate.add_sibling(
|
||||
"right",
|
||||
title=title,
|
||||
content=base64_yjs_content,
|
||||
attachments=attachments,
|
||||
duplicated_from=document_to_duplicate,
|
||||
creator=user,
|
||||
**link_kwargs,
|
||||
)
|
||||
|
||||
duplicated_document = document_to_duplicate.add_sibling(
|
||||
"right",
|
||||
title=title,
|
||||
content=base64_yjs_content,
|
||||
attachments=attachments,
|
||||
duplicated_from=document_to_duplicate,
|
||||
creator=request.user,
|
||||
**link_kwargs,
|
||||
)
|
||||
|
||||
# Always add the logged-in user as OWNER for root documents
|
||||
if document_to_duplicate.is_root():
|
||||
accesses_to_create = [
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user=request.user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
]
|
||||
|
||||
# If accesses should be duplicated, add other users' accesses as per original document
|
||||
if with_accesses and is_owner_or_admin:
|
||||
original_accesses = models.DocumentAccess.objects.filter(
|
||||
document=document_to_duplicate
|
||||
).exclude(user=request.user)
|
||||
|
||||
accesses_to_create.extend(
|
||||
# Always add the logged-in user as OWNER for root documents
|
||||
if document_to_duplicate.is_root():
|
||||
accesses_to_create = [
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user_id=access.user_id,
|
||||
team=access.team,
|
||||
role=access.role,
|
||||
user=user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
for access in original_accesses
|
||||
]
|
||||
|
||||
# If accesses should be duplicated,
|
||||
# add other users' accesses as per original document
|
||||
if with_accesses and is_owner_or_admin:
|
||||
original_accesses = models.DocumentAccess.objects.filter(
|
||||
document=document_to_duplicate
|
||||
).exclude(user=user)
|
||||
|
||||
accesses_to_create.extend(
|
||||
models.DocumentAccess(
|
||||
document=duplicated_document,
|
||||
user_id=access.user_id,
|
||||
team=access.team,
|
||||
role=access.role,
|
||||
)
|
||||
for access in original_accesses
|
||||
)
|
||||
|
||||
# Bulk create all the duplicated accesses
|
||||
models.DocumentAccess.objects.bulk_create(accesses_to_create)
|
||||
|
||||
if with_descendants:
|
||||
for child in document_to_duplicate.get_children().filter(
|
||||
ancestors_deleted_at__isnull=True
|
||||
):
|
||||
# When duplicating descendants, attach duplicates under the duplicated_document
|
||||
self._duplicate_document(
|
||||
document_to_duplicate=child,
|
||||
serializer=serializer,
|
||||
user=user,
|
||||
new_parent=duplicated_document,
|
||||
)
|
||||
|
||||
# Bulk create all the duplicated accesses
|
||||
models.DocumentAccess.objects.bulk_create(accesses_to_create)
|
||||
|
||||
return drf_response.Response(
|
||||
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return duplicated_document
|
||||
|
||||
def _search_simple(self, request, text):
|
||||
"""
|
||||
|
||||
@@ -318,3 +318,424 @@ def test_api_documents_duplicate_reader_non_root_document():
|
||||
assert duplicated_document.is_root()
|
||||
assert duplicated_document.accesses.count() == 1
|
||||
assert duplicated_document.accesses.get(user=user).role == "owner"
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_simple():
|
||||
"""
|
||||
Duplicating a document with descendants flag should recursively duplicate all children.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create document tree
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Root Document",
|
||||
)
|
||||
child1 = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child 1",
|
||||
)
|
||||
child2 = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child 2",
|
||||
)
|
||||
|
||||
initial_count = models.Document.objects.count()
|
||||
assert initial_count == 3
|
||||
|
||||
# Duplicate with descendants
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Check that all documents were duplicated (6 total: 3 original + 3 duplicated)
|
||||
assert models.Document.objects.count() == 6
|
||||
|
||||
# Check root duplication
|
||||
assert duplicated_root.title == "Copy of Root Document"
|
||||
assert duplicated_root.creator == user
|
||||
assert duplicated_root.duplicated_from == root
|
||||
assert duplicated_root.get_children().count() == 2
|
||||
|
||||
# Check children duplication
|
||||
duplicated_children = duplicated_root.get_children().order_by("title")
|
||||
assert duplicated_children.count() == 2
|
||||
|
||||
duplicated_child1 = duplicated_children.first()
|
||||
assert duplicated_child1.title == "Copy of Child 1"
|
||||
assert duplicated_child1.creator == user
|
||||
assert duplicated_child1.duplicated_from == child1
|
||||
assert duplicated_child1.get_parent() == duplicated_root
|
||||
|
||||
duplicated_child2 = duplicated_children.last()
|
||||
assert duplicated_child2.title == "Copy of Child 2"
|
||||
assert duplicated_child2.creator == user
|
||||
assert duplicated_child2.duplicated_from == child2
|
||||
assert duplicated_child2.get_parent() == duplicated_root
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_multi_level():
|
||||
"""
|
||||
Duplicating should recursively handle multiple levels of nesting.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Level 0",
|
||||
)
|
||||
child = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Level 1",
|
||||
)
|
||||
grandchild = factories.DocumentFactory(
|
||||
parent=child,
|
||||
title="Level 2",
|
||||
)
|
||||
great_grandchild = factories.DocumentFactory(
|
||||
parent=grandchild,
|
||||
title="Level 3",
|
||||
)
|
||||
|
||||
initial_count = models.Document.objects.count()
|
||||
assert initial_count == 4
|
||||
|
||||
# Duplicate with descendants
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Check that all documents were duplicated
|
||||
assert models.Document.objects.count() == 8
|
||||
|
||||
# Verify the tree structure
|
||||
assert duplicated_root.depth == root.depth
|
||||
dup_children = duplicated_root.get_children()
|
||||
assert dup_children.count() == 1
|
||||
|
||||
dup_child = dup_children.first()
|
||||
assert dup_child.title == "Copy of Level 1"
|
||||
assert dup_child.duplicated_from == child
|
||||
dup_grandchildren = dup_child.get_children()
|
||||
assert dup_grandchildren.count() == 1
|
||||
|
||||
dup_grandchild = dup_grandchildren.first()
|
||||
assert dup_grandchild.title == "Copy of Level 2"
|
||||
assert dup_grandchild.duplicated_from == grandchild
|
||||
dup_great_grandchildren = dup_grandchild.get_children()
|
||||
assert dup_great_grandchildren.count() == 1
|
||||
|
||||
dup_great_grandchild = dup_great_grandchildren.first()
|
||||
assert dup_great_grandchild.title == "Copy of Level 3"
|
||||
assert dup_great_grandchild.duplicated_from == great_grandchild
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_and_attachments():
|
||||
"""
|
||||
Duplicating with descendants should properly handle attachments in all children.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create documents with attachments
|
||||
root_id = uuid.uuid4()
|
||||
child_id = uuid.uuid4()
|
||||
image_key_root, image_url_root = get_image_refs(root_id)
|
||||
image_key_child, image_url_child = get_image_refs(child_id)
|
||||
|
||||
# Create root document with attachment
|
||||
ydoc = pycrdt.Doc()
|
||||
fragment = pycrdt.XmlFragment(
|
||||
[
|
||||
pycrdt.XmlElement("img", {"src": image_url_root}),
|
||||
]
|
||||
)
|
||||
ydoc["document-store"] = fragment
|
||||
update = ydoc.get_update()
|
||||
root_content = base64.b64encode(update).decode("utf-8")
|
||||
|
||||
root = factories.DocumentFactory(
|
||||
id=root_id,
|
||||
users=[(user, "owner")],
|
||||
title="Root with Image",
|
||||
content=root_content,
|
||||
attachments=[image_key_root],
|
||||
)
|
||||
|
||||
# Create child with different attachment
|
||||
ydoc_child = pycrdt.Doc()
|
||||
fragment_child = pycrdt.XmlFragment(
|
||||
[
|
||||
pycrdt.XmlElement("img", {"src": image_url_child}),
|
||||
]
|
||||
)
|
||||
ydoc_child["document-store"] = fragment_child
|
||||
update_child = ydoc_child.get_update()
|
||||
child_content = base64.b64encode(update_child).decode("utf-8")
|
||||
|
||||
child = factories.DocumentFactory(
|
||||
id=child_id,
|
||||
parent=root,
|
||||
title="Child with Image",
|
||||
content=child_content,
|
||||
attachments=[image_key_child],
|
||||
)
|
||||
|
||||
# Duplicate with descendants
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Check root attachments
|
||||
assert duplicated_root.attachments == [image_key_root]
|
||||
assert duplicated_root.content == root_content
|
||||
|
||||
# Check child attachments
|
||||
dup_children = duplicated_root.get_children()
|
||||
assert dup_children.count() == 1
|
||||
dup_child = dup_children.first()
|
||||
assert dup_child.attachments == [image_key_child]
|
||||
assert dup_child.content == child_content
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_and_accesses():
|
||||
"""
|
||||
Duplicating with descendants and accesses should propagate accesses to all children.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create document tree with accesses
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner"), (other_user, "editor")],
|
||||
title="Root",
|
||||
)
|
||||
child = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child",
|
||||
)
|
||||
factories.UserDocumentAccessFactory(document=child, user=other_user, role="reader")
|
||||
|
||||
# Duplicate with descendants and accesses
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True, "with_accesses": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Check root accesses (should be duplicated)
|
||||
root_accesses = duplicated_root.accesses.order_by("user_id")
|
||||
assert root_accesses.count() == 2
|
||||
assert root_accesses.get(user=user).role == "owner"
|
||||
assert root_accesses.get(user=other_user).role == "editor"
|
||||
|
||||
# Check child accesses (should be duplicated)
|
||||
dup_children = duplicated_root.get_children()
|
||||
dup_child = dup_children.first()
|
||||
child_accesses = dup_child.accesses.order_by("user_id")
|
||||
assert child_accesses.count() == 1
|
||||
assert child_accesses.get(user=other_user).role == "reader"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["editor", "reader"])
|
||||
def test_api_documents_duplicate_with_descendants_non_root_document_becomes_root(role):
|
||||
"""
|
||||
When duplicating a non-root document with descendants as a reader/editor,
|
||||
it should become a root document and still duplicate its children.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(users=[(user, "owner")])
|
||||
child = factories.DocumentFactory(
|
||||
parent=parent,
|
||||
users=[(user, role)],
|
||||
title="Sub Document",
|
||||
)
|
||||
grandchild = factories.DocumentFactory(
|
||||
parent=child,
|
||||
title="Grandchild",
|
||||
)
|
||||
|
||||
assert child.is_child_of(parent)
|
||||
|
||||
# Duplicate the child (non-root) with descendants
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{child.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_child = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
assert duplicated_child.title == "Copy of Sub Document"
|
||||
|
||||
dup_grandchildren = duplicated_child.get_children()
|
||||
assert dup_grandchildren.count() == 1
|
||||
dup_grandchild = dup_grandchildren.first()
|
||||
assert dup_grandchild.title == "Copy of Grandchild"
|
||||
assert dup_grandchild.duplicated_from == grandchild
|
||||
|
||||
|
||||
def test_api_documents_duplicate_without_descendants_should_not_duplicate_children():
|
||||
"""
|
||||
When with_descendants is not set or False, children should not be duplicated.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create document tree
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Root",
|
||||
)
|
||||
child = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child",
|
||||
)
|
||||
|
||||
initial_count = models.Document.objects.count()
|
||||
assert initial_count == 2
|
||||
|
||||
# Duplicate without descendants (default behavior)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Only root should be duplicated, not children
|
||||
assert models.Document.objects.count() == 3
|
||||
assert duplicated_root.get_children().count() == 0
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_preserves_link_configuration():
|
||||
"""
|
||||
Duplicating with descendants should preserve link configuration (link_reach, link_role)
|
||||
for all children when with_accesses is True.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create document tree with specific link configurations
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Root",
|
||||
link_reach="public",
|
||||
link_role="reader",
|
||||
)
|
||||
child = factories.DocumentFactory(
|
||||
parent=root,
|
||||
title="Child",
|
||||
link_reach="restricted",
|
||||
link_role="editor",
|
||||
)
|
||||
|
||||
# Duplicate with descendants and accesses
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True, "with_accesses": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# Check root link configuration
|
||||
assert duplicated_root.link_reach == root.link_reach
|
||||
assert duplicated_root.link_role == root.link_role
|
||||
|
||||
# Check child link configuration
|
||||
dup_children = duplicated_root.get_children()
|
||||
dup_child = dup_children.first()
|
||||
assert dup_child.link_reach == child.link_reach
|
||||
assert dup_child.link_role == child.link_role
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_descendants_complex_tree():
|
||||
"""
|
||||
Test duplication of a complex tree structure with multiple branches.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create a complex tree:
|
||||
# root
|
||||
# / \
|
||||
# c1 c2
|
||||
# / \ \
|
||||
# gc1 gc2 gc3
|
||||
root = factories.DocumentFactory(
|
||||
users=[(user, "owner")],
|
||||
title="Root",
|
||||
)
|
||||
child1 = factories.DocumentFactory(parent=root, title="Child 1")
|
||||
child2 = factories.DocumentFactory(parent=root, title="Child 2")
|
||||
_grandchild1 = factories.DocumentFactory(parent=child1, title="GrandChild 1")
|
||||
_grandchild2 = factories.DocumentFactory(parent=child1, title="GrandChild 2")
|
||||
_grandchild3 = factories.DocumentFactory(parent=child2, title="GrandChild 3")
|
||||
|
||||
initial_count = models.Document.objects.count()
|
||||
assert initial_count == 6
|
||||
|
||||
# Duplicate with descendants
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{root.id!s}/duplicate/",
|
||||
{"with_descendants": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
duplicated_root = models.Document.objects.get(id=response.json()["id"])
|
||||
|
||||
# All documents should be duplicated
|
||||
assert models.Document.objects.count() == 12
|
||||
|
||||
# Check structure is preserved
|
||||
dup_children = duplicated_root.get_children().order_by("title")
|
||||
assert dup_children.count() == 2
|
||||
|
||||
dup_child1 = dup_children.first()
|
||||
assert dup_child1.title == "Copy of Child 1"
|
||||
dup_grandchildren1 = dup_child1.get_children().order_by("title")
|
||||
assert dup_grandchildren1.count() == 2
|
||||
assert dup_grandchildren1.first().title == "Copy of GrandChild 1"
|
||||
assert dup_grandchildren1.last().title == "Copy of GrandChild 2"
|
||||
|
||||
dup_child2 = dup_children.last()
|
||||
assert dup_child2.title == "Copy of Child 2"
|
||||
dup_grandchildren2 = dup_child2.get_children()
|
||||
assert dup_grandchildren2.count() == 1
|
||||
assert dup_grandchildren2.first().title == "Copy of GrandChild 3"
|
||||
|
||||
Reference in New Issue
Block a user