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>