[REF] mail: improve propagation and setup of render context when posting messages

PURPOSE

Purpose of this commit is to cleanup flow of values going through message_post
and its sub methods until email generation for notifications.

SPECIFICATIONS

Ensure some values are given directly when creating message linked to post
methods to avoid browsing message when value is not known. Notably make
signature propagation (add_sign) more explicit in message_post and its sub
methods (notify, log, ...).

Prepare future language related improvements by allowing to force company and
lang values for rendering context. Also add ``is_html_empty`` tool method to
use it in notification templates. It will be used notably to check for empty
html blocks, e.g. user signature.

Rename some internal variables to better understand their purpose.

Finally update some outdated and/or badly indented docstrings.

Task-2726501 (Mail: Propagate message values in post methods)

Part-of: odoo/odoo#82167
This commit is contained in:
Thibault Delavallée
2022-01-31 17:47:30 +00:00
parent 517df26613
commit 3eb9680602
2 changed files with 163 additions and 90 deletions
+12 -1
View File
@@ -7,6 +7,7 @@ import logging
from odoo import _, api, fields, models, tools, Command
from odoo.exceptions import UserError
from odoo.tools import is_html_empty
_logger = logging.getLogger(__name__)
@@ -301,10 +302,20 @@ class MailTemplate(models.Model):
model = model.with_context(lang=lang)
template_ctx = {
# message
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
'subtype': self.env['mail.message.subtype'].sudo(),
# record
'model_description': model.display_name,
'company': 'company_id' in record and record['company_id'] or self.env.company,
'record': record,
'record_name': False,
# user / environment
'company': 'company_id' in record and record['company_id'] or self.env.company,
'email_add_signature': False,
'signature': '',
'website_url': '',
# tools
'is_html_empty': is_html_empty,
}
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
+133 -71
View File
@@ -26,7 +26,7 @@ from xmlrpc import client as xmlrpclib
from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_ID, Command
from odoo.exceptions import MissingError
from odoo.osv import expression
from odoo.tools import is_html_empty
from odoo.tools.misc import clean_context, split_every
_logger = logging.getLogger(__name__)
@@ -1644,12 +1644,22 @@ class MailThread(models.AbstractModel):
def _message_post_process_attachments(self, attachments, attachment_ids, message_values):
""" Preprocess attachments for mail_thread.message_post() or mail_mail.create().
Purpose is to
:param list attachments: list of attachment tuples in the form ``(name,content)``, #todo xdo update that
where content is NOT base64 encoded
:param list attachment_ids: a list of attachment ids, not in tomany command form
:param dict message_data: model: the model of the attachments parent record,
res_id: the id of the attachments parent record
* transfer attachments given by ``attachment_ids`` from the composer to
the record (if any);
* limit attachments manipulation when being a shared user;
* create attachments from ``attachments``. If those are linked to the
content (body) through CIDs body is updated accordingly;
:param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
is NOT base64 encoded;
:param list attachment_ids: list of existing attachments to link to this message;
:param message_values: dictionary of values that will be used to create the
message. It is used to find back record- or content- context;
:return dict: new values for message: 'attachment_ids' and optionally 'body'
"""
return_values = {}
body = message_values.get('body')
@@ -1753,29 +1763,37 @@ class MailThread(models.AbstractModel):
email_from=None, author_id=None, parent_id=False,
subtype_xmlid=None, subtype_id=False, partner_ids=None,
attachments=None, attachment_ids=None,
add_sign=True, record_name=False,
**kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID.
""" Post a new message in an existing thread, returning the new mail.message.
:param str body: body of the message, usually raw HTML that will
be sanitized
:param str subject: subject of the message
:param str message_type: see mail_message.message_type field. Can be anything but
user_notification, reserved for message_notify
:param str email_from: from address of the author. See ``_message_compute_author``
that uses it to make email_from / author_id coherent;
:param int author_id: optional ID of partner record being the author. See
``_message_compute_author`` that uses it to make email_from / author_id coherent;
:param int parent_id: handle thread formation
:param int subtype_id: subtype_id of the message, used mainly use for
followers notification mechanism;
:param int subtype_id: subtype_id of the message, used mainly for followers
notification mechanism;
:param list(int) partner_ids: partner_ids to notify in addition to partners
computed based on subtype / followers matching;
:param list(tuple(str,str), tuple(str,str, dict) or int) attachments : list of attachment tuples in the form
``(name,content)`` or ``(name,content, info)``, where content is NOT base64 encoded
:param list id attachment_ids: list of existing attachement to link to this message
-Should only be setted by chatter
-Attachement object attached to mail.compose.message(0) will be attached
:param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
is NOT base64 encoded;
:param list attachment_ids: list of existing attachments to link to this message
-Should only be set by chatter
-Attachment object attached to mail.compose.message(0) will be attached
to the related document.
Extra keyword arguments will be used as default column values for the
new mail.message record.
:return int: ID of newly created mail.message
Extra keyword arguments will be used either
* as default column values for the new mail.message record if they match
mail.message fields;
* propagated to notification methods;
:return record: newly create mail.message
"""
self.ensure_one() # should always be posted on a record, use message_notify if no record
# split message additional values from notify additional values
@@ -1795,12 +1813,11 @@ class MailThread(models.AbstractModel):
if any(not isinstance(pc_id, int) for pc_id in partner_ids):
raise ValueError(_('message_post partner_ids and must be integer list, not commands.'))
self = self._fallback_lang() # add lang to context imediatly since it will be usefull in various flows latter.
self = self._fallback_lang() # add lang to context immediately since it will be useful in various flows latter.
# Explicit access rights check, because display_name is computed as sudo.
self.check_access_rights('read')
self.check_access_rule('read')
record_name = record_name or self.display_name
# Find the message's author
if self.env.user._is_public() and 'guest' in self.env.context:
@@ -1821,38 +1838,43 @@ class MailThread(models.AbstractModel):
parent_id = self._message_compute_parent_id(parent_id)
values = dict(msg_kwargs)
values.update({
msg_values = dict(msg_kwargs)
if 'add_sign' not in msg_values:
msg_values['add_sign'] = True
if not msg_values.get('record_name'):
msg_values['record_name'] = self.display_name
msg_values.update({
'author_id': author_id,
'author_guest_id': author_guest_id,
'email_from': email_from,
'model': self._name,
'res_id': self.id,
# content
'body': body,
'subject': subject or False,
'message_type': message_type,
'parent_id': parent_id,
'subtype_id': subtype_id,
# recipients
'partner_ids': partner_ids,
'add_sign': add_sign,
'record_name': record_name,
})
attachments = attachments or []
attachment_ids = attachment_ids or []
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, values)
values.update(attachement_values) # attachement_ids, [body]
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, msg_values)
msg_values.update(attachement_values) # attachement_ids, [body]
new_message = self._message_create(values)
new_message = self._message_create(msg_values)
# Set main attachment field if necessary
self._message_set_main_attachment_id(values['attachment_ids'])
self._message_set_main_attachment_id(msg_values['attachment_ids'])
if values['author_id'] and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
if self.env['res.partner'].browse(values['author_id']).active: # we dont want to add odoobot/inactive as a follower
self._message_subscribe(partner_ids=[values['author_id']])
if msg_values['author_id'] and msg_values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
if self.env['res.partner'].browse(msg_values['author_id']).active: # we dont want to add odoobot/inactive as a follower
self._message_subscribe(partner_ids=[msg_values['author_id']])
self._message_post_after_hook(new_message, values)
self._notify_thread(new_message, values, **notif_kwargs)
self._message_post_after_hook(new_message, msg_values)
self._notify_thread(new_message, msg_values, **notif_kwargs)
return new_message
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
@@ -1957,7 +1979,7 @@ class MailThread(models.AbstractModel):
res_id = False
MailThread = self.env['mail.thread']
values = {
msg_values = {
'parent_id': parent_id,
'model': self._name if self else model,
'res_id': self.id if self else res_id,
@@ -1973,9 +1995,12 @@ class MailThread(models.AbstractModel):
'reply_to': MailThread._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'),
}
values.update(msg_kwargs)
new_message = MailThread._message_create(values)
MailThread._notify_thread(new_message, values, **notif_kwargs)
msg_values.update(msg_kwargs)
if 'add_sign' not in msg_values:
msg_values['add_sign'] = True
new_message = MailThread._message_create(msg_values)
MailThread._notify_thread(new_message, msg_values, **notif_kwargs)
return new_message
def _message_log_with_view(self, views_or_xmlid, **kwargs):
@@ -1992,7 +2017,7 @@ class MailThread(models.AbstractModel):
self.ensure_one()
author_id, email_from = self._message_compute_author(author_id, email_from, raise_exception=False)
message_values = {
msg_values = {
'subject': subject,
'body': body,
'author_id': author_id,
@@ -2005,9 +2030,10 @@ class MailThread(models.AbstractModel):
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
'add_sign': False, # False as no notification -> no need to compute signature
}
message_values.update(kwargs)
return self.sudo()._message_create(message_values)
msg_values.update(kwargs)
return self.sudo()._message_create(msg_values)
def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification'):
""" Shortcut allowing to post notes on a batch of documents. It achieve the
@@ -2028,6 +2054,7 @@ class MailThread(models.AbstractModel):
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
'add_sign': False,
}
values_list = [dict(base_message_values,
res_id=record.id,
@@ -2121,6 +2148,9 @@ class MailThread(models.AbstractModel):
:return: recipients data (see ``MailThread._notify_get_recipients()``)
"""
# add lang to context immediately since it will be useful in various rendering later
self = self._fallback_lang()
msg_vals = msg_vals if msg_vals else {}
recipients_data = self._notify_get_recipients(message, msg_vals)
if not recipients_data:
@@ -2171,7 +2201,7 @@ class MailThread(models.AbstractModel):
def _notify_thread_by_email(self, message, recipients_data, msg_vals=False,
mail_auto_delete=True, # mail.mail
model_description=False, # rendering
model_description=False, force_email_company=False, force_email_lang=False, # rendering
check_existing=False, force_send=True, send_after_commit=True, # email send
**kwargs):
""" Method to send email linked to notified messages.
@@ -2196,10 +2226,11 @@ class MailThread(models.AbstractModel):
:param model_description: model description used in email notification process
(computed if not given);
:param force_email_company: see ``_notify_by_email_prepare_rendering_context``;
:param force_email_lang: see ``_notify_by_email_prepare_rendering_context``;
:param check_existing: check for existing notifications to update based on
mailed recipient, otherwise create new notifications;
:param force_send: send emails directly instead of using queue;
:param send_after_commit: if force_send, tells whether to send emails after
the transaction has been committed using a post-commit hook;
@@ -2209,14 +2240,18 @@ class MailThread(models.AbstractModel):
return True
model = msg_vals.get('model') if msg_vals else message.model
model_name = model_description or (self._fallback_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name
model_name = model_description or (self.env['ir.model']._get(model).display_name if model else False) # one query for display name
recipients_groups_data = self._notify_get_recipients_classify(partners_data, model_name, msg_vals=msg_vals)
if not recipients_groups_data:
return True
force_send = self.env.context.get('mail_notify_force_send', force_send)
template_values = self._notify_by_email_prepare_rendering_context(message, msg_vals, model_description=model_description) # 10 queries
template_values = self._notify_by_email_prepare_rendering_context(
message, msg_vals=msg_vals, model_description=model_description,
force_email_company=force_email_company,
force_email_lang=force_email_lang,
) # 10 queries
email_layout_xmlid = msg_vals.get('email_layout_xmlid') if msg_vals else message.email_layout_xmlid
template_xmlid = email_layout_xmlid if email_layout_xmlid else 'mail.message_notification_email'
@@ -2316,7 +2351,8 @@ class MailThread(models.AbstractModel):
return True
@api.model
def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False):
def _notify_by_email_prepare_rendering_context(self, message, msg_vals=False, model_description=False,
force_email_company=False, force_email_lang=False):
""" Prepare rendering context for notification email.
Signature: if asked a default signature is computed based on author. Either
@@ -2324,52 +2360,71 @@ class MailThread(models.AbstractModel):
user and we compute a default one based on the author's name.
Company: either there is one defined on the record (company_id field set
with a value), either we use env.company.
with a value), either we use env.company. A new parameter allows to force
its value.
Lang: when calling this method, ``_fallback_lang`` should already been
called, or a lang set in context with another way. A wild guess is done
based on templates to try to retrieve the recipient's language when a flow
like "send by email" is performed. Lang is used to try to have the
notification layout in the same language as the email content.
notification layout in the same language as the email content. A new
parameter allows to force its value.
:param msg_vals: dictionary of values used to create the message. If given it
may be used to access values related to ``message`` without accessing it
directly. It lessens query count in some optimized use cases by avoiding
access message content in db;
:param model_description: model description used in email notification process
(computed if not given);
:param force_email_company: res.company record used when rendering notification
layout. Otherwise computed based on current record;
:param force_email_lang: lang used when rendering content, used notably to
compute model name;
"""
if msg_vals is False:
msg_vals = {}
# compute send user and its related signature; try to use self.env.user instead of browsing
# user_ids if he is the author will give a sudo user, improving access performances and cache usage.
signature = ''
user = self.env.user
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if msg_vals else message.author_id
model = msg_vals.get('model') if msg_vals else message.model
add_sign = msg_vals.get('add_sign') if msg_vals else message.add_sign
subtype_id = msg_vals.get('subtype_id') if msg_vals else message.subtype_id.id
message_id = message.id
record_name = msg_vals.get('record_name') if msg_vals else message.record_name
author_user = user if user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
# trying to use user (self.env.user) instead of browing user_ids if he is the author will give a sudo user,
# improving access performances and cache usage.
add_sign = msg_vals.get('add_sign') if 'add_sign' in msg_vals else message.add_sign
if add_sign:
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if 'author_id' in msg_vals else message.author_id
author_user = self.env.user if self.env.user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
if author_user:
user = author_user
if add_sign:
signature = user.signature
signature = author_user.signature
else:
if add_sign:
signature = "<p>-- <br/>%s</p>" % author.name
if force_email_company:
company = force_email_company
else:
company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
if company.website:
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
else:
website_url = False
# compute lang in which content was rendered or typed
lang = False
if force_email_lang:
lang = force_email_lang
elif {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
# TDE FIXME: this whole brol should be cleaned !
lang = self.env.context.get('lang')
if {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
if template and template.lang:
lang = template._render_lang([self.env.context['default_res_id']])[self.env.context['default_res_id']]
if not lang:
lang = self.env.context.get('lang')
if not model_description and model:
# record, model
if not model_description:
model = msg_vals.get('model') if 'model' in msg_vals else message.model
if model:
model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name
record_name = msg_vals.get('record_name') if 'record_name' in msg_vals else message.record_name
# tracking
tracking = []
if msg_vals.get('tracking_value_ids', True) if msg_vals else bool(self): # could be tracking
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
@@ -2379,20 +2434,27 @@ class MailThread(models.AbstractModel):
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
subtype_id = msg_vals.get('subtype_id') if msg_vals and 'subtype_id' in msg_vals else message.subtype_id.id
is_discussion = subtype_id == self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
return {
# message
'is_discussion': is_discussion,
'message': message,
'signature': signature,
'website_url': website_url,
'company': company,
'subtype': message.subtype_id,
'tracking_values': tracking,
# record
'model_description': model_description,
'record': self,
'record_name': record_name,
'tracking_values': tracking,
'is_discussion': is_discussion,
'subtype': message.subtype_id,
# user / environment
'add_sign': add_sign,
'company': company,
'lang': lang,
'signature': signature,
'website_url': website_url,
# tools
'is_html_empty': is_html_empty,
}
def _notify_by_email_get_base_mail_values(self, message, additional_values=None):