Skip to content
Snippets Groups Projects
Commit 1c42994c authored by adrien.bonamy's avatar adrien.bonamy
Browse files

redner: backport redner evolution + add ImageFile and ImageDataURL converters

parent 492b4881
No related branches found
No related tags found
1 merge request!8redner: backport redner evolution + add ImageFile and ImageDataURL converters
NEWS.rst 0 → 100644
====
NEWS
====
11.0.2.3
========
* Add a new template engine "od+mustache"
11.0.2.2
========
* Previous working 11.0 version.
......@@ -21,7 +21,7 @@
"name": "Redner",
"summary": """
Allows to generate transactional emails and documents in PDF or HTML format""",
"version": "11.0.1.0.0",
"version": "11.0.2.0.0",
"author": "XCG Consulting",
"category": "Technical",
"description": """
......@@ -59,7 +59,7 @@
.. _mjml-app: http://mjmlio.github.io/mjml-app/
""",
"depends": ["base", "mail"],
"depends": ["base", "mail", "converter"],
"data": [
"security/ir.model.access.csv",
"views/redner_template_views.xml",
......
import base64
from typing import Any, Dict, Optional
from odoo import models
from odoo.tools.mimetypes import guess_mimetype
from odoo.addons.converter import Converter
class ImageFile(Converter):
def __init__(self, fieldname):
self.fieldname = fieldname
def odoo_to_message(
self, instance: models.Model, ctx: Optional[Dict] = None
) -> Any:
value = getattr(instance, self.fieldname)
if not value:
return {}
content = base64.b64decode(value)
mimetype = guess_mimetype(content)
return {"body": value.decode("ascii"), "mime-type": mimetype}
class ImageDataURL(Converter):
def __init__(self, fieldname):
self.fieldname = fieldname
def odoo_to_message(
self, instance: models.Model, ctx: Optional[Dict] = None
) -> Any:
value = getattr(instance, self.fieldname)
if not value:
return ""
content = base64.b64decode(value)
mimetype = guess_mimetype(content)
return "data:%s;base64,%s" % (mimetype, value.decode("ascii"))
import ast
import logging
import time
......@@ -42,7 +41,7 @@
report_type = fields.Selection(selection_add=[("redner", "redner")])
redner_multi_in_one = fields.Boolean(
string="Multiple Records in a Single Report",
string="Multiple Records in a Single Redner Report",
help="If you execute a report on several records, "
"by default Odoo will generate a ZIP file that contains as many "
"files as selected records. If you enable this option, Odoo will "
......@@ -50,7 +49,7 @@
)
redner_filetype = fields.Selection(
selection="_get_redner_filetypes", string="Output Format"
selection="_get_redner_filetypes", string="Redner Output Format"
)
is_redner_native_format = fields.Boolean(
......@@ -81,8 +80,7 @@
@api.multi
def action_get_substitutions(self):
""" Call by: action button `Get Substitutions from Redner Report`
"""
"""Call by: action button `Get Substitutions from Redner Report`"""
self.ensure_one()
if self.redner_tmpl_id:
......@@ -134,6 +132,6 @@
return metadata_values
def get_report_data(self, res_id):
# Get redner template values
values_sent_to_redner = {}
if not res_id:
return {}
......@@ -139,4 +137,5 @@
if not res_id:
return values_sent_to_redner
conv = self.substitution_ids.filtered(
lambda r: r.depth == 0
).build_converter()
......@@ -142,3 +141,3 @@
Template = self.env["mail.template"].with_context(safe=True)
instance = self.env[self.model].browse(res_id)
......@@ -144,7 +143,3 @@
for sub in self.substitution_ids:
value = Template.render_template(sub.value, self.model, res_id)
if sub.deserialize:
value = ast.literal_eval(value)
values_sent_to_redner[sub.keyword] = value
return conv.odoo_to_message(instance)
......@@ -150,5 +145,12 @@
return values_sent_to_redner
@api.model
def get_from_report_name(self, report_name, report_type):
return self.search(
[
("report_name", "=", report_name),
("report_type", "=", report_type),
]
)
def render_redner(self, res_ids, data):
self.ensure_one()
......@@ -164,8 +166,8 @@
)
def gen_report_download_filename(self, res_ids, data):
"""Override this function to change the name of the downloaded report
"""
"""Override this function to change the name of the downloaded
report"""
self.ensure_one()
report = self.get_from_report_name(self.report_name, self.report_type)
if report.print_report_name and not len(res_ids) > 1:
......@@ -176,8 +178,7 @@
return "{}.{}".format(self.name, self.redner_filetype)
def _get_attachments(self, res_ids):
""" Return the report already generated for the given res_ids
"""
"""Return the report already generated for the given res_ids"""
self.ensure_one()
save_in_attachment = {}
if res_ids:
......
import ast
import base64
......@@ -2,4 +1,5 @@
import base64
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
......@@ -3,7 +3,16 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import pycompat
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
def image(value):
# get MIME type associated with the decoded_data.
image_base64 = base64.b64decode(value)
mimetype = guess_mimetype(image_base64)
return {"body": value.decode("utf-8"), "mime-type": mimetype}
class MailTemplate(models.Model):
......@@ -33,8 +42,7 @@
@api.multi
def action_get_substitutions(self):
""" Call by: action button `Get Substitutions from Redner Template`
"""
"""Call by: action button `Get Substitutions from Redner Template`"""
self.ensure_one()
if self.redner_tmpl_id:
......@@ -59,13 +67,13 @@
def _patch_email_values(self, values, res_id):
# get redner template values
values_sent_to_redner = {}
for sub in self.redner_substitution_ids:
value = self.render_template(sub.value, self.model, res_id)
if sub.deserialize:
value = ast.literal_eval(value)
values_sent_to_redner[sub.keyword] = value
conv = self.redner_substitution_ids.filtered(
lambda r: r.depth == 0
).build_converter()
instance = self.env[self.model].browse(res_id)
values_sent_to_redner = conv.odoo_to_message(instance)
try:
res = self.redner_tmpl_id.redner.templates.render(
......@@ -98,7 +106,7 @@
return results
multi_mode = True
if isinstance(res_ids, pycompat.integer_types):
if isinstance(res_ids, int):
res_ids = [res_ids]
multi_mode = False
......@@ -108,3 +116,9 @@
for res_id, values in results.items()
}
return self._patch_email_values(results, res_ids[0])
def render_variable_hook(self, variables):
""" Override to add additional variables in mail "render template" func
"""
variables.update({"image": lambda value: image(value)})
return super(MailTemplate, self).render_variable_hook(variables)
......@@ -143,7 +143,6 @@
language,
body,
name,
produces,
version,
produces="text/html",
body_format="text",
locale="fr_FR",
......@@ -148,5 +147,6 @@
body_format="text",
locale="fr_FR",
version="N/A",
):
"""Store template in Redner
......@@ -154,6 +154,6 @@
name(string): Name of your template. This is to help the user find
its templates in a list.
language(string): Language your template is written with.
Can be mustache or handlebar
Can be mustache, handlebar or od+mustache.
body(string): Content you want to create.
......@@ -158,11 +158,9 @@
body(string): Content you want to create.
produces(string): One of: ``text/mjml``, ``text/html``.
version(string): Version number. In this module we send dates.
produces(string): Can be text/html or
body_format (string): The body attribute format. Can be 'text' or
'base64'. Default 'base64'
locale(string):
......@@ -163,9 +161,11 @@
body_format (string): The body attribute format. Can be 'text' or
'base64'. Default 'base64'
locale(string):
version(string):
Returns:
name(string): Redner template Name.
"""
......@@ -189,8 +189,7 @@
template_id,
language,
body,
name,
produces,
version,
name="",
produces="text/html",
body_format="text",
locale="fr_FR",
......@@ -195,5 +194,6 @@
body_format="text",
locale="fr_FR",
version="N/A",
):
"""Store template in Redner
......@@ -202,7 +202,7 @@
This is to help the user find its templates in a list.
name(string): The new template name (optional)
language(string): Language your template is written with.
Can be mustache or handlebar
Can be mustache, handlebar or od+mustache
body(string): Content you want to create.
......@@ -206,12 +206,10 @@
body(string): Content you want to create.
produces(string): One of: ``text/mjml``, ``text/html``.
version(string): Version number. In this module we send dates.
produces(string): Can be text/html or
body_format (string): The body attribute format. Can be 'text' or
'base64'. Default 'base64'
locale(string):
......@@ -212,9 +210,11 @@
body_format (string): The body attribute format. Can be 'text' or
'base64'. Default 'base64'
locale(string):
version(string):
Returns:
name(string): Redner template Name.
"""
......@@ -247,3 +247,19 @@
"v1/template/%s/%s" % (self.master.account, name),
http_verb="delete",
)
def account_template_varlist(self, name):
"""Extract the list of variables present in the template.
The list is not quaranteed to be accurate depending on the
template language.
Args:
name(string): Redner template name.
Returns:
dict: API response.
"""
params = {"account": self.master.account, "name": name}
return self.master.call("v1/varlist", **params)
from odoo import fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.addons import converter
from ..converter import ImageDataURL, ImageFile
class Substitution(models.Model):
......@@ -2,10 +7,9 @@
class Substitution(models.Model):
""" Substitution values for a Redner email message
"""
"""Substitution values for a Redner email message"""
_name = "redner.substitution"
_description = "Redner Substitution"
......@@ -7,19 +11,9 @@
_name = "redner.substitution"
_description = "Redner Substitution"
deserialize = fields.Boolean(
string="Deserialize",
help=(
"Enable this to deserialize before sending to redner. Useful "
"whend sending lists to the redner server."
),
)
keyword = fields.Char(
string="Keyword", help="Keywords templates are added here"
)
keyword = fields.Char(string="Variable", help="Template variable name")
template_id = fields.Many2one(
comodel_name="mail.template", string="Use template"
......@@ -29,8 +23,21 @@
comodel_name="ir.actions.report", string="Use report"
)
value = fields.Char(
string="Value",
help="Send this to your redner server and you will "
"get a nice html output ready to be sent by email.",
value = fields.Char(string="Expression")
converter = fields.Selection(
selection=[
("mail_template", "Odoo Template"),
("mail_template+deserialize", "Odoo Template + Eval"),
("field", "Field"),
("image-file", "Image file"),
("image-data-url", "Image data url"),
("relation-to-many", "Relation to many"),
("relation-path", "Relation Path"),
("constant", "Constant value"),
]
)
depth = fields.Integer(
string="Depth", compute="_compute_depth", store=True
)
......@@ -36,1 +43,62 @@
)
@api.depends("keyword")
def _compute_depth(self):
for record in self:
record.depth = record.keyword.count(".")
def get_children(self):
return self.search(
[
("ir_actions_report_id", "=", self.ir_actions_report_id.id),
("keyword", "=like", self.keyword + ".%"),
("depth", "=", self.depth + 1),
]
)
def build_converter(self):
d = {}
for sub in self:
if sub.converter == "mail_template":
conv = converter.MailTemplate(sub.value, False)
elif sub.converter == "mail_template+deserialize":
conv = converter.MailTemplate(sub.value, True)
elif sub.converter == "constant":
conv = converter.Constant(sub.value)
elif sub.converter == "field":
if "." in sub.value:
path, name = sub.value.rsplit(".", 1)
else:
path, name = None, sub.value
conv = converter.Field(name)
if path:
conv = converter.Relation(path.replace(".", "/"), conv)
elif sub.converter == "image-file":
if "." in sub.value:
path, name = sub.value.rsplit(".", 1)
else:
path, name = None, sub.value
conv = ImageFile(name)
if path:
conv = converter.Relation(path.replace(".", "/"), conv)
elif sub.converter == "image-data-url":
conv = ImageDataURL(sub.value)
elif sub.converter == "relation-to-many":
conv = converter.RelationToMany(
sub.value,
None,
converter=sub.get_children().build_converter(),
)
elif sub.converter == "relation-path":
conv = converter.Relation(
sub.value, sub.get_children().build_converter()
)
elif sub.converter is False:
continue
else:
raise ValidationError(
_("invalid converter type: %s") % sub.converter
)
d[sub.keyword.rsplit(".", 2)[-1]] = conv
return converter.Model("", d)
import logging
import re
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
......@@ -8,48 +7,7 @@
logger = logging.getLogger(__name__)
PREFIX = "tmpl_"
expression_pattern = re.compile(
r"""
(?P<open_bracket>\{\{) # Match opening brackets {{
(?P<content>.*) # Match anything
(?P<close_bracket>\}\}) # Match closing brackets }}
""",
re.VERBOSE,
)
# Match every words which starts with PREFIX
variable_pattern = re.compile(r"(?P<variable>%s[\w]+)" % PREFIX, re.VERBOSE)
def get_redner_tmpl_keys(data) -> list: # noqa
"""Retrieve every substitution template variables, should be prefixed by
``tmpl_``.
Args:
data(string): xml body i.e::
<mjml><mj-body>{{tmpl_replace_me}}</mj-body></mjml>
Returns:
list: of keywords
"""
if not data:
return []
return list(
set(
[
v.group("variable")
for e in expression_pattern.finditer(data)
for v in variable_pattern.finditer(e.group("content"))
]
)
)
class RednerTemplate(models.Model):
_name = "redner.template"
......@@ -52,7 +10,8 @@
class RednerTemplate(models.Model):
_name = "redner.template"
_description = "Redner Template"
name = fields.Char(
string="Name",
......@@ -63,7 +22,6 @@
body = fields.Text(
string="Template remote Id",
translate=True,
required=True,
help="Code for the mjml redner template must be added here",
)
......@@ -85,8 +43,8 @@
)
detected_keywords = fields.Text(
string="Keywords", readonly=True, compute="_compute_keywords"
string="Variables", readonly=True, compute="_compute_keywords"
)
language = fields.Selection(
string=_("Language"),
......@@ -89,12 +47,19 @@
)
language = fields.Selection(
string=_("Language"),
selection=[("mustache", "mustache"), ("handlebar", "handlebar")],
default="mustache",
selection=[
("text/html|mustache", "HTML + mustache"),
("text/mjml|mustache", "MJML + mustache"),
(
"application/vnd.oasis.opendocument.text|od+mustache",
"OpenDocument + mustache",
),
],
default="text/html|mustache",
required=True,
help="templating language",
)
redner_id = fields.Char(string="Redner ID", readonly=True)
......@@ -95,18 +60,12 @@
required=True,
help="templating language",
)
redner_id = fields.Char(string="Redner ID", readonly=True)
produces = fields.Selection(
selection=[("text/mjml", "MJML"), ("text/html", "HTML")],
string="Produces",
default="text/mjml",
)
locale_id = fields.Many2one(
comodel_name="res.lang",
string="Locale",
help="Optional translation language (ISO code).",
)
......@@ -107,9 +66,11 @@
locale_id = fields.Many2one(
comodel_name="res.lang",
string="Locale",
help="Optional translation language (ISO code).",
)
template_data = fields.Binary("Libreoffice Template")
_redner = None
@property
......@@ -133,6 +94,16 @@
def create(self, vals):
"""Overwrite create to create redner template"""
# Prepare template params according to the selected language.
# Use template data field if the selected language is "od";
# otherwise the body field is used.
produces, language = vals.get("language").split("|")
body, body_format = (
(vals.get("template_data", ""), "base64")
if language == "od+mustache"
else (vals.get("body"), "text")
)
# We depend on the API for consistency here
# So raised error should not result with a created template
vals["redner_id"] = self.redner.templates.account_template_add(
......@@ -136,6 +107,6 @@
# We depend on the API for consistency here
# So raised error should not result with a created template
vals["redner_id"] = self.redner.templates.account_template_add(
vals.get("language"),
vals.get("body"),
language,
body,
vals.get("name"),
......@@ -141,6 +112,7 @@
vals.get("name"),
vals.get("produces"),
fields.Datetime.now(),
produces=produces,
body_format=body_format,
version=fields.Datetime.now(),
)
return super(RednerTemplate, self).create(vals)
......@@ -163,5 +135,12 @@
ret = super(RednerTemplate, self).write(vals)
for record in self:
try:
produces, language = record.language.split("|")
body, body_format = (
(record.template_data.decode("utf8"), "base64")
if language == "od+mustache"
else (record.body, "text")
)
if "name" not in vals:
redner_id = record.redner_id
......@@ -166,4 +145,5 @@
if "name" not in vals:
redner_id = record.redner_id
record.redner.templates.account_template_update(
redner_id,
......@@ -168,10 +148,11 @@
record.redner.templates.account_template_update(
redner_id,
vals.get("language", record.language),
vals.get("body", record.body),
vals.get("name") or "",
vals.get("produces", record.produces),
record.write_date,
language,
body,
name=vals.get("name", ""),
produces=produces,
body_format=body_format,
version=record.write_date,
)
except Exception as e:
logger.error("Failed to update redner template :%s" % e)
......@@ -200,5 +181,5 @@
return super(RednerTemplate, self).copy(default)
@api.one
@api.depends("body")
@api.depends("body", "template_data")
def _compute_keywords(self):
......@@ -204,5 +185,6 @@
def _compute_keywords(self):
self.detected_keywords = get_redner_tmpl_keys(self.body)
for record in self:
record.detected_keywords = record.template_varlist_fetch()
@api.model
def get_keywords(self):
......@@ -206,9 +188,34 @@
@api.model
def get_keywords(self):
""" Return mjml redner keywords
"""
return get_redner_tmpl_keys(self.body)
"""Return template redner keywords"""
varlist = self.template_varlist_fetch()
for name in varlist:
while "." in name:
name = name[: name.rfind(".")]
if name not in varlist:
varlist.append(name)
varlist.sort()
return varlist
@api.model
def template_varlist_fetch(self):
"""Retrieve the list of variables present in the template."""
try:
if not self.redner_id:
return []
return self.redner.templates.account_template_varlist(
self.redner_id
)
except Exception as e:
logger.warning("Failed to fetch account template varlist :%s" % e)
return []
@api.one
def send_to_rednerd_server(self):
......@@ -218,10 +225,11 @@
"""
# Similar to the "write" method override; not worth factoring out.
common_args = (
self.language,
self.body,
self.name,
self.produces,
self.write_date,
templates = self.redner.templates
produces, language = self.language.split("|")
body, body_format = (
(self.template_data.decode("utf8"), "base64")
if self.language == "od+mustache"
else (self.body, "text")
)
......@@ -227,2 +235,3 @@
)
try:
......@@ -228,5 +237,11 @@
try:
self.redner.templates.account_template_update(
self.redner_id, *common_args
templates.account_template_update(
self.redner_id,
language,
body,
self.name,
produces=produces,
body_format=body_format,
version=self.write_date,
)
except ValidationError:
......@@ -231,5 +246,10 @@
)
except ValidationError:
self.redner_id = self.redner.templates.account_template_add(
*common_args
self.redner_id = templates.account_template_add(
language,
body,
self.name,
produces=produces,
body_format=body_format,
version=fields.Datetime.now(),
)
......@@ -47,8 +47,7 @@
return result_path
def _create_single_report(self, model_instance, data):
""" This function to generate our redner report
"""
"""This function to generate our redner report"""
self.ensure_one()
report_xml = self.ir_actions_report_id
......@@ -118,7 +117,7 @@
@api.model
def _merge_pdf(self, reports_path):
""" Merge PDF files into one.
"""Merge PDF files into one.
:param reports_path: list of path of pdf files
:returns: path of the merged pdf
"""
......
......@@ -30,4 +30,5 @@
<field name="substitution_ids" nolabel="1">
<tree editable="top">
<field name="keyword"/>
<field name="converter" />
<field name="value"/>
......@@ -33,5 +34,4 @@
<field name="value"/>
<field name="deserialize" />
</tree>
</field>
</group>
......@@ -40,4 +40,4 @@
</field>
</record>
</odoo>
\ No newline at end of file
</odoo>
......@@ -39,4 +39,5 @@
<field name="redner_substitution_ids" nolabel="1">
<tree editable="top">
<field name="keyword" />
<field name="converter" />
<field name="value" />
......@@ -42,5 +43,4 @@
<field name="value" />
<field name="deserialize" />
</tree>
</field>
</group>
......
......@@ -48,14 +48,13 @@
</div>
<notebook>
<page string="Template Body">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">
<b>Info</b>: Your template substitutions variables should be prefixed with
<code>tmpl_</code>, Example: <code>tmpl_replace_me</code>
</span>
</div>
<field name="body" widget="ace" />
<page string="Template Body" attrs="{'invisible': [('language', '=', 'application/vnd.oasis.opendocument.text|od+mustache')]}">
<field name="body" widget="ace" attrs="{'required': [('language', '!=', 'application/vnd.oasis.opendocument.text|od+mustache')]}"/>
</page>
<page string="Template Libreoffice" attrs="{'invisible': [('language', '!=', 'application/vnd.oasis.opendocument.text|od+mustache')]}">
<group>
<field name="template_data" filename="name" nolabel="1" />
</group>
</page>
<page string="Settings">
<group>
......@@ -59,7 +58,6 @@
</page>
<page string="Settings">
<group>
<field name="produces" />
<field name="locale_id" />
</group>
</page>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment