#!/usr/bin/env python3 # -*- coding: utf-8 -*- from secretary import Renderer from lxml import etree from xml.dom.minidom import parseString from xml.parsers.expat import ExpatError, ErrorString from datetime import datetime import locale from num2words import num2words from PIL import Image import re from django.conf import settings def set_locale(): language_code = settings.LANGUAGE_CODE.split("-") language_code = language_code[0] + "_" + language_code[1].upper() for language_suffix in (".utf8", ""): try: locale.setlocale(locale.LC_TIME, language_code + language_suffix) break except locale.Error: pass RE_UNITS = re.compile("([.0-9]+)([a-z]+)") def parse_value_unit(value): m = RE_UNITS.match(value) if not m: return None, None value, unit = m.groups() value = float(value) return value, unit def float_format(value): """ 10350.5 -> 10 350,5 5 -> 5 5.449999 -> 5,45 """ if value is None or value == "": return "" try: value = float(value) except ValueError: return "" locale.setlocale(locale.LC_ALL, "fr_FR.UTF-8") if int(value) != value: value = int(value * 100)/100 return f"{value:n}" def euro_format(value): """ 15000 -> 15 000,00 € 5 -> 5,00 € """ value = float_format(value) if not value: return "" parts = value.split(",") if len(parts) < 2: return value + ",00 €" elif len(parts[1]) == 1: return value + "0 €" return value + " €" def number_to_words(value): if value is None or value == "": return "" try: value = float(value) except ValueError: return "" return num2words(value, lang=settings.LANGUAGE_CODE.split("-")[0]) def replace_line_breaks(value): return (value or "").replace("\r\n", "\n") def capfirst_filter(value): return value[0].upper() + value[1:] if value else value def lowerfirst_filter(value): return value[0].lower() + value[1:] if value else value def add_filter(value1, value2): try: return float(value1 or 0) + float(value2 or 0) except ValueError: return 0 def sub_filter(value1, value2): try: return float(value1 or 0) - float(value2 or 0) except ValueError: return 0 def multiply_filter(value1, value2): try: return float(value1 or 0) * float(value2 or 0) except ValueError: return 0 RE_CAP = re.compile(r"[^-' ]+") SEP = ("un", "une", "le", "la", "les", "lez", "d", "l", "de", "des", "du", "sur", "sous", "en") def capitalize_filter(value): if not value: return "" value = value.lower() res = "" for m in RE_CAP.finditer(value): start = m.start() if start: res += value[start - 1] v = m.group() if v not in SEP: v = v[0].upper() + v[1:] res += v return res def human_date_filter(value): try: value = datetime.strptime(value, "%Y-%m-%d") except (ValueError, TypeError) as __: return "" set_locale() return value.strftime(settings.DATE_FORMAT) def splitpart(value, index, index_end=None, char=",", merge_character=None): if index_end: try: index_end = int(index_end) if not merge_character: # merge is assumed merge_character = char except ValueError: # old filter use - manage compatibility merge_character = char char = index_end index_end = None if not value or (not index and index != 0): return "" if merge_character is True: # old filter use merge_character = char splited = value.split(char) if len(splited) <= index: return "" if not merge_character: return splited[index] if index_end: splited = splited[index:index_end] else: splited = splited[index:] return merge_character.join(splited) class IshtarSecretaryRenderer(Renderer): def __init__(self, *args, **kwargs): super(IshtarSecretaryRenderer, self).__init__(*args, **kwargs) self.media_callback = self.ishtar_media_loader self.media_path = settings.MEDIA_ROOT self.environment.filters["human_date"] = human_date_filter self.environment.filters["capfirst"] = capfirst_filter self.environment.filters["lowerfirst"] = lowerfirst_filter self.environment.filters["capitalize"] = capitalize_filter self.environment.filters["float_format"] = float_format self.environment.filters["euro_format"] = euro_format self.environment.filters["number_to_words"] = number_to_words self.environment.filters["replace_line_breaks"] = replace_line_breaks self.environment.filters["splitpart"] = splitpart self.environment.filters["multiply"] = multiply_filter self.environment.filters["add"] = add_filter self.environment.filters["sub"] = sub_filter def ishtar_media_loader(self, media, *args, **kwargs): res = self.fs_loader(media, *args, **kwargs) if not res or not res[0]: return image_file, mime = res if "width" in kwargs: kwargs["frame_attrs"]["svg:width"] = kwargs["width"] if "height" in kwargs: kwargs["frame_attrs"]["svg:height"] = kwargs["height"] if "keep_ratio" in args: image = Image.open(image_file.name) width, width_unit = parse_value_unit(kwargs["frame_attrs"]["svg:width"]) height, height_unit = parse_value_unit(kwargs["frame_attrs"]["svg:height"]) if "height" not in kwargs and width: new_height = width * image.height / image.width kwargs["frame_attrs"]["svg:height"] = "{}{}".format( new_height, width_unit ) if "width" not in kwargs and height: new_width = height * image.width / image.height kwargs["frame_attrs"]["svg:width"] = "{}{}".format( new_width, height_unit ) return image_file, mime def _render_xml(self, xml_document, **kwargs): # Prepare the xml object to be processed by jinja2 self.log.debug("Rendering XML object") template_string = "" try: self.template_images = dict() self._prepare_document_tags(xml_document) xml_source = xml_document.toxml() xml_source = xml_source.encode("ascii", "xmlcharrefreplace") jinja_template = self.environment.from_string( self._unescape_entities(xml_source.decode("utf-8")) ) result = jinja_template.render(**kwargs) # try to fix xml with mismatched tags parser = etree.XMLParser(recover=True) recovered_xml = etree.fromstring(result.encode("ascii", "xmlcharrefreplace"), parser) final_xml = parseString(etree.tostring(recovered_xml)) if self.template_images: self.replace_images(final_xml) return final_xml except ExpatError as e: if "result" not in locals(): result = xml_source ### changes try: near = result.split("\n")[e.lineno - 1][e.offset - 500 : e.offset + 500] except IndexError: near = "..." print(result) ### endchanges raise ExpatError( 'ExpatError "%s" at line %d, column %d\nNear of: "[...]%s[...]"' % (ErrorString(e.code), e.lineno, e.offset, near) ) except: self.log.error( "Error rendering template:\n%s", xml_document.toprettyxml(), exc_info=True, ) self.log.error("Unescaped template was:\n{0}".format(template_string)) raise finally: self.log.debug("Rendering xml object finished")