From 545e8b2a3c98d7e3c73ba08630ea9aa35adf9519 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 6 Jan 2025 16:20:57 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5(backend)=20remove=20code=20related?= =?UTF-8?q?=20to=20export=20pdf=20docx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The export is managed by the frontend, so we don't need the code related to the export in the backend side anymore. --- Dockerfile | 1 - src/backend/core/api/viewsets.py | 34 --- src/backend/core/models.py | 113 +--------- src/backend/core/static/reference.docx | Bin 7227 -> 0 bytes .../test_api_templates_generate_document.py | 208 ------------------ .../core/tests/test_models_templates.py | 32 --- src/backend/demo/data/template/code.txt | 12 +- src/backend/demo/data/template/css.txt | 20 -- src/backend/pyproject.toml | 3 - 9 files changed, 3 insertions(+), 420 deletions(-) delete mode 100644 src/backend/core/static/reference.docx delete mode 100644 src/backend/core/tests/templates/test_api_templates_generate_document.py 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 2192455df1a394ac05c4739b93f04abd2a6d6bc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7227 zcmaJ`1yo$ivPFWsyL)g5?(Q(SJA=CicM=HhHUWZ5g1ZL~4k5S&3mM#kg+JW)-%D=( zf463xv(BuU>eF4js=8`7KneC4E)*gnBGiKtsy@_jf&zIqbq6|nu(Lhgt7m!@VL373 zH~imPWcm9vX-iwxcfOj+kq-<>96N%~QI;<;SYHDmw*ho|$+>U#_GpS$NgulWN>j-Y z;40ffW5Ue8_vLSRT{m<@t2Eo1+I}dOKHIl=KdL&6ESvNrBy(KeWG|a0(tXzB%wUug zjj-|MEbL~R-4VwFIK+8I->EvRp^>@F`)$(CFL5AgLsnum27^JZrWF3sVx!Vf=}_Rg z(cn#LnG*K~aO(D$^2pz2KWV&IGf|065E)L%8&qVOi?eE^X@#!8@OUO&I!5~KD!a&T zYx%r)q);SbGFv7#Qvv%OVVp~2=bM9h9w+mY<3nCvz{`_EkiY8W zgFV-JMr1RrlQf+sE*#aFcwG!iR_Q*l@uYN^J6%yVR#7qr-E2y@T``loYUZSj8Ey%g z1H_#i#gSGR*BE2wxB#zc4^x^LZ=~B+G%t~7DYHm04n1V<8>dv0s;T5=xnJ3fMaC*X z!tz;%pc>kS5SdZJAp9L=A44^qads?*!LG7CvOmC8eJLf}&!wNT>bqc>T=m=~#b)!) zM-QP2cB}g5@Qh0FahN5c1{)x5O5;}0OS09~5BQ9+AAP~mnqtVhZu0b4uWgQzeqMHQ zg0~cH(EMT8cFi*JN83oew-X{h!lm8JvkDtJ1fR|+MrnDBh7q@W%9)~UG$ZV}QGM~z zBKLSCWui|ZB3jRlnamrP( zOkGHKQoQqD(UHcI3s}k`o2=^e^a8hUgbX6nWtd|Rn1>`+8Vo;h1+c%RKj)}Hw?fNH zZb4PE{fO6I;PCDRF~b!6n#2-tL9mn{LT}9igZIqon@}8=2tUR+sH_BwlmhSZKwU6- z@bt~bbU{@uXup@ELy!&qsJo#g&&94gICW2>&5zXX{ZU-Amj%53GB4Pc;7qmV#}AscNo9 zpou5zz7z_4%lR3%faJRgot7&V*deiOtYZ&UFxviAArTcuR9-E`6A-)21N%x4AQb)) z2%O)6u=MiqbaDC%i-}rfNVr69u9hM$E^8qq%{0XF3u#>^t~Eh>xD2br2Kv*49mPje zh^o$tJV;exE@IiYvNJP5v7(_fu(~`H^DFd@Y?zTpcu5>m~J@*=IkNtGA(W8{yK?PL)fcmavNSDZe?C$6<}YXa!tY zscPlveZ?Np^*01Hqq5)74HY(De<1m;zc6hgu40vu>#uFe5NO46z`+#vofdBYxpT4L zDDn+${NBnX59QsI)v{#BGCoqxz5OYLlV*BcU!!^{I!|Ej2IxKOZ9b!MB<)7%yL{OD z_6IBZyBXWjoq_?MFoH|we$OfNM85;z%4F6;u zZ>tKYImp^G{t@-36#18Rez!}p)^dCf2WH^0E@d}^cxp9ugOzEaGjn65{9;fDC#>H0 z97i19hg)~G>my_yTO}NQGMJk zI$g7JBTb%P3X!au(a}gSG#s)VrJ9T3Q#j*nqAZi^e7`xU;<(rSSxkv@z^hs7uz@x1 zGit6BO%dUD#nI2oDfE0VR?f$WcGE|Ti&wQrOe0BUUClka)hD>-8EIPhYA8*!J2r)Q z=6d>cx)Tnk59XB}lUHY|i>``!IxXSV8aBf2K280ge!kkJ_StaqXTtHp&mx~k*_tdQ z+D~0`iiQS=xwKl?mxnUL@>l$TNH+Ed`>40?$kJ1~5nr`|m1R^#VGj|w>D&@Jp)whK zFL4f>P*Bd&m|Z_k5RG!6d_)#Muf6b℘@?oP|hbzENoUK*;U@Fnf%Md=*NTNkQ{4 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 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", ]