[IMP] mail: allow message creation in batch

PURPOSE

Allow quick message creation by allowing create multi on mail.message.
Also introduce a new helper tool method to log in batch on records.

SPECIFICATIONS

Purpose of this commit is to add a way to log notes in batch on a document.
Those notes are logged without any notification mechanism, allowing to
attach a note in as few queries as possible.

This is implemented by

  * allowing create multi on mail.message. Nothing changes in its behavior
    except that it is not working on a list of dictionaries;
  * adding a ``_message_log_batch`` tool method allowing to post a batch
    of notes on records;
  * adding a tool method to correctly find author / email_from, to be used
    in various message_post / log / notify methods in order to have the
    same behavior in all those methods;

Logging in batch will soon be used in SMS when attaching a note to sent
SMS without having extra notifications. This will be used notably when
performing mass SMS without using the standard notification mechanism.

LINKS

Task 1997464
PR #34424
Original SMS addition: Task 1922163 (4287481)
This commit is contained in:
Thibault Delavallée
2019-08-12 12:00:22 +00:00
parent 5d1f08a012
commit 9bd9cbf532
2 changed files with 140 additions and 103 deletions
+62 -63
View File
@@ -985,78 +985,77 @@ class Message(models.Model):
'message_needaction_counter',
], ids=[res_id])
@api.model
def create(self, values):
# coming from mail.js that does not have pid in its values
if self.env.context.get('default_starred'):
self = self.with_context({'default_starred_partner_ids': [(4, self.env.user.partner_id.id)]})
@api.model_create_multi
def create(self, values_list):
tracking_values_list = []
for values in values_list:
if 'email_from' not in values: # needed to compute reply_to
values['email_from'] = self._get_default_from()
if not values.get('message_id'):
values['message_id'] = self._get_message_id(values)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(values)
if 'record_name' not in values and 'default_record_name' not in self.env.context:
values['record_name'] = self._get_record_name(values)
if 'email_from' not in values: # needed to compute reply_to
values['email_from'] = self._get_default_from()
if not values.get('message_id'):
values['message_id'] = self._get_message_id(values)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(values)
if 'record_name' not in values and 'default_record_name' not in self.env.context:
values['record_name'] = self._get_record_name(values)
if 'attachment_ids' not in values:
values['attachment_ids'] = []
# extract base64 images
if 'body' in values:
Attachments = self.env['ir.attachment']
data_to_url = {}
def base64_to_boundary(match):
key = match.group(2)
if not data_to_url.get(key):
name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
try:
attachment = Attachments.create({
'name': name,
'datas': match.group(2),
'res_model': values.get('model'),
'res_id': values.get('res_id'),
})
except binascii_error:
_logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.")
return match.group(3) # group(3) is the url ending single/double quote matched by the regexp
else:
attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
if 'attachment_ids' not in values:
values['attachment_ids'] = []
# extract base64 images
if 'body' in values:
Attachments = self.env['ir.attachment']
data_to_url = {}
def base64_to_boundary(match):
key = match.group(2)
if not data_to_url.get(key):
name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
try:
attachment = Attachments.create({
'name': name,
'datas': match.group(2),
'res_model': values.get('model'),
'res_id': values.get('res_id'),
})
except binascii_error:
_logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.")
return match.group(3) # group(3) is the url ending single/double quote matched by the regexp
else:
attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
# delegate creation of tracking after the create as sudo to avoid access rights issues
tracking_values_list.append(values.pop('tracking_value_ids', False))
# delegate creation of tracking after the create as sudo to avoid access rights issues
tracking_values_cmd = values.pop('tracking_value_ids', False)
message = super(Message, self).create(values)
messages = super(Message, self).create(values_list)
# check attachement access
if values.get('attachment_ids'):
attachment_ids = []
if all(isinstance(command, int) or command[0] in (4, 6) for command in values.get('attachment_ids')):
attachment_ids = []
check_attachment_access = []
if all(isinstance(command, int) or command[0] in (4, 6) for values in values_list for command in values.get('attachment_ids')):
for values in values_list:
for command in values.get('attachment_ids'):
if isinstance(command, int):
attachment_ids += [command]
check_attachment_access += [command]
elif command[0] == 6:
attachment_ids += command[2]
else: # command[0] == 4:
attachment_ids += [command[1]]
else:
attachment_ids = message.mapped('attachment_ids').ids # fallback on read if any unknow command
self.env['ir.attachment'].browse(attachment_ids).check(mode='read')
check_attachment_access += command[2]
else: # command[0] == 4:
check_attachment_access += [command[1]]
else:
check_attachment_access = messages.mapped('attachment_ids').ids # fallback on read if any unknow command
if check_attachment_access:
self.env['ir.attachment'].browse(check_attachment_access).check(mode='read')
if tracking_values_cmd:
vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if len(cmd) == 3 and cmd[0] == 0]
other_cmd = [cmd for cmd in tracking_values_cmd if len(cmd) != 3 or cmd[0] != 0]
if vals_lst:
self.env['mail.tracking.value'].sudo().create(vals_lst)
if other_cmd:
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
for message, values, tracking_values_cmd in zip(messages, values_list, tracking_values_list):
if tracking_values_cmd:
vals_lst = [dict(cmd[2], mail_message_id=message.id) for cmd in tracking_values_cmd if len(cmd) == 3 and cmd[0] == 0]
other_cmd = [cmd for cmd in tracking_values_cmd if len(cmd) != 3 or cmd[0] != 0]
if vals_lst:
self.env['mail.tracking.value'].sudo().create(vals_lst)
if other_cmd:
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
if message.is_thread_message(values):
message._invalidate_documents(values.get('model'), values.get('res_id'))
if message.is_thread_message(values):
message._invalidate_documents(values.get('model'), values.get('res_id'))
return message
+78 -40
View File
@@ -1699,7 +1699,7 @@ class MailThread(models.AbstractModel):
@api.returns('mail.message', lambda value: value.id)
def message_post(self,
body='', subject=None, message_type='notification',
email_from=False, author_id=None, parent_id=False,
email_from=None, author_id=None, parent_id=False,
subtype_id=False, subtype=None, partner_ids=None, channel_ids=None,
attachments=None, attachment_ids=None,
add_sign=True, record_name=False,
@@ -1750,10 +1750,8 @@ class MailThread(models.AbstractModel):
raise ValueError('message_post partner_ids and channel_ids must be integer list, not commands')
# Find the message's author
if author_id is None: # keep False values
author_id = self.env.user.partner_id.id
if not email_from:
email_from = self.env['res.partner'].browse(author_id).sudo().email_formatted if author_id else self.env['mail.message']._get_default_from()
author_info = self._message_compute_author(author_id, email_from, raise_exception=True)
author_id, email_from = author_info['author_id'], author_info['email_from']
if not subtype_id:
subtype = subtype or 'mt_note'
@@ -1786,6 +1784,7 @@ class MailThread(models.AbstractModel):
values = dict(msg_kwargs)
values.update({
'author_id': author_id,
'email_from': email_from,
'model': self._name,
'res_id': self.id,
'body': body,
@@ -1797,7 +1796,6 @@ class MailThread(models.AbstractModel):
'channel_ids': channel_ids,
'add_sign': add_sign,
'record_name': record_name,
'email_from': email_from,
})
attachments = attachments or []
attachment_ids = attachment_ids or []
@@ -1887,7 +1885,7 @@ class MailThread(models.AbstractModel):
return composer.send_mail()
def message_notify(self, partner_ids=False, parent_id=False, model=False, res_id=False,
author_id=False, body='', subject=False, **kwargs):
author_id=None, email_from=None, body='', subject=False, **kwargs):
""" Shortcut allowing to notify partners of messages that shouldn't be
displayed on a document. It pushes notifications on inbox or by email depending
on the user configuration, like other notifications. """
@@ -1897,16 +1895,8 @@ class MailThread(models.AbstractModel):
msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs)
if author_id:
author = self.env['res.partner'].sudo().browse(author_id)
else:
author = self.env.user.partner_id
if not author.email:
raise exceptions.UserError(_("Unable to notify message, please configure the sender's email address."))
email_from = author.email_formatted
partner_ids = partner_ids or set()
author_info = self._message_compute_author(author_id, email_from, raise_exception=True)
author_id, email_from = author_info['author_id'], author_info['email_from']
if not partner_ids:
_logger.warning('Message notify called without recipient_ids, skipping')
@@ -1924,7 +1914,7 @@ class MailThread(models.AbstractModel):
'message_type': 'user_notification',
'subject': subject,
'body': body,
'author_id': author.id,
'author_id': author_id,
'email_from': email_from,
'partner_ids': partner_ids,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
@@ -1937,7 +1927,7 @@ class MailThread(models.AbstractModel):
MailThread._notify_thread(new_message, values, **notif_kwargs)
return new_message
def _message_log(self, body='', author_id=None, subject=False, message_type='notification', **kwargs):
def _message_log(self, body='', author_id=None, email_from=None, subject=False, message_type='notification', **kwargs):
""" Shortcut allowing to post note on a document. It does not perform
any notification and pre-computes some values to have a short code
as optimized as possible. This method is private as it does not check
@@ -1945,24 +1935,13 @@ class MailThread(models.AbstractModel):
the log process. This method should be called within methods where
access rights are already granted to avoid privilege escalation. """
self.ensure_one()
if author_id:
author = self.env['res.partner'].sudo().browse(author_id)
else:
author = self.env.user.partner_id
author_id = author.id
if author.email:
email_from = author.email_formatted
elif self.env.su:
# superuser mode without author email -> probably public user; anyway we don't want to crash
email_from = False
else:
raise exceptions.UserError(_("Unable to log message, please configure the sender's email address."))
author_info = self._message_compute_author(author_id, email_from, raise_exception=False)
author_id, email_from = author_info['author_id'], author_info['email_from']
message_values = {
'subject': subject,
'body': body,
'author_id': author.id,
'author_id': author_id,
'email_from': email_from,
'message_type': message_type,
'model': kwargs.get('model', self._name),
@@ -1973,14 +1952,73 @@ class MailThread(models.AbstractModel):
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
}
message_values.update(kwargs)
message = self.sudo()._message_create(message_values)
return message
return self.sudo()._message_create(message_values)
def _message_create(self, values):
create_values = dict(values)
create_values['partner_ids'] = [(4, pid) for pid in create_values.get('partner_ids', [])]
create_values['channel_ids'] = [(4, cid) for cid in create_values.get('channel_ids', [])]
return self.env['mail.message'].create(create_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
same purpose as _message_log, done in batch to speedup quick note log.
:param bodies: dict {record_id: body}
"""
author_info = self._message_compute_author(author_id, email_from, raise_exception=False)
author_id, email_from = author_info['author_id'], author_info['email_from']
base_message_values = {
'subject': subject,
'author_id': author_id,
'email_from': email_from,
'message_type': message_type,
'model': self._name,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
}
values_list = [dict(base_message_values,
res_id=record.id,
body=bodies.get(record.id, ''))
for record in self]
return self.sudo()._message_create(values_list)
def _message_compute_author(self, author_id=None, email_from=None, raise_exception=True):
""" Tool method computing author information for messages. Purpose is
to ensure maximum coherence between author / current user / email_from
when sending emails. """
if author_id is None:
if email_from:
author = self._mail_find_partner_from_emails([email_from])[0]
else:
author = self.env.user.partner_id
email_from = author.email_formatted
author_id = author.id
if email_from is None:
if author_id:
author = self.env['res.partner'].browse(author_id)
email_from = author.email_formatted
# superuser mode without author email -> probably public user; anyway we don't want to crash
if not email_from and not self.env.su and raise_exception:
raise exceptions.UserError(_("Unable to log message, please configure the sender's email address."))
return {
'author_id': author_id,
'email_from': email_from,
}
def _message_create(self, values_list):
if not isinstance(values_list, (list)):
values_list = [values_list]
create_values_list = []
for values in values_list:
create_values = dict(values)
# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc', 'canned_response_ids'):
create_values.pop(x, None)
create_values['partner_ids'] = [(4, pid) for pid in create_values.get('partner_ids', [])]
create_values['channel_ids'] = [(4, cid) for cid in create_values.get('channel_ids', [])]
create_values_list.append(create_values)
return self.env['mail.message'].create(create_values_list)
# ------------------------------------------------------
# Notification API