changeset 1bd2d71d8f63 in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=1bd2d71d8f63
description:
        Support MsgDirective and cache template for report

        We add the Translator directives to the template so we can use the i18n
        extension on HTML report.
        We also cache in memory the parsed template and avoid to write it in 
temp
        directory for the loader.

        issue9511
        issue6489
        review321891002
diffstat:

 CHANGELOG                             |    2 +
 trytond/ir/action.py                  |   10 ++-
 trytond/ir/translation.py             |   28 ++++++++
 trytond/report/report.py              |  106 ++++++++++-----------------------
 trytond/res/email_reset_password.html |   14 ++-
 5 files changed, 82 insertions(+), 78 deletions(-)

diffs (295 lines):

diff -r 97dba6118e44 -r 1bd2d71d8f63 CHANGELOG
--- a/CHANGELOG Sat Sep 12 10:00:03 2020 +0200
+++ b/CHANGELOG Sat Sep 12 16:35:52 2020 +0200
@@ -1,3 +1,5 @@
+* Cache in memory the report template instances
+* Support Genshi's MsgDirective in report
 * Support PYSON comparison of date and datetime
 * Add cron task to clean old queue tasks
 * Add option to run cron once
diff -r 97dba6118e44 -r 1bd2d71d8f63 trytond/ir/action.py
--- a/trytond/ir/action.py      Sat Sep 12 10:00:03 2020 +0200
+++ b/trytond/ir/action.py      Sat Sep 12 16:35:52 2020 +0200
@@ -7,7 +7,7 @@
 
 from sql import Null
 
-from trytond.cache import Cache
+from trytond.cache import Cache, MemoryCache
 from trytond.config import config
 from trytond.i18n import gettext
 from trytond.model import (
@@ -544,6 +544,7 @@
         help='Python dictonary where keys define "to" "cc" "subject"\n'
         "Example: {'to': '[email protected]', 'cc': '[email protected]'}")
     pyson_email = fields.Function(fields.Char('PySON Email'), 'get_pyson')
+    _template_cache = MemoryCache('ir.action.report.template', context=False)
 
     @classmethod
     def __register__(cls, module_name):
@@ -706,8 +707,15 @@
                 args.extend((reports, values))
             reports, values = args[:2]
             args = args[2:]
+        cls._template_cache.clear()
         super(ActionReport, cls).write(reports, values, *args)
 
+    def get_template_cached(self):
+        return self._template_cache.get(self.id)
+
+    def set_template_cached(self, template):
+        self._template_cache.set(self.id, template)
+
 
 class ActionActWindow(ActionMixin, ModelSQL, ModelView):
     "Action act window"
diff -r 97dba6118e44 -r 1bd2d71d8f63 trytond/ir/translation.py
--- a/trytond/ir/translation.py Sat Sep 12 10:00:03 2020 +0200
+++ b/trytond/ir/translation.py Sat Sep 12 16:35:52 2020 +0200
@@ -72,6 +72,8 @@
     overriding_module = fields.Char('Overriding Module', readonly=True)
     _translation_cache = Cache('ir.translation', size_limit=10240,
         context=False)
+    _translation_report_cache = Cache(
+        'ir.translation.get_report', context=False)
     _get_language_cache = Cache('ir.translation.get_language')
 
     @classmethod
@@ -573,6 +575,29 @@
         return res
 
     @classmethod
+    def get_report(cls, report_name, text):
+        language = Transaction().language
+        key = (report_name, language)
+        if cls._translation_report_cache.get(key) is None:
+            cache = {}
+            code = language
+            while code:
+                translations = cls.search([
+                        ('lang', '=', code),
+                        ('type', '=', 'report'),
+                        ('name', '=', report_name),
+                        ('value', '!=', ''),
+                        ('value', '!=', None),
+                        ('fuzzy', '=', False),
+                        ('res_id', '=', -1),
+                        ], order=[('module', 'DESC')])
+                for translation in translations:
+                    cache.setdefault(translation.src, translation.value)
+                code = get_parent(code)
+            cls._translation_report_cache.set(key, cache)
+        return cls._translation_report_cache.get(key, {}).get(text, text)
+
+    @classmethod
     def delete(cls, translations):
         pool = Pool()
         Message = pool.get('ir.message')
@@ -582,6 +607,7 @@
         Model._get_name_cache.clear()
         ModelField._get_name_cache.clear()
         cls._translation_cache.clear()
+        cls._translation_report_cache.clear()
         ModelView._fields_view_get_cache.clear()
         return super(Translation, cls).delete(translations)
 
@@ -595,6 +621,7 @@
         Model._get_name_cache.clear()
         ModelField._get_name_cache.clear()
         cls._translation_cache.clear()
+        cls._translation_report_cache.clear()
         ModelView._fields_view_get_cache.clear()
         vlist = [x.copy() for x in vlist]
 
@@ -614,6 +641,7 @@
         Model._get_name_cache.clear()
         ModelField._get_name_cache.clear()
         cls._translation_cache.clear()
+        cls._translation_report_cache.clear()
         ModelView._fields_view_get_cache.clear()
         return super(Translation, cls).write(*args)
 
diff -r 97dba6118e44 -r 1bd2d71d8f63 trytond/report/report.py
--- a/trytond/report/report.py  Sat Sep 12 10:00:03 2020 +0200
+++ b/trytond/report/report.py  Sat Sep 12 16:35:52 2020 +0200
@@ -98,42 +98,12 @@
 
 class TranslateFactory:
 
-    def __init__(self, report_name, language, translation):
+    def __init__(self, report_name, translation):
         self.report_name = report_name
-        self.language = language
         self.translation = translation
-        self.cache = {}
 
     def __call__(self, text):
-        from trytond.ir.lang import get_parent_language
-        if self.language not in self.cache:
-            cache = self.cache[self.language] = {}
-            code = self.language
-            while code:
-                # Order to get empty module/custom report first
-                translations = self.translation.search([
-                    ('lang', '=', code),
-                    ('type', '=', 'report'),
-                    ('name', '=', self.report_name),
-                    ('value', '!=', ''),
-                    ('value', '!=', None),
-                    ('fuzzy', '=', False),
-                    ('res_id', '=', -1),
-                    ], order=[('module', 'DESC')])
-                for translation in translations:
-                    cache.setdefault(translation.src, translation.value)
-                code = get_parent_language(code)
-        return self.cache[self.language].get(text, text)
-
-    def set_language(self, language=None):
-        pool = Pool()
-        Config = pool.get('ir.configuration')
-        Lang = pool.get('ir.lang')
-        if isinstance(language, Lang):
-            language = language.code
-        if not language:
-            language = Config.get_language()
-        self.language = language
+        return self.translation.get_report(self.report_name, text)
 
 
 class Report(URLMixin, PoolBase):
@@ -208,8 +178,11 @@
 
     @classmethod
     def _execute(cls, records, data, action):
-        report_context = cls.get_context(records, data)
-        return cls.convert(action, cls.render(action, report_context))
+        # Ensure to restore original context
+        # set_lang may modify it
+        with Transaction().set_context(Transaction().context):
+            report_context = cls.get_context(records, data)
+            return cls.convert(action, cls.render(action, report_context))
 
     @classmethod
     def _get_records(cls, ids, model, data):
@@ -257,6 +230,7 @@
     def get_context(cls, records, data):
         pool = Pool()
         User = pool.get('res.user')
+        Lang = pool.get('ir.lang')
 
         report_context = {}
         report_context['data'] = data
@@ -270,53 +244,41 @@
         report_context['format_number'] = cls.format_number
         report_context['datetime'] = datetime
 
+        def set_lang(language=None):
+            if isinstance(language, Lang):
+                language = language.code
+            Transaction().set_context(language=language)
+        report_context['set_lang'] = set_lang
+
         return report_context
 
     @classmethod
-    def _prepare_template_file(cls, report):
-        # Convert to str as value from DB is not supported by StringIO
-        report_content = (bytes(report.report_content) if report.report_content
-            else None)
-        if not report_content:
-            raise Exception('Error', 'Missing report file!')
-
-        fd, path = tempfile.mkstemp(
-            suffix=(os.extsep + report.template_extension),
-            prefix='trytond_')
-        with open(path, 'wb') as f:
-            f.write(report_content)
-        return fd, path
-
-    @classmethod
-    def _add_translation_hook(cls, relatorio_report, context):
-        pool = Pool()
-        Translation = pool.get('ir.translation')
-
-        translate = TranslateFactory(cls.__name__, Transaction().language,
-            Translation)
-        context['set_lang'] = lambda language: translate.set_language(language)
-        translator = Translator(lambda text: translate(text))
-        relatorio_report.filters.insert(0, translator)
+    def _callback_loader(cls, report, template):
+        if report.translatable:
+            pool = Pool()
+            Translation = pool.get('ir.translation')
+            translate = TranslateFactory(cls.__name__, Translation)
+            translator = Translator(lambda text: translate(text))
+            # Do not use Translator.setup to add filter at the end
+            # after set_lang evaluation
+            template.filters.append(translator)
+            if hasattr(template, 'add_directives'):
+                template.add_directives(Translator.NAMESPACE, translator)
 
     @classmethod
     def render(cls, report, report_context):
         "calls the underlying templating engine to renders the report"
-        fd, path = cls._prepare_template_file(report)
-
-        mimetype = MIMETYPES[report.template_extension]
-        rel_report = relatorio.reporting.Report(path, mimetype,
-                ReportFactory(), relatorio.reporting.MIMETemplateLoader())
-        if report.translatable:
-            cls._add_translation_hook(rel_report, report_context)
-        else:
-            report_context['set_lang'] = lambda language: None
-
-        data = rel_report(**report_context).render()
+        template = report.get_template_cached()
+        if template is None:
+            mimetype = MIMETYPES[report.template_extension]
+            loader = relatorio.reporting.MIMETemplateLoader()
+            klass = loader.factories[loader.get_type(mimetype)]
+            template = klass(BytesIO(report.report_content))
+            cls._callback_loader(report, template)
+            report.set_template_cached(template)
+        data = template.generate(**report_context).render()
         if hasattr(data, 'getvalue'):
             data = data.getvalue()
-        os.close(fd)
-        os.remove(path)
-
         return data
 
     @classmethod
diff -r 97dba6118e44 -r 1bd2d71d8f63 trytond/res/email_reset_password.html
--- a/trytond/res/email_reset_password.html     Sat Sep 12 10:00:03 2020 +0200
+++ b/trytond/res/email_reset_password.html     Sat Sep 12 16:35:52 2020 +0200
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html xmlns:py="http://genshi.edgewall.org/";>
+<html xmlns:py="http://genshi.edgewall.org/"; 
xmlns:i18n="http://genshi.edgewall.org/i18n";>
     <head>
         <title>Reset Password</title>
     </head>
@@ -7,15 +7,19 @@
         <div style="display: block; text-align: center">
             <div>
                 <h1>Reset Password</h1>
-                <p>The password for your account, 
<strong>${record.login}</strong>, has been reset.<br/>
+                <p i18n:msg="login,password,host,database,host,database">
+                The password for your account, 
<strong>${record.login}</strong>, has been reset.<br/>
                 You must set a new one from the user's preferences.<br/>
-                You can connect with this temporary password 
<strong>${record.password_reset}</strong> to</p>
+                You can connect with this temporary password 
<strong>${record.password_reset}</strong> to<br/>
                 <a 
href="tryton://${host}/${database}">tryton://${host}/${database}</a><br/>
-                <a 
href="${http_host}/#${database}">${http_host}/#${database}</a><br/>
+                <a 
href="${http_host}/#${database}">${http_host}/#${database}</a>
+                </p>
             </div>
             <hr style="margin-top: 20px; border-style: solid none none; 
border-color: #EEE"></hr>
             <div style="font-size: 80%; color: #777">
-                <p>The password will expire at <time 
datetime="${record.password_reset_expire.isoformat()}">${expire} UTC</time>.</p>
+                <p i18n:msg="datetime">
+                The password will expire at <time 
datetime="${record.password_reset_expire.isoformat()}">${expire} UTC</time>.
+                </p>
             </div>
         </div>
     </body>

Reply via email to