Skip to content

Commit 5d7985e

Browse files
committed
🐛(backend) duplicate sub docs as root for reader user
Reader user should be able to duplicate a doc in the doc tree. It should be created a new doc at the root level.
1 parent 0dd6818 commit 5d7985e

File tree

3 files changed

+66
-11
lines changed

3 files changed

+66
-11
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
### Fixed
12+
13+
- 🐛(backend) duplicate sub docs as root for reader user
1114

1215
## [3.7.0] - 2025-09-12
1316

src/backend/core/api/viewsets.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -941,37 +941,64 @@ def duplicate(self, request, *args, **kwargs):
941941
in the payload.
942942
"""
943943
# Get document while checking permissions
944-
document = self.get_object()
944+
document_to_duplicate = self.get_object()
945945

946946
serializer = serializers.DocumentDuplicationSerializer(
947947
data=request.data, partial=True
948948
)
949949
serializer.is_valid(raise_exception=True)
950950
with_accesses = serializer.validated_data.get("with_accesses", False)
951-
is_owner_or_admin = document.get_role(request.user) in models.PRIVILEGED_ROLES
951+
user_role = document_to_duplicate.get_role(request.user)
952+
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
952953

953-
base64_yjs_content = document.content
954+
base64_yjs_content = document_to_duplicate.content
954955

955956
# Duplicate the document instance
956957
link_kwargs = (
957-
{"link_reach": document.link_reach, "link_role": document.link_role}
958+
{
959+
"link_reach": document_to_duplicate.link_reach,
960+
"link_role": document_to_duplicate.link_role,
961+
}
958962
if with_accesses
959963
else {}
960964
)
961-
extracted_attachments = set(extract_attachments(document.content))
962-
attachments = list(extracted_attachments & set(document.attachments))
963-
duplicated_document = document.add_sibling(
965+
extracted_attachments = set(extract_attachments(document_to_duplicate.content))
966+
attachments = list(
967+
extracted_attachments & set(document_to_duplicate.attachments)
968+
)
969+
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
970+
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
971+
user_role
972+
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
973+
duplicated_document = models.Document.add_root(
974+
creator=self.request.user,
975+
title=title,
976+
content=base64_yjs_content,
977+
attachments=attachments,
978+
duplicated_from=document_to_duplicate,
979+
**link_kwargs,
980+
)
981+
models.DocumentAccess.objects.create(
982+
document=duplicated_document,
983+
user=self.request.user,
984+
role=models.RoleChoices.OWNER,
985+
)
986+
return drf_response.Response(
987+
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
988+
)
989+
990+
duplicated_document = document_to_duplicate.add_sibling(
964991
"right",
965-
title=capfirst(_("copy of {title}").format(title=document.title)),
992+
title=title,
966993
content=base64_yjs_content,
967994
attachments=attachments,
968-
duplicated_from=document,
995+
duplicated_from=document_to_duplicate,
969996
creator=request.user,
970997
**link_kwargs,
971998
)
972999

9731000
# Always add the logged-in user as OWNER for root documents
974-
if document.is_root():
1001+
if document_to_duplicate.is_root():
9751002
accesses_to_create = [
9761003
models.DocumentAccess(
9771004
document=duplicated_document,
@@ -983,7 +1010,7 @@ def duplicate(self, request, *args, **kwargs):
9831010
# If accesses should be duplicated, add other users' accesses as per original document
9841011
if with_accesses and is_owner_or_admin:
9851012
original_accesses = models.DocumentAccess.objects.filter(
986-
document=document
1013+
document=document_to_duplicate
9871014
).exclude(user=request.user)
9881015

9891016
accesses_to_create.extend(

src/backend/core/tests/documents/test_api_documents_duplicate.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,28 @@ def test_api_documents_duplicate_non_root_document(role):
293293
assert duplicated_accesses.count() == 0
294294
assert duplicated_document.is_sibling_of(child)
295295
assert duplicated_document.is_child_of(document)
296+
297+
298+
def test_api_documents_duplicate_reader_non_root_document():
299+
"""
300+
Reader users should be able to duplicate non-root documents but will be
301+
created as a root document.
302+
"""
303+
user = factories.UserFactory()
304+
client = APIClient()
305+
client.force_login(user)
306+
307+
document = factories.DocumentFactory(users=[(user, "reader")])
308+
child = factories.DocumentFactory(parent=document)
309+
310+
assert child.get_role(user) == "reader"
311+
312+
response = client.post(
313+
f"/api/v1.0/documents/{child.id!s}/duplicate/", format="json"
314+
)
315+
assert response.status_code == 201
316+
317+
duplicated_document = models.Document.objects.get(id=response.json()["id"])
318+
assert duplicated_document.is_root()
319+
assert duplicated_document.accesses.count() == 1
320+
assert duplicated_document.accesses.get(user=user).role == "owner"

0 commit comments

Comments
 (0)