mirror of https://github.com/OCA/social.git
259 lines
9.3 KiB
Python
259 lines
9.3 KiB
Python
# Copyright 2021 Hunki Enterprises BV
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
import itertools
|
|
|
|
from lxml import html as lxml_html
|
|
|
|
from odoo import _, api, exceptions, models, tools
|
|
from odoo.tools import mail
|
|
from odoo.tools.safe_eval import _BUILTINS
|
|
|
|
|
|
class ModelProxy(object):
|
|
""" A wrapper for Odoo models that only exposes fields """
|
|
|
|
def __init__(self, model):
|
|
self.__model__ = model
|
|
|
|
def __getattribute__(self, name):
|
|
model = object.__getattribute__(self, "__model__")
|
|
if name in model._fields:
|
|
field = model._fields[name]
|
|
value = model[name]
|
|
if field.relational:
|
|
value = ModelProxy(value)
|
|
return value
|
|
# TODO: also allow functions with some decorator set
|
|
raise AttributeError(name)
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, str):
|
|
return getattr(self, key)
|
|
return ModelProxy(object.__getattribute__(self, "__model__")[key])
|
|
|
|
|
|
class MailActivity(models.Model):
|
|
_inherit = "mail.activity"
|
|
_mail_activity_form_prefix = "data-form-"
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""
|
|
Apply computations in note, check for template
|
|
"""
|
|
result = super().create(vals_list)
|
|
for this in result:
|
|
if not this.activity_type_id.uses_forms:
|
|
continue
|
|
this._mail_activity_form_assert_template()
|
|
this._mail_activity_form_compute()
|
|
return result
|
|
|
|
def write(self, vals):
|
|
"""
|
|
Apply computations in note, check for template
|
|
"""
|
|
result = super().write(vals)
|
|
if (
|
|
"note" in vals
|
|
and self.env.context.get("_mail_activity_form_compute") != self
|
|
):
|
|
for this in self:
|
|
if not this.activity_type_id.uses_forms:
|
|
continue
|
|
this._mail_activity_form_assert_template()
|
|
this._mail_activity_form_compute()
|
|
return result
|
|
|
|
def read(self, fields=None, load="_classic_read"):
|
|
"""
|
|
Update note from template if we're told so
|
|
"""
|
|
result = super().read(fields=fields, load=load)
|
|
|
|
if (
|
|
not fields
|
|
or "note" in fields
|
|
and self.env.context.get("_mail_activity_form_update") != self
|
|
):
|
|
for this, this_values in zip(
|
|
self.with_context(_mail_activity_form_update=self), result,
|
|
):
|
|
activity_type = this.activity_type_id
|
|
if (
|
|
not activity_type.uses_forms
|
|
or this.write_date > activity_type.write_date
|
|
):
|
|
continue
|
|
# if the type has been updated, return html from type
|
|
# but with values from current activity
|
|
fromstring = lxml_html.fromstring
|
|
template = fromstring(activity_type.default_description)
|
|
current = fromstring(this_values["note"])
|
|
this_values["note"] = str(
|
|
lxml_html.tostring(
|
|
this._mail_activity_form_update(
|
|
template, this._mail_activity_form_extract(current),
|
|
),
|
|
)
|
|
)
|
|
return result
|
|
|
|
def _mail_activity_form_compute(self, raise_on_error=True):
|
|
"""
|
|
Compute computed nodes and write the result to the note field
|
|
"""
|
|
self.ensure_one()
|
|
if not self.note:
|
|
return
|
|
_id, _editable, _compute, _type = self._mail_activity_form_attributes()
|
|
doc = lxml_html.fromstring(self.note)
|
|
vals = self._mail_activity_form_extract(doc, compute=True)
|
|
for node in doc.xpath("//*[@%s]" % _compute):
|
|
if node.get(_id) in vals:
|
|
node.text = str(vals.get(node.get(_id)))
|
|
else:
|
|
node.text = str(
|
|
self._mail_activity_form_extract_value(node, vals, compute=True,)
|
|
)
|
|
self.with_context(_mail_activity_form_compute=self,).write(
|
|
{"note": lxml_html.tostring(doc)}
|
|
)
|
|
|
|
def _mail_activity_form_assert_template(self):
|
|
"""
|
|
Compare notes with activity_type_id.default_description and be
|
|
sure only editable nodes have been changed
|
|
"""
|
|
if not self.note or not self.activity_type_id.default_description:
|
|
return
|
|
template_html = lxml_html.fromstring(self.activity_type_id.default_description)
|
|
activity_html = lxml_html.fromstring(
|
|
self.with_context(_mail_activity_form_update=self).note
|
|
)
|
|
_id, _editable, _compute, _type = self._mail_activity_form_attributes()
|
|
different = False
|
|
for t, a in itertools.zip_longest(template_html.iter(), activity_html.iter()):
|
|
if a is None or t is None or t.tag != a.tag:
|
|
different = True
|
|
break
|
|
|
|
if set(t.attrib.keys()) != set(a.attrib.keys()):
|
|
different = True
|
|
break
|
|
|
|
for attribute in t.attrib:
|
|
if t.attrib[attribute].strip() != a.attrib[attribute].strip():
|
|
different = True
|
|
|
|
if different:
|
|
break
|
|
|
|
if (t.text or "").strip() != (a.text or "").strip():
|
|
if t.attrib.get(_compute):
|
|
continue
|
|
if t.attrib.get(_editable, "").lower() not in ("1", "true"):
|
|
different = True
|
|
break
|
|
|
|
if different:
|
|
raise exceptions.UserError(
|
|
_("You are not supposed to change the content of this activity.")
|
|
)
|
|
|
|
def _mail_activity_form_attributes(self):
|
|
"""
|
|
Return a tuple of the attribute names constructed with the above prefix
|
|
"""
|
|
return (
|
|
"%sid" % self._mail_activity_form_prefix,
|
|
"%seditable" % self._mail_activity_form_prefix,
|
|
"%scompute" % self._mail_activity_form_prefix,
|
|
"%stype" % self._mail_activity_form_prefix,
|
|
)
|
|
|
|
def _mail_activity_form_update(self, html, values):
|
|
"""
|
|
Replace the content of all nodes in html with an -id attribute with the
|
|
value from values of that name, return modified html
|
|
"""
|
|
_id, _editable, _compute, _type = self._mail_activity_form_attributes()
|
|
for node in html.xpath("//*[@%s]" % (_id)):
|
|
node.text = self._mail_activity_form_format_value(
|
|
values.get(node.attrib[_id]), node.attrib.get(_type),
|
|
)
|
|
return html
|
|
|
|
def _mail_activity_form_extract(self, html, compute=False):
|
|
"""
|
|
Parse html for nodes marked with {prefix}-editable or -compute
|
|
and return (possibly computed) values
|
|
"""
|
|
self.ensure_one()
|
|
_id, _editable, _compute, _type = self._mail_activity_form_attributes()
|
|
vals = {}
|
|
# get static values first, do computations thereafter
|
|
for node in html.xpath("//*[@%s]" % (_id)):
|
|
vals[node.get(_id)] = self._mail_activity_form_extract_value(
|
|
node, vals, compute=False,
|
|
)
|
|
for node in html.xpath("//*[@%s]" % _compute):
|
|
if not node.get(_id):
|
|
# don't compute anonymous nodes here, this happens in
|
|
# _compute
|
|
continue
|
|
vals[node.get(_id)] = self._mail_activity_form_extract_value(
|
|
node, vals, compute=compute,
|
|
)
|
|
return vals
|
|
|
|
def _mail_activity_form_extract_value(self, node, values, compute=False):
|
|
"""
|
|
Return a value given by node, possibly involving custom evaluation
|
|
"""
|
|
_id, _editable, _compute, _type = self._mail_activity_form_attributes()
|
|
expression = node.get(_compute)
|
|
value = None
|
|
if compute and expression:
|
|
value = self._mail_activity_form_eval(expression, values)
|
|
else:
|
|
value = node.text_content().strip()
|
|
value_type = node.get(_type)
|
|
return self._mail_activity_form_parse_value(value, value_type)
|
|
|
|
def _mail_activity_form_parse_value(self, value, value_type):
|
|
"""
|
|
Cast value to value_type
|
|
"""
|
|
if value_type not in ("float", "int", "str", None):
|
|
raise exceptions.UserError(_("Value type %s is invalid",) % value_type)
|
|
return _BUILTINS.get(value_type, _BUILTINS["str"])(value)
|
|
|
|
def _mail_activity_form_format_value(self, value, value_type):
|
|
"""
|
|
Format a value for representation
|
|
"""
|
|
return str(value)
|
|
|
|
def _mail_activity_form_eval(self, expression, values):
|
|
"""
|
|
Evaluate an expression for this activity
|
|
"""
|
|
return tools.safe_eval(
|
|
expression, self._mail_activity_form_eval_context(values,),
|
|
)
|
|
|
|
def _mail_activity_form_eval_context(self, values):
|
|
"""
|
|
Return the evaluation context for an expression
|
|
"""
|
|
res_object = self.env[self.res_model].browse(self.res_id)
|
|
return dict(object=ModelProxy(res_object), activity=ModelProxy(self), **values)
|
|
|
|
def _register_hook(self):
|
|
"""
|
|
Don't have the HTML cleaner remove our attributes
|
|
"""
|
|
mail.safe_attrs |= frozenset(self._mail_activity_form_attributes())
|
|
return super()._register_hook()
|