[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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user