From 9bd9cbf5325eccde2e9fd2387c9d8167309bfb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 9 Aug 2019 09:50:20 +0000 Subject: [PATCH] [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) --- addons/mail/models/mail_message.py | 125 ++++++++++++++--------------- addons/mail/models/mail_thread.py | 118 ++++++++++++++++++--------- 2 files changed, 140 insertions(+), 103 deletions(-) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index c0ff09fec94..efcd8f8d2ae 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -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 diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index bdfd821967c..701690039ca 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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