Skip to content

Commit cd75a17

Browse files
authored
🐛(backend) support creating subdoc from file
The children/ endpoint was missing file upload support that the root documents endpoint already had. Added file-to-YJS conversion handling to subdocument creation.
1 parent db46740 commit cd75a17

3 files changed

Lines changed: 161 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- ✨(backend) support creating subdoc from file #1987
12+
13+
914
## [v5.1.0] - 2026-05-11
1015

1116
### Added

src/backend/core/api/viewsets.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,13 @@ def retrieve(self, request, *args, **kwargs):
676676

677677
return drf.response.Response(serializer.data)
678678

679-
def perform_create(self, serializer):
680-
"""Set the current user as creator and owner of the newly created object."""
681-
# Remove file from validated_data as it's not a model field
682-
# Process it if present
679+
def _apply_uploaded_file_conversion(self, serializer):
680+
"""
681+
Check if a file has been uploaded with a doc or a children is created.
682+
If a file is present and the conversion upload enabled, the file is converted
683+
using the converter service and the validated_data in the serializer are filled
684+
with the converted file and the file name.
685+
"""
683686
uploaded_file = serializer.validated_data.pop("file", None)
684687

685688
if uploaded_file and not settings.CONVERSION_UPLOAD_ENABLED:
@@ -707,6 +710,11 @@ def perform_create(self, serializer):
707710
{"file": ["Could not convert file content"]}
708711
) from err
709712

713+
def perform_create(self, serializer):
714+
"""Set the current user as creator and owner of the newly created object."""
715+
716+
self._apply_uploaded_file_conversion(serializer)
717+
710718
obj = create_tree_node_with_retry(
711719
lambda: models.Document.add_root(
712720
creator=self.request.user,
@@ -1016,6 +1024,8 @@ def children(self, request, *args, **kwargs):
10161024
)
10171025
serializer.is_valid(raise_exception=True)
10181026

1027+
self._apply_uploaded_file_conversion(serializer)
1028+
10191029
child_document = create_tree_node_with_retry(
10201030
lambda: document.add_child(
10211031
creator=request.user,

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
"""
44

55
from concurrent.futures import ThreadPoolExecutor
6+
from io import BytesIO
7+
from unittest.mock import patch
68
from uuid import uuid4
79

810
import pytest
911
from rest_framework.test import APIClient
1012

1113
from core import factories
1214
from core.models import Document, LinkReachChoices, LinkRoleChoices
15+
from core.services import mime_types
1316

1417
pytestmark = pytest.mark.django_db
1518

@@ -292,3 +295,142 @@ def create_document():
292295

293296
document.refresh_from_db()
294297
assert document.numchild == 2
298+
299+
300+
@patch("core.services.converter_services.Converter.convert")
301+
def test_api_documents_children_create_with_docx_file_success(mock_convert, settings):
302+
"""
303+
Authenticated users should be able to create children document by uploading a DOCX file.
304+
The file should be converted to YJS format and the title should be set from filename.
305+
"""
306+
user = factories.UserFactory()
307+
client = APIClient()
308+
client.force_login(user)
309+
310+
settings.CONVERSION_UPLOAD_ENABLED = True
311+
312+
# Mock the conversion
313+
converted_yjs = "base64encodedyjscontent"
314+
mock_convert.return_value = converted_yjs
315+
316+
# Create a fake DOCX file
317+
file_content = b"fake docx content"
318+
file = BytesIO(file_content)
319+
file.name = "My Important Document.docx"
320+
321+
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
322+
323+
response = client.post(
324+
f"/api/v1.0/documents/{parent.id}/children/",
325+
{
326+
"file": file,
327+
},
328+
format="multipart",
329+
)
330+
331+
assert response.status_code == 201
332+
assert Document.objects.count() == 2
333+
children = Document.objects.get(pk=response.json()["id"])
334+
assert children.title == "My Important Document.docx"
335+
assert children.content == converted_yjs
336+
337+
# Verify the converter was called correctly
338+
mock_convert.assert_called_once_with(
339+
file_content,
340+
content_type=mime_types.DOCX,
341+
accept=mime_types.YJS,
342+
)
343+
344+
345+
@patch("core.services.converter_services.Converter.convert")
346+
def test_api_documents_children_create_with_docx_file_disabled(mock_convert, settings):
347+
"""
348+
When conversion is not enabled, uploading a file should have no effect
349+
"""
350+
user = factories.UserFactory()
351+
client = APIClient()
352+
client.force_login(user)
353+
354+
settings.CONVERSION_UPLOAD_ENABLED = False
355+
356+
# Create a fake DOCX file
357+
file_content = b"fake docx content"
358+
file = BytesIO(file_content)
359+
file.name = "My Important Document.docx"
360+
361+
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
362+
363+
response = client.post(
364+
f"/api/v1.0/documents/{parent.id}/children/",
365+
{
366+
"file": file,
367+
},
368+
format="multipart",
369+
)
370+
371+
assert response.status_code == 400
372+
assert response.json() == {"file": ["file upload is not allowed"]}
373+
374+
# Verify the converter was not called
375+
mock_convert.assert_not_called()
376+
377+
378+
def test_api_documents_children_create_with_file_max_size_exceeded(settings):
379+
"""
380+
The uploaded file should not exceed the maximum size in settings.
381+
"""
382+
settings.CONVERSION_FILE_MAX_SIZE = 1 # 1 byte for test
383+
settings.CONVERSION_UPLOAD_ENABLED = True
384+
385+
user = factories.UserFactory()
386+
client = APIClient()
387+
client.force_login(user)
388+
389+
file = BytesIO(b"a" * (10))
390+
file.name = "test.docx"
391+
392+
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
393+
394+
response = client.post(
395+
f"/api/v1.0/documents/{parent.id}/children/",
396+
{
397+
"file": file,
398+
},
399+
format="multipart",
400+
)
401+
402+
assert response.status_code == 400
403+
404+
assert response.json() == {"file": ["File size exceeds the maximum limit of 0 MB."]}
405+
406+
407+
def test_api_documents_children_create_with_file_extension_not_allowed(settings):
408+
"""
409+
The uploaded file should not have an allowed extension.
410+
"""
411+
settings.CONVERSION_FILE_EXTENSIONS_ALLOWED = [".docx"]
412+
settings.CONVERSION_UPLOAD_ENABLED = True
413+
414+
user = factories.UserFactory()
415+
client = APIClient()
416+
client.force_login(user)
417+
418+
file = BytesIO(b"fake docx content")
419+
file.name = "test.md"
420+
421+
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
422+
423+
response = client.post(
424+
f"/api/v1.0/documents/{parent.id}/children/",
425+
{
426+
"file": file,
427+
},
428+
format="multipart",
429+
)
430+
431+
assert response.status_code == 400
432+
assert response.json() == {
433+
"file": [
434+
"File extension .md is not allowed. Allowed extensions are: ['.docx']."
435+
]
436+
}

0 commit comments

Comments
 (0)