Skip to content
Snippets Groups Projects
redner_template.py 8.90 KiB
import logging
import re

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError

from .redner import Redner

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"
    _description = "Redner Template"

    name = fields.Char(
        string="Name",
        required=True,
        help="This is a name of template mjml redner",
    )

    body = fields.Text(
        string="Template remote Id",
        translate=True,
        help="Code for the mjml redner template must be added here",
    )

    slug = fields.Char(string="Slug")

    active = fields.Boolean(
        string="Active",
        default=True,
        help=(
            "If unchecked, it will allow you to hide the "
            "template without removing it."
        ),
    )

    is_mjml = fields.Boolean(
        string="Is MJML",
        default=True,
        help="set to false if your template doesn't contain MJML",
    )

    detected_keywords = fields.Text(
        string="Keywords", readonly=True, compute="_compute_keywords"
    )

    language = fields.Selection(
        string=_("Language"),
        selection=[
            ("mustache", "mustache"),
            ("handlebar", "handlebar"),
            ("od+mustache", "od+mustache"),
        ],
        default="mustache",
        required=True,
        help="templating language",
    )

    redner_id = fields.Char(string="Redner ID", readonly=True)

    produces = fields.Selection(
        selection=[
            ("text/mjml", "MJML"),
            ("text/html", "HTML"),
            ("application/vnd.oasis.opendocument.text", "OpenDocument Text"),
        ],
        string="Produces",
        default="text/mjml",
    )

    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
    def redner(self):
        """Try to avoid Redner instance to be over created"""
        if self._redner is None:

            # Bypass security rules when reading these configuration params. By
            # default, only some administrators have access to that model.
            config_model = self.env["ir.config_parameter"].sudo()

            self._redner = Redner(
                config_model.get_param("redner.api_key"),
                config_model.get_param("redner.server_url"),
                config_model.get_param("redner.account"),
            )

        return self._redner

    @api.model
    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.
        body, body_format = (
            (vals.get("template_data", ""), "base64")
            if vals.get("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(
            vals.get("language"),
            body,
            vals.get("name"),
            produces=vals.get("produces"),
            body_format=body_format,
            version=fields.Date.to_string(fields.Datetime.now()),
        )

        return super(RednerTemplate, self).create(vals)

    def write(self, vals):
        """Overwrite write to update redner template"""

        # Similar to the "send_to_rednerd_server" method; not worth factoring
        # out.

        # We depend on the API for consistency here
        # So raised error should not result with an updated template
        if "name" in vals:
            self.ensure_one()

            redner_id = self.redner_id
            vals["redner_id"] = vals["name"]

        ret = super(RednerTemplate, self).write(vals)
        for record in self:
            try:
                body, body_format = (
                    (record.template_data.decode("utf8"), "base64")
                    if record.language == "od+mustache"
                    else (record.body, "text")
                )

                if "name" not in vals:
                    redner_id = record.redner_id

                record.redner.templates.account_template_update(
                    redner_id,
                    vals.get("language", record.language),
                    body,
                    name=vals.get("name", ""),
                    produces=vals.get("produces", record.produces),
                    body_format=body_format,
                    version=fields.Date.to_string(record.write_date),
                )
            except Exception as e:
                logger.error("Failed to update redner template :%s" % e)
                raise ValidationError(
                    _("Failed to update render template, %s" % e)
                )
        return ret

    def unlink(self):
        """Overwrite unlink to delete redner template"""

        # We do NOT depend on the API for consistency here
        # So raised error should not result block template deletion
        try:
            self.redner.templates.account_template_delete(self.redner_id)
        except Exception:
            pass

        return super(RednerTemplate, self).unlink()

    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {}, name=_("%s (copy)") % self.name)
        return super(RednerTemplate, self).copy(default)

    @api.depends("body", "template_data")
    def _compute_keywords(self):
        for record in self:
            if record.language == "od+mustache":
                record.detected_keywords = record.template_varlist_fetch()
            else:
                record.detected_keywords = get_redner_tmpl_keys(record.body)

    @api.model
    def get_keywords(self):
        """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 []

    def send_to_rednerd_server(self):
        """Send templates to the rednerd server. Useful when you have
        existing templates you want to register onto a new rednerd server (or
        with a new user).
        """
        for record in self:
            # Similar to the "write" method override; not worth factoring out.
            templates = record.redner.templates
            body, body_format = (
                (record.template_data.decode("utf8"), "base64")
                if record.language == "od+mustache"
                else (record.body, "text")
            )

            try:
                templates.account_template_update(
                    record.redner_id,
                    record.language,
                    body,
                    record.name,
                    produces=record.produces,
                    body_format=body_format,
                    version=fields.Date.to_string(record.write_date),
                )
            except ValidationError:
                record.redner_id = templates.account_template_add(
                    record.language,
                    body,
                    record.name,
                    produces=record.produces,
                    body_format=body_format,
                    version=fields.Date.to_string(fields.Datetime.now()),
                )