diff --git a/Dockerfile b/Dockerfile index 6547f0b6..60f464ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,7 +72,6 @@ RUN apk add \ gettext \ gdk-pixbuf \ libffi-dev \ - pandoc \ pango \ shared-mime-info diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 10adee35..5f2be8d8 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -939,40 +939,6 @@ class TemplateViewSet( role=models.RoleChoices.OWNER, ) - @drf.decorators.action( - detail=True, - methods=["post"], - url_path="generate-document", - permission_classes=[permissions.AccessPermission], - ) - # pylint: disable=unused-argument - def generate_document(self, request, pk=None): - """ - Generate and return a document for this template around the - body passed as argument. - - 2 types of body are accepted: - - HTML: body_type = "html" - - Markdown: body_type = "markdown" - - 2 types of documents can be generated: - - PDF: format = "pdf" - - Docx: format = "docx" - """ - serializer = serializers.DocumentGenerationSerializer(data=request.data) - - if not serializer.is_valid(): - return drf.response.Response( - serializer.errors, status=drf.status.HTTP_400_BAD_REQUEST - ) - - body = serializer.validated_data["body"] - body_type = serializer.validated_data["body_type"] - export_format = serializer.validated_data["format"] - - template = self.get_object() - return template.generate_document(body, body_type, export_format) - class TemplateAccessViewSet( ResourceAccessViewsetMixin, diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 52e66aab..1dc85739 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -5,11 +5,8 @@ Declare and configure the models for the impress core application import hashlib import smtplib -import tempfile -import textwrap import uuid from datetime import timedelta -from io import BytesIO from logging import getLogger from django.conf import settings @@ -21,19 +18,12 @@ from django.core.files.base import ContentFile from django.core.files.storage import default_storage from django.core.mail import send_mail from django.db import models -from django.http import FileResponse -from django.template.base import Template as DjangoTemplate -from django.template.context import Context from django.template.loader import render_to_string -from django.utils import html, timezone +from django.utils import timezone from django.utils.functional import cached_property, lazy from django.utils.translation import get_language, override from django.utils.translation import gettext_lazy as _ -import frontmatter -import markdown -import pypandoc -import weasyprint from botocore.exceptions import ClientError from timezone_field import TimeZoneField @@ -794,107 +784,6 @@ class Template(BaseModel): "retrieve": can_get, } - def generate_pdf(self, body_html, metadata): - """ - Generate and return a pdf document wrapped around the current template - """ - document_html = weasyprint.HTML( - string=DjangoTemplate(self.code).render( - Context({"body": html.format_html(body_html), **metadata}) - ) - ) - css = weasyprint.CSS( - string=self.css, - font_config=weasyprint.text.fonts.FontConfiguration(), - ) - - pdf_content = document_html.write_pdf(stylesheets=[css], zoom=1) - response = FileResponse(BytesIO(pdf_content), content_type="application/pdf") - response["Content-Disposition"] = f"attachment; filename={self.title}.pdf" - - return response - - def generate_word(self, body_html, metadata): - """ - Generate and return a docx document wrapped around the current template - """ - template_string = DjangoTemplate(self.code).render( - Context({"body": html.format_html(body_html), **metadata}) - ) - - html_string = f""" - - - - - - - {template_string} - - - """ - - reference_docx = "core/static/reference.docx" - output = BytesIO() - - # Convert the HTML to a temporary docx file - with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file: - output_path = tmp_file.name - - pypandoc.convert_text( - html_string, - "docx", - format="html", - outputfile=output_path, - extra_args=["--reference-doc", reference_docx], - ) - - # Create a BytesIO object to store the output of the temporary docx file - with open(output_path, "rb") as f: - output = BytesIO(f.read()) - - # Ensure the pointer is at the beginning - output.seek(0) - - response = FileResponse( - output, - content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ) - response["Content-Disposition"] = f"attachment; filename={self.title}.docx" - - return response - - def generate_document(self, body, body_type, export_format): - """ - Generate and return a document for this template around the - body passed as argument. - - 2 types of body are accepted: - - HTML: body_type = "html" - - Markdown: body_type = "markdown" - - 2 types of documents can be generated: - - PDF: export_format = "pdf" - - Docx: export_format = "docx" - """ - document = frontmatter.loads(body) - metadata = document.metadata - strip_body = document.content.strip() - - if body_type == "html": - body_html = strip_body - else: - body_html = ( - markdown.markdown(textwrap.dedent(strip_body)) if strip_body else "" - ) - - if export_format == "pdf": - return self.generate_pdf(body_html, metadata) - - return self.generate_word(body_html, metadata) - class TemplateAccess(BaseAccess): """Relation model to give access to a template for a user or a team with a role.""" diff --git a/src/backend/core/static/reference.docx b/src/backend/core/static/reference.docx deleted file mode 100644 index 2192455d..00000000 Binary files a/src/backend/core/static/reference.docx and /dev/null differ diff --git a/src/backend/core/tests/templates/test_api_templates_generate_document.py b/src/backend/core/tests/templates/test_api_templates_generate_document.py deleted file mode 100644 index 0ed8f4d3..00000000 --- a/src/backend/core/tests/templates/test_api_templates_generate_document.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -Test users API endpoints in the impress core app. -""" - -import pytest -from rest_framework.test import APIClient - -from core import factories -from core.tests.conftest import TEAM, USER, VIA - -pytestmark = pytest.mark.django_db - - -def test_api_templates_generate_document_anonymous_public(): - """Anonymous users can generate pdf document with public templates.""" - template = factories.TemplateFactory(is_public=True) - data = { - "body": "# Test markdown body", - } - - response = APIClient().post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "application/pdf" - - -def test_api_templates_generate_document_anonymous_not_public(): - """ - Anonymous users should not be allowed to generate pdf document with templates - that are not marked as public. - """ - template = factories.TemplateFactory(is_public=False) - data = { - "body": "# Test markdown body", - } - - response = APIClient().post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 401 - assert response.json() == { - "detail": "Authentication credentials were not provided." - } - - -def test_api_templates_generate_document_authenticated_public(): - """Authenticated users can generate pdf document with public templates.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=True) - data = {"body": "# Test markdown body"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "application/pdf" - - -def test_api_templates_generate_document_authenticated_not_public(): - """ - Authenticated users should not be allowed to generate pdf document with templates - that are not marked as public. - """ - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=False) - data = {"body": "# Test markdown body"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 403 - assert response.json() == { - "detail": "You do not have permission to perform this action." - } - - -@pytest.mark.parametrize("via", VIA) -def test_api_templates_generate_document_related(via, mock_user_teams): - """Users related to a template can generate pdf document.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - access = None - if via == USER: - access = factories.UserTemplateAccessFactory(user=user) - elif via == TEAM: - mock_user_teams.return_value = ["lasuite", "unknown"] - access = factories.TeamTemplateAccessFactory(team="lasuite") - - data = {"body": "# Test markdown body"} - - response = client.post( - f"/api/v1.0/templates/{access.template_id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "application/pdf" - - -def test_api_templates_generate_document_type_html(): - """Generate pdf document with the body type html.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=True) - data = {"body": "

Test body

", "body_type": "html"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "application/pdf" - - -def test_api_templates_generate_document_type_markdown(): - """Generate pdf document with the body type markdown.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=True) - data = {"body": "# Test markdown body", "body_type": "markdown"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert response.headers["content-type"] == "application/pdf" - - -def test_api_templates_generate_document_type_unknown(): - """Generate pdf document with the body type unknown.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=True) - data = {"body": "# Test markdown body", "body_type": "unknown"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 400 - assert response.json() == { - "body_type": [ - '"unknown" is not a valid choice.', - ] - } - - -def test_api_templates_generate_document_export_docx(): - """Generate pdf document with the body type html.""" - user = factories.UserFactory() - - client = APIClient() - client.force_login(user) - - template = factories.TemplateFactory(is_public=True) - data = {"body": "

Test body

", "body_type": "html", "format": "docx"} - - response = client.post( - f"/api/v1.0/templates/{template.id!s}/generate-document/", - data, - format="json", - ) - - assert response.status_code == 200 - assert ( - response.headers["content-type"] - == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ) diff --git a/src/backend/core/tests/test_models_templates.py b/src/backend/core/tests/test_models_templates.py index 6e7cba2c..95f8fbde 100644 --- a/src/backend/core/tests/test_models_templates.py +++ b/src/backend/core/tests/test_models_templates.py @@ -2,10 +2,6 @@ Unit tests for the Template model """ -import os -import time -from unittest import mock - from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ValidationError @@ -189,31 +185,3 @@ def test_models_templates_get_abilities_preset_role(django_assert_num_queries): "partial_update": False, "generate_document": True, } - - -def test_models_templates__generate_word(): - """Generate word document and assert no tmp files are left in /tmp folder.""" - template = factories.TemplateFactory() - response = template.generate_word("

Test body

", {}) - - assert response.status_code == 200 - assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0 - - -@mock.patch( - "pypandoc.convert_text", - side_effect=RuntimeError("Conversion failed"), -) -def test_models_templates__generate_word__raise_error(_mock_pypandoc): - """ - Generate word document and assert no tmp files are left in /tmp folder - even when the conversion fails. - """ - template = factories.TemplateFactory() - - try: - template.generate_word("

Test body

", {}) - except RuntimeError as e: - assert str(e) == "Conversion failed" - time.sleep(0.5) - assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0 diff --git a/src/backend/demo/data/template/code.txt b/src/backend/demo/data/template/code.txt index 0ab83f60..56f6736b 100644 --- a/src/backend/demo/data/template/code.txt +++ b/src/backend/demo/data/template/code.txt @@ -1,10 +1,2 @@ - -
- -
-
-
{{ body }}
-
-
+ +
\ No newline at end of file diff --git a/src/backend/demo/data/template/css.txt b/src/backend/demo/data/template/css.txt index 79a440ab..e69de29b 100644 --- a/src/backend/demo/data/template/css.txt +++ b/src/backend/demo/data/template/css.txt @@ -1,20 +0,0 @@ -body { - background: white; - font-family: arial; -} -.header img { - width: 5cm; - margin-left: -0.4cm; -} -.body{ - margin-top: 1.5rem; -} -img { - max-width: 100%; -} -[custom-style="center"] { - text-align: center; -} -[custom-style="right"] { - text-align: right; -} diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 4ce29fa6..286f07b7 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -50,13 +50,10 @@ dependencies = [ "openai==1.58.1", "psycopg[binary]==3.2.3", "PyJWT==2.10.1", - "pypandoc==1.14", - "python-frontmatter==1.1.0", "python-magic==0.4.27", "requests==2.32.3", "sentry-sdk==2.19.2", "url-normalize==1.4.3", - "WeasyPrint>=60.2", "whitenoise==6.8.2", "mozilla-django-oidc==4.0.1", ]