diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2024-10-08 10:53:50 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2024-10-08 10:53:50 +0200 |
commit | 347a069fcd2274f5ae2c31dfde35b65fa492257a (patch) | |
tree | 0664cc84fc29c98af096dce8d0acc53a09a0e35e | |
parent | 2f526ba22f183ef744cfd7ac7284585f8b318d95 (diff) | |
download | Ishtar-347a069fcd2274f5ae2c31dfde35b65fa492257a.tar.bz2 Ishtar-347a069fcd2274f5ae2c31dfde35b65fa492257a.zip |
✨ document templates: manage export in PDF, HTML, xlsx, docx using LO unoconv
-rw-r--r-- | example_project/settings.py | 2 | ||||
-rw-r--r-- | ishtar_common/libreoffice.py | 30 | ||||
-rw-r--r-- | ishtar_common/models.py | 20 | ||||
-rw-r--r-- | ishtar_common/utils.py | 8 | ||||
-rw-r--r-- | ishtar_common/views.py | 19 | ||||
-rw-r--r-- | requirements.txt | 5 |
6 files changed, 70 insertions, 14 deletions
diff --git a/example_project/settings.py b/example_project/settings.py index 4fa80206c..cc2e44b3d 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -328,6 +328,8 @@ DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240 DOT_BINARY = "/usr/bin/dot" PDFTOPPM_BINARY = "" +UNOCONV_BINARY = "/usr/bin/unoconv" + TEST_RUNNER = "ishtar_common.tests.ManagedModelTestRunner" SELENIUM_TEST = False CELERY_BROKER_URL = "" diff --git a/ishtar_common/libreoffice.py b/ishtar_common/libreoffice.py index 3869ca81a..573d99361 100644 --- a/ishtar_common/libreoffice.py +++ b/ishtar_common/libreoffice.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time @@ -8,15 +8,37 @@ from com.sun.star.beans import PropertyValue from com.sun.star.connection import NoConnectException from com.sun.star.sheet.ValidationType import LIST +# nosec: filename used is generated and sanitized +import subprocess # nosec + #from com.sun.star.table import CellRangeAddress, CellAddress -from ishtar_common.utils import num2col +from ishtar_common.utils import num2col, sanitize_filepath from django.conf import settings RETRY = 5 +def get_connection(): + return "socket,host={},port={};urp".format( + settings.LIBREOFFICE_HOST, settings.LIBREOFFICE_PORT + ) + + +def convert_document(document, destination_format): + if not document.lower().endswith(".ods") and not document.lower().endswith(".odt"): + return document + document = sanitize_filepath(document) + connection = get_connection() + ";StarOffice.ComponentContext" + args = [settings.UNOCONV_BINARY, "--connection", connection, "-f", destination_format, + document] + # nosec: filename is generated and sanitized + popen = subprocess.Popen(args) # nosec + popen.wait(timeout=20) + dest_document = document[:-4] + "." + destination_format + return dest_document + class UnoClient: def __init__(self): @@ -30,9 +52,7 @@ class UnoClient: resolver = local_context.ServiceManager.createInstanceWithContext( "com.sun.star.bridge.UnoUrlResolver", local_context) - connection = "socket,host={},port={};urp".format( - settings.LIBREOFFICE_HOST, settings.LIBREOFFICE_PORT - ) + connection = get_connection() try: self.service_manager = resolver.resolve( "uno:{};StarOffice.ServiceManager".format(connection)) diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 73f140625..c7fe7be8a 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -2057,11 +2057,23 @@ class Dashboard(object): return v -EXPORT_FORMATS = [] +EXPORT_FORMATS = [("", "---")] +EXPORT_FORMATS_CONTENT_TYPE = { + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "pdf": "application/pdf", + "html": "text/html", + "odt": "application/vnd.oasis.opendocument.text", + "ods": "application/vnd.oasis.opendocument.spreadsheet", +} if settings.USE_LIBREOFFICE: - EXPORT_FORMATS.append(("xlsx", _("XLSX"))) - EXPORT_FORMATS.append(("pdf", _("PDF"))) + EXPORT_FORMATS = [ + ("docx", _("DOCX")), + ("html", _("HTML")), + ("pdf", _("PDF")), + ("xlsx", _("XLSX")), + ] EXPORT_FORMATS_DICT = dict(EXPORT_FORMATS) @@ -2124,7 +2136,7 @@ class DocumentTemplate(models.Model): def clean(self): if self.for_labels and not self.label_per_page: raise ValidationError( - _("For label template, you must provide " "number of label per page.") + _("For label template, you must provide number of label per page.") ) def generate_label_template(self): diff --git a/ishtar_common/utils.py b/ishtar_common/utils.py index bca7ce181..d4453ff5a 100644 --- a/ishtar_common/utils.py +++ b/ishtar_common/utils.py @@ -1446,6 +1446,14 @@ def clean_session_cache(session): cache.set(cache_key_list, [], settings.CACHE_TIMEOUT) +def sanitize_filepath(filepath): + #TODO: python3-pathvalidate + # from pathvalidate import sanitize_filepath + # filepath -> sanitize_filepath(filepath) + keep_characters = (" ", ".", "_", "-", "/") + return "".join(c for c in filepath if c.isalnum() or c in keep_characters).rstrip() + + def get_field_labels_from_path(model, path): """ :param model: base model diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 5b675eb52..a6c3b373d 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -91,6 +91,10 @@ from ishtar_common.utils import ( from ishtar_common.widgets import JQueryAutoComplete from ishtar_common import tasks +convert_document = None +if settings.USE_LIBREOFFICE: + from ishtar_common.libreoffice import convert_document + from .views_item import ( CURRENT_ITEM_KEYS, CURRENT_ITEM_KEYS_DICT, @@ -1535,10 +1539,19 @@ class GenerateView(IshtarMixin, LoginRequiredMixin, View): document = self.publish(tpl, objects) if not document: return HttpResponse(content_type="text/plain") + base_extension = tpl.template.name.split(".")[-1].lower() + extension = tpl.export_format + if not extension: + extension = base_extension + if not settings.USE_LIBREOFFICE and extension not in ("ods", "odt"): + return HttpResponseBadRequest("Invalid format type - need LibreOffice daemon") # bad configuration + content_type = models.EXPORT_FORMATS_CONTENT_TYPE.get( + extension, "application/vnd.oasis.opendocument.text" + ) + if tpl.export_format and convert_document: + document = convert_document(document, tpl.export_format) with open(document, "rb") as f: - response = HttpResponse( - f.read(), content_type="application/vnd.oasis.opendocument.text" - ) + response = HttpResponse(f.read(), content_type=content_type) response["Content-Disposition"] = "attachment; filename={}".format( document.split(os.sep)[-1] ) diff --git a/requirements.txt b/requirements.txt index 2e666c9de..d5344e5b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,8 @@ django-extensions==3.0.3 pandas==1.1.5 django-ipware==3.0.0 - django-axes==5.4.3 +num2words==0.5.9 + -num2words==0.5.9
\ No newline at end of file +# TODO v5: python3-pathvalidate -> libreoffice.py
\ No newline at end of file |