✨(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.

Signed-off-by: Gustavo Trott <gustavo@trott.com.br>
This commit is contained in:
Gustavo Trott
2026-05-11 16:00:40 +02:00
committed by Manuel Raynaud
parent fa3beca494
commit 1d28732842
3 changed files with 157 additions and 4 deletions
+1
View File
@@ -8,6 +8,7 @@ and this project adheres to
### Added
- ✨(backend) support creating subdoc from file #1987
- ⚡️(frontend) add skeleton on content loading #2254
- ⚡️(frontend) close websocket connection when user change tab #2264
+14 -4
View File
@@ -676,10 +676,13 @@ class DocumentViewSet(
return drf.response.Response(serializer.data)
def perform_create(self, serializer):
"""Set the current user as creator and owner of the newly created object."""
# Remove file from validated_data as it's not a model field
# Process it if present
def _apply_uploaded_file_conversion(self, serializer):
"""
Check if a file has been uploaded with a doc or a children is created.
If a file is present and the conversion upload enabled, the file is converted
using the converter service and the validated_data in the serializer are filled
with the converted file and the file name.
"""
uploaded_file = serializer.validated_data.pop("file", None)
if uploaded_file and not settings.CONVERSION_UPLOAD_ENABLED:
@@ -707,6 +710,11 @@ class DocumentViewSet(
{"file": ["Could not convert file content"]}
) from err
def perform_create(self, serializer):
"""Set the current user as creator and owner of the newly created object."""
self._apply_uploaded_file_conversion(serializer)
obj = create_tree_node_with_retry(
lambda: models.Document.add_root(
creator=self.request.user,
@@ -1016,6 +1024,8 @@ class DocumentViewSet(
)
serializer.is_valid(raise_exception=True)
self._apply_uploaded_file_conversion(serializer)
child_document = create_tree_node_with_retry(
lambda: document.add_child(
creator=request.user,
@@ -3,6 +3,8 @@ Tests for Documents API endpoint in impress's core app: children create
"""
from concurrent.futures import ThreadPoolExecutor
from io import BytesIO
from unittest.mock import patch
from uuid import uuid4
import pytest
@@ -10,6 +12,7 @@ from rest_framework.test import APIClient
from core import factories
from core.models import Document, LinkReachChoices, LinkRoleChoices
from core.services import mime_types
pytestmark = pytest.mark.django_db
@@ -292,3 +295,142 @@ def test_api_documents_create_document_children_race_condition():
document.refresh_from_db()
assert document.numchild == 2
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_children_create_with_docx_file_success(mock_convert, settings):
"""
Authenticated users should be able to create children document by uploading a DOCX file.
The file should be converted to YJS format and the title should be set from filename.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
settings.CONVERSION_UPLOAD_ENABLED = True
# Mock the conversion
converted_yjs = "base64encodedyjscontent"
mock_convert.return_value = converted_yjs
# Create a fake DOCX file
file_content = b"fake docx content"
file = BytesIO(file_content)
file.name = "My Important Document.docx"
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 201
assert Document.objects.count() == 2
children = Document.objects.get(pk=response.json()["id"])
assert children.title == "My Important Document.docx"
assert children.content == converted_yjs
# Verify the converter was called correctly
mock_convert.assert_called_once_with(
file_content,
content_type=mime_types.DOCX,
accept=mime_types.YJS,
)
@patch("core.services.converter_services.Converter.convert")
def test_api_documents_children_create_with_docx_file_disabled(mock_convert, settings):
"""
When conversion is not enabled, uploading a file should have no effect
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
settings.CONVERSION_UPLOAD_ENABLED = False
# Create a fake DOCX file
file_content = b"fake docx content"
file = BytesIO(file_content)
file.name = "My Important Document.docx"
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["file upload is not allowed"]}
# Verify the converter was not called
mock_convert.assert_not_called()
def test_api_documents_children_create_with_file_max_size_exceeded(settings):
"""
The uploaded file should not exceed the maximum size in settings.
"""
settings.CONVERSION_FILE_MAX_SIZE = 1 # 1 byte for test
settings.CONVERSION_UPLOAD_ENABLED = True
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
file = BytesIO(b"a" * (10))
file.name = "test.docx"
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {"file": ["File size exceeds the maximum limit of 0 MB."]}
def test_api_documents_children_create_with_file_extension_not_allowed(settings):
"""
The uploaded file should not have an allowed extension.
"""
settings.CONVERSION_FILE_EXTENSIONS_ALLOWED = [".docx"]
settings.CONVERSION_UPLOAD_ENABLED = True
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
file = BytesIO(b"fake docx content")
file.name = "test.md"
parent = factories.DocumentFactory(creator=user, users=[(user, "owner")])
response = client.post(
f"/api/v1.0/documents/{parent.id}/children/",
{
"file": file,
},
format="multipart",
)
assert response.status_code == 400
assert response.json() == {
"file": [
"File extension .md is not allowed. Allowed extensions are: ['.docx']."
]
}