diff options
Diffstat (limited to 'addons/mail')
| -rw-r--r-- | addons/mail/models/mail_channel.py | 22 | ||||
| -rw-r--r-- | addons/mail/models/mail_message.py | 127 | ||||
| -rw-r--r-- | addons/mail/models/mail_thread.py | 383 | ||||
| -rw-r--r-- | addons/mail/models/res_partner.py | 11 | ||||
| -rw-r--r-- | addons/mail/wizard/mail_compose_message.py | 2 |
5 files changed, 311 insertions, 234 deletions
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index e2163810dab..70e2ba63ea4 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -506,20 +506,6 @@ class Channel(models.Model): notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info]) return notifications - @api.multi - def _notify(self, message): - """ Broadcast the given message on the current channels. - Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)). - A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field - of the received message indicates on wich mail channel the message should be displayed. - :param : mail.message to broadcast - """ - if not self: - return - message.ensure_one() - notifications = self._channel_message_notifications(message) - self.env['bus.bus'].sendmany(notifications) - def _notify_thread(self, message, msg_vals=False, model_description=False, mail_auto_delete=True): # When posting a message on a mail channel, manage moderation and postpone notify users if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation': @@ -533,18 +519,18 @@ class Channel(models.Model): message._notify_pending_by_chat() @api.multi - def _channel_message_notifications(self, message): + def _channel_message_notifications(self, message, message_format=False): """ Generate the bus notifications for the given message :param message : the mail.message to sent :returns list of bus notifications (tuple (bus_channe, message_content)) """ - message_values = message.message_format()[0] + message_format = message_format or message.message_format()[0] notifications = [] for channel in self: - notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)]) + notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_format)]) # add uuid to allow anonymous to listen if channel.public == 'public': - notifications.append([channel.uuid, dict(message_values)]) + notifications.append([channel.uuid, dict(message_format)]) return notifications @api.model diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 95fc750066b..e1fc850d433 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -393,8 +393,8 @@ class Message(models.Model): customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status)) has_access_to_model = message.model and self.env[message.model].check_access_rights('read', raise_exception=False) - if message.attachment_ids: - main_attachment = has_access_to_model and message.res_id and self.env[message.model].search([('id', '=', message.res_id)]) and getattr(self.env[message.model].browse(message.res_id), 'message_main_attachment_id') + if message.attachment_ids and message.res_id and issubclass(self.pool[message.model], self.pool['mail.thread']) and has_access_to_model: + main_attachment = self.env[message.model].browse(message.res_id).message_main_attachment_id attachment_ids = [] for attachment in message.attachment_ids: if attachment.id in attachments_tree: @@ -860,6 +860,55 @@ class Message(models.Model): author_ids = [mid for mid, message in message_values.items() if not self.is_thread_message(message)] + # Moderator condition: allow to WRITE, UNLINK if moderator of a pending message + moderator_ids = [] + if operation in ['write', 'unlink']: + moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')] + messages_to_check = self.ids + messages_to_check = set(messages_to_check).difference(set(author_ids), set(moderator_ids)) + if not messages_to_check: + return + + # Recipients condition, for read and write (partner_ids) + # keep on top, usefull for systray notifications + notified_ids = [] + model_record_ids = _generate_model_record_ids(message_values, messages_to_check) + if operation in ['read', 'write']: + notified_ids = [mid for mid, message in message_values.items() if message.get('notified')] + + messages_to_check = set(messages_to_check).difference(set(notified_ids)) + if not messages_to_check: + return + + # CRUD: Access rights related to the document + document_related_ids = [] + document_related_candidate_ids = [mid for mid, message in message_values.items() + if (message.get('model') and message.get('res_id') and + message.get('message_type') != 'user_notification' and + (message.get('moderation_status') != 'pending_moderation' or operation not in ['write', 'unlink']))] + model_record_ids = _generate_model_record_ids(message_values, document_related_candidate_ids) + for model, doc_ids in model_record_ids.items(): + DocumentModel = self.env[model] + if hasattr(DocumentModel, 'get_mail_message_access'): + check_operation = DocumentModel.get_mail_message_access(doc_ids, operation) ## why not giving model here? + else: + check_operation = self.env['mail.thread'].get_mail_message_access(doc_ids, operation, model_name=model) + records = DocumentModel.browse(doc_ids) + records.check_access_rights(check_operation) + mids = records.browse(doc_ids)._filter_access_rules(check_operation) + document_related_ids += [ + mid for mid, message in message_values.items() + if (message.get('model') == model and + message.get('res_id') in mids.ids and + message.get('message_type') != 'user_notification' and + (message.get('moderation_status') != 'pending_moderation' or + operation not in ['write', 'unlink']))] + + messages_to_check = messages_to_check.difference(set(document_related_ids)) + + if not messages_to_check: + return + # Parent condition, for create (check for received notifications for the created message parent) notified_ids = [] if operation == 'create': @@ -880,17 +929,12 @@ class Message(models.Model): notified_ids += [mid for mid, message in message_values.items() if message.get('parent_id') in not_parent_ids] - # Moderator condition: allow to WRITE, UNLINK if moderator of a pending message - moderator_ids = [] - if operation in ['write', 'unlink']: - moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')] + messages_to_check = messages_to_check.difference(set(notified_ids)) + if not messages_to_check: + return - # Recipients condition, for read and write (partner_ids) and create (message_follower_ids) - other_ids = set(self.ids).difference(set(author_ids), set(notified_ids), set(moderator_ids)) - model_record_ids = _generate_model_record_ids(message_values, other_ids) - if operation in ['read', 'write']: - notified_ids = [mid for mid, message in message_values.items() if message.get('notified')] - elif operation == 'create': + # Recipients condition for create (message_follower_ids) + if operation == 'create': for doc_model, doc_ids in model_record_ids.items(): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', doc_model), @@ -904,28 +948,11 @@ class Message(models.Model): message.get('message_type') != 'user_notification' ] - # CRUD: Access rights related to the document - other_ids = other_ids.difference(set(notified_ids)) - model_record_ids = _generate_model_record_ids(message_values, other_ids) - document_related_ids = [] - for model, doc_ids in model_record_ids.items(): - DocumentModel = self.env[model] - mids = DocumentModel.browse(doc_ids).exists() - if hasattr(DocumentModel, 'check_mail_message_access'): - DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ? - else: - self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model) - document_related_ids += [ - mid for mid, message in message_values.items() - if (message.get('model') == model and - message.get('res_id') in mids.ids and - message.get('message_type') != 'user_notification' and - (message.get('moderation_status') != 'pending_moderation' or - operation not in ['write', 'unlink']))] + messages_to_check = messages_to_check.difference(set(notified_ids)) + if not messages_to_check: + return - # Calculate remaining ids: if not void, raise an error - other_ids = other_ids.difference(set(document_related_ids)) - if not (other_ids and self.browse(other_ids).exists()): + if not self.browse(messages_to_check).exists(): return raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % @@ -966,17 +993,19 @@ class Message(models.Model): return message_id @api.multi - def _invalidate_documents(self): + def _invalidate_documents(self, model=None, res_id=None): """ Invalidate the cache of the documents followed by ``self``. """ for record in self: - if record.is_thread_message() and 'message_ids' in self.env[record.model]: - self.env[record.model].invalidate_cache(fnames=[ + model = model or record.model + res_id = res_id or record.res_id + if 'message_ids' in self.env[model]: + self.env[model].invalidate_cache(fnames=[ 'message_ids', 'message_unread', 'message_unread_counter', 'message_needaction', 'message_needaction_counter', - ], ids=[record.res_id]) + ], ids=[res_id]) @api.model def create(self, values): @@ -994,8 +1023,7 @@ class Message(models.Model): values['record_name'] = self._get_record_name(values) if 'attachment_ids' not in values: - values.setdefault('attachment_ids', []) - + values['attachment_ids'] = [] # extract base64 images if 'body' in values: Attachments = self.env['ir.attachment'] @@ -1021,8 +1049,21 @@ class Message(models.Model): tracking_values_cmd = values.pop('tracking_value_ids', False) message = super(Message, self).create(values) + # check attachement access if values.get('attachment_ids'): - message.attachment_ids.check(mode='read') + attachment_ids = [] + if all(isinstance(command, int) or command[0] in (4, 6) for command in values.get('attachment_ids')): + attachment_ids = [] + for command in values.get('attachment_ids'): + if isinstance(command, int): + attachment_ids += [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') 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] @@ -1033,7 +1074,7 @@ class Message(models.Model): message.sudo().write({'tracking_value_ids': tracking_values_cmd}) if message.is_thread_message(values): - message._invalidate_documents() + message._invalidate_documents(values.get('model'), values.get('res_id')) return message @@ -1067,7 +1108,9 @@ class Message(models.Model): self.mapped('attachment_ids').filtered( lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0) ).unlink() - self._invalidate_documents() + for elem in self: + if elem.is_thread_message(): + elem._invalidate_documents() return super(Message, self).unlink() # -------------------------------------------------- diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 7f29e0d13c4..17d6941377b 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -488,19 +488,14 @@ class MailThread(models.AbstractModel): return True @api.model - def check_mail_message_access(self, res_ids, operation, model_name=None): + def get_mail_message_access(self, res_ids, operation, model_name=None): """ mail.message check permission rules for related document. This method is meant to be inherited in order to implement addons-specific behavior. A common behavior would be to allow creating messages when having read access rule on the document, for portal document such as issues. """ - if model_name: - DocModel = self.env[model_name] - else: - DocModel = self - if hasattr(DocModel, '_mail_post_access'): - create_allow = DocModel._mail_post_access - else: - create_allow = 'write' + + DocModel = self.env[model_name] if model_name else self + create_allow = getattr(DocModel, '_mail_post_access', 'write') if operation in ['write', 'unlink']: check_operation = 'write' @@ -510,9 +505,7 @@ class MailThread(models.AbstractModel): check_operation = 'write' else: check_operation = operation - - DocModel.check_access_rights(check_operation) - DocModel.browse(res_ids).check_access_rule(check_operation) + return check_operation @api.multi def message_change_thread(self, new_thread): @@ -1582,97 +1575,137 @@ class MailThread(models.AbstractModel): # Post / Send message API # ------------------------------------------------------ - def _message_post_process_attachments(self, attachments, attachment_ids, message_data): + def _message_post_process_attachments(self, attachments, attachment_ids, message_values): """ Preprocess attachments for mail_thread.message_post() or mail_mail.create(). - :param list attachments: list of attachment tuples in the form ``(name,content)``, + :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 """ - IrAttachment = self.env['ir.attachment'] + return_values = {} + body = message_values.get('body') + model = message_values['model'] + res_id = message_values['res_id'] + m2m_attachment_ids = [] - cid_mapping = {} - fname_mapping = {} if attachment_ids: - filtered_attachment_ids = self.env['ir.attachment'].sudo().search([ - ('res_model', '=', 'mail.compose.message'), - ('create_uid', '=', self._uid), - ('id', 'in', attachment_ids)]) + # taking advantage of cache looks better in this case, to check + filtered_attachment_ids = self.env['ir.attachment'].sudo().browse(attachment_ids).filtered( + lambda a: a.res_model == 'mail.compose.message' and a.create_uid.id == self._uid) if filtered_attachment_ids: - filtered_attachment_ids.write({'res_model': message_data['model'], 'res_id': message_data['res_id']}) + filtered_attachment_ids.write({'res_model': model, 'res_id': res_id}) m2m_attachment_ids += [(4, id) for id in attachment_ids] # Handle attachments parameter, that is a dictionary of attachments - for attachment in attachments: - cid = False - if len(attachment) == 2: - name, content = attachment - elif len(attachment) == 3: - name, content, info = attachment - cid = info and info.get('cid') - else: - continue - if isinstance(content, str): - content = content.encode('utf-8') - elif content is None: - continue - data_attach = { - 'name': name, - 'datas': base64.b64encode(content), - 'type': 'binary', - 'datas_fname': name, - 'description': name, - 'res_model': message_data['model'], - 'res_id': message_data['res_id'], - } - new_attachment = IrAttachment.create(data_attach) - m2m_attachment_ids.append((4, new_attachment.id)) - if cid: - cid_mapping[cid] = new_attachment - fname_mapping[name] = new_attachment - - if cid_mapping and message_data.get('body'): - root = lxml.html.fromstring(tools.ustr(message_data['body'])) - postprocessed = False - for node in root.iter('img'): - if node.get('src', '').startswith('cid:'): - cid = node.get('src').split('cid:')[1] - attachment = cid_mapping.get(cid) - if not attachment: - attachment = fname_mapping.get(node.get('data-filename'), '') - if attachment: - attachment.generate_access_token() - node.set('src', '/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token)) - postprocessed = True - if postprocessed: - body = lxml.html.tostring(root, pretty_print=False, encoding='UTF-8') - message_data['body'] = body - return m2m_attachment_ids + if attachments: # generate + cids_in_body = set() + names_in_body = set() + cid_list = [] + name_list = [] + + if body: + root = lxml.html.fromstring(tools.ustr(body)) + # first list all attachments that will be needed in body + for node in root.iter('img'): + if node.get('src', '').startswith('cid:'): + cids_in_body.add(node.get('src').split('cid:')[1]) + elif node.get('data-filename'): + names_in_body.add(node.get('data-filename')) + attachement_values_list = [] + + # generate values + for attachment in attachments: + cid = False + if len(attachment) == 2: + name, content = attachment + elif len(attachment) == 3: + name, content, info = attachment + cid = info and info.get('cid') + else: + continue + if isinstance(content, str): + content = content.encode('utf-8') + elif content is None: + continue + attachement_values= { + 'name': name, + 'datas': base64.b64encode(content), + 'type': 'binary', + 'datas_fname': name, + 'description': name, + 'res_model': model, + 'res_id': res_id, + } + if body and (cid and cid in cids_in_body or name in names_in_body): + attachement_values['access_token'] = self.env['ir.attachment']._generate_access_token() + attachement_values_list.append(attachement_values) + # keep cid and name list synced with attachement_values_list length to match ids latter + cid_list.append(cid) + name_list.append(name) + new_attachments = self.env['ir.attachment'].create(attachement_values_list) + cid_mapping = {} + name_mapping = {} + for counter, new_attachment in enumerate(new_attachments): + cid = cid_list[counter] + if 'access_token' in attachement_values_list[counter]: + if cid: + cid_mapping[cid] = (new_attachment.id, attachement_values_list[counter]['access_token']) + name = name_list[counter] + name_mapping[name] = (new_attachment.id, attachement_values_list[counter]['access_token']) + m2m_attachment_ids.append((4, new_attachment.id)) + + # note: right know we are only taking attachments and ignoring attachment_ids. + if (cid_mapping or name_mapping) and body: + postprocessed = False + for node in root.iter('img'): + attachment_data = False + if node.get('src', '').startswith('cid:'): + cid = node.get('src').split('cid:')[1] + attachment_data = cid_mapping.get(cid) + if not attachment_data and node.get('data-filename'): + attachment_data = name_mapping.get(node.get('data-filename'), False) + if attachment_data: + node.set('src', '/web/image/%s?access_token=%s' % attachment_data) + postprocessed = True + if postprocessed: + return_values['body'] = lxml.html.tostring(root, pretty_print=False, encoding='UTF-8') + return_values['attachment_ids'] = m2m_attachment_ids + return return_values @api.multi @api.returns('mail.message', lambda value: value.id) - def message_post(self, body='', subject=None, subtype_id=False, - message_type='notification', subtype=None, author_id=None, partner_ids=None, channel_ids=None, - parent_id=False, attachments=None, attachment_ids=None, model=False, - add_sign=True, model_description=False, - mail_auto_delete=True, **kwargs): + def message_post(self, + body='', subject=None, message_type='notification', + email_from=False, 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, model_description=False, mail_auto_delete=True, record_name=False, + **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. - :param int thread_id: thread ID to post into, or list with one ID; - if False/0, mail.message model will also be set as False :param str body: body of the message, usually raw HTML that will be sanitized - :param str type: see mail_message.message_type field + :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 int parent_id: handle reply to a previous message by adding the parent partners to the message in case of private discussion - :param tuple(str,str) attachments or list id: list of attachment tuples in the form - ``(name,content)``, where content is NOT base64 encoded + :param int subtype_id: subtype_id of the message, mainly use fore + followers mechanism + :param int subtype: xmlid that will be used to compute subtype_id + if subtype_id is not given. + :param list(int) partner_ids: partner_ids to notify + :param list(int) channel_ids: channel_ids to notify + :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 + to the related document. Extra keyword arguments will be used as default column values for the - new mail.message record. Special cases: - - attachment_ids: supposed not attached to any document; attach them - to the related document. Should only be set by Chatter. + new mail.message record. :return int: ID of newly created mail.message """ self.ensure_one() # should always be posted on a record, use message_notify if no record @@ -1683,16 +1716,21 @@ class MailThread(models.AbstractModel): if 'model' in kwargs or 'res_id' in kwargs: raise ValueError("message_post doesn't support model and res_id parameters anymore. Please call message_post on record") - # Find the message's author, because we need it for private discussion - if author_id is None: # keep False values - author_id = self.env['mail.message']._get_default_author().id # self.env.user.partner_id + self = self.with_lang() # add lang to context imediatly since it will be usefull in various flows latter. + + record_name = record_name or self.display_name partner_ids = set(partner_ids or []) channel_ids = set(channel_ids or []) - for pc_id in partner_ids | channel_ids: - if not isinstance(pc_id, int): - raise ValueError('message_post partner_ids and channel_ids must be integer list, not commands') + if any(not isinstance(pc_id, int) for pc_id in partner_ids | channel_ids): + raise ValueError('message_post partner_ids and channel_ids must be integer list, not commands') + + # Find the message's author, because we need it for private discussion + 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() if not subtype_id: subtype = subtype or 'mt_note' @@ -1702,33 +1740,30 @@ class MailThread(models.AbstractModel): # automatically subscribe recipients if asked to if self._context.get('mail_post_autofollow') and partner_ids: - partner_to_subscribe = partner_ids - if self._context.get('mail_post_autofollow_partner_ids'): - partner_to_subscribe = [p for p in partner_ids if p in self._context.get('mail_post_autofollow_partner_ids')] - self.message_subscribe(list(partner_to_subscribe)) + self.message_subscribe(list(partner_ids)) - # _mail_flat_thread: automatically set free messages to the first posted message - MailMessage = self.env['mail.message'] + MailMessage_sudo = self.env['mail.message'].sudo() if self._mail_flat_thread and not parent_id: - messages = MailMessage.search(['&', ('res_id', '=', self.ids[0]), ('model', '=', self._name), ('message_type', '!=', 'user_notification')], order="id ASC", limit=1) - parent_id = messages.ids and messages.ids[0] or False - # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread - elif parent_id: - messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1) + parent_message = MailMessage_sudo.search([('res_id', '=', self.id), ('model', '=', self._name), ('message_type', '!=', 'user_notification')], order="id ASC", limit=1) + # parent_message searched in sudo for performance, only used for id. + # Note that with sudo we will match message with internal subtypes. + parent_id = parent_message.id if parent_message else False + elif parent_id: + old_parent_id = parent_id + parent_message = MailMessage_sudo.search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1) # avoid loops when finding ancestors processed_list = [] - if messages: - message = messages[0] - while (message.parent_id and message.parent_id.id not in processed_list): - processed_list.append(message.parent_id.id) - message = message.parent_id - parent_id = message.id - - values = kwargs + if parent_message: + new_parent_id = parent_message.parent_id and parent_message.parent_id.id + while (new_parent_id and new_parent_id not in processed_list): + processed_list.append(new_parent_id) + parent_message = parent_message.parent_id + parent_id = parent_message.id + values = dict(kwargs) values.update({ 'author_id': author_id, 'model': self._name, - 'res_id': self._name and self.id or False, + 'res_id': self.id, 'body': body, 'subject': subject or False, 'message_type': message_type, @@ -1737,13 +1772,16 @@ class MailThread(models.AbstractModel): 'partner_ids': partner_ids, 'channel_ids': channel_ids, 'add_sign': add_sign, + 'record_name': record_name, + 'email_from': email_from, }) + 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] - # 3. Attachments - # - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message + new_message= self._message_create(values) - values['attachment_ids'] = self._message_post_process_attachments(attachments or [], attachment_ids or [], values) - new_message = self._message_create(values) # Set main attachment field if necessary self._message_set_main_attachment_id(values['attachment_ids']) @@ -1761,7 +1799,7 @@ class MailThread(models.AbstractModel): prioritary_attachments = all_attachments.filtered(lambda x: x.mimetype.endswith('pdf')) \ or all_attachments.filtered(lambda x: x.mimetype.startswith('image')) \ or all_attachments - self.sudo().write({'message_main_attachment_id': prioritary_attachments[0].id}) + self.sudo().with_context(tracking_disable=True).write({'message_main_attachment_id': prioritary_attachments[0].id}) def _message_post_after_hook(self, message, msg_vals): """ Hook to add custom behavior after having posted the message. Both @@ -1833,7 +1871,6 @@ class MailThread(models.AbstractModel): displayed on a document. It pushes notifications on inbox or by email depending on the user configuration, like other notifications. """ - # should we handle channel_ids here? if self: self.ensure_one() @@ -1844,7 +1881,7 @@ class MailThread(models.AbstractModel): if not author.email: raise exceptions.UserError(_("Unable to notify message, please configure the sender's email address.")) - email_from = formataddr((author.name, author.email)) + email_from = author.email_formatted partner_ids = partner_ids or set() if parent_id: # looks like no test case are going throug this condition. Linked to private discussion. This may be removed soon. @@ -1928,7 +1965,6 @@ class MailThread(models.AbstractModel): create_values['channel_ids'] = [(4, cid) for cid in create_values.get('channel_ids', [])] return self.env['mail.message'].create(create_values) - # ------------------------------------------------------ # Notification API # ------------------------------------------------------ @@ -1970,20 +2006,28 @@ class MailThread(models.AbstractModel): # (could also be interresting for, we could add partners with r['notif'] = 'ocn_client' and r['needaction']=False) # then overide a notify_recipients (as it was before) to effectively send ocn notifications. # enveloppe will contain more needaction, those for the member of a email channel. - if message_values and self and hasattr(self, '_notify_customize_recipients'): - message_values.update(self._notify_customize_recipients(message, msg_vals, rdata)) + if message_values and self: + message_values.update(self._notify_customize_recipients(message, msg_vals)) if message_values: message.write(message_values) inbox_pids = [r['id'] for r in rdata['partners'] if r['notif'] == 'inbox'] partner_email_rdata = [r for r in rdata['partners'] if r['notif'] == 'email'] + channel_ids = [r['id'] for r in rdata['channels']] + + notifications = [] + if inbox_pids or channel_ids: + message_values = False + if inbox_pids: + message_values = message.message_format()[0] + for partner in self.env['res.partner'].browse(inbox_pids): + notifications.append([(self._cr.dbname, 'ir.needaction', partner), dict(message_values)]) + if channel_ids: + notifications += self.env['mail.channel'].sudo().browse(channel_ids)._channel_message_notifications(message, message_values) if partner_email_rdata: self._notify_record_by_email(message, partner_email_rdata, msg_vals=msg_vals, model_description=model_description, mail_auto_delete=mail_auto_delete) - if inbox_pids: - self.env['res.partner'].browse(inbox_pids)._notify_by_chat(message) - if rdata['channels']: - # send a notification on bus to update channel - self.env['mail.channel'].sudo().browse([r['id'] for r in rdata['channels']])._notify(message) + if notifications: + self.env['bus.bus'].sudo().sendmany(notifications) return True def _notify_record_by_email(self, message, partners_data, msg_vals=False, model_description=False, mail_auto_delete=True, send_after_commit=False): @@ -1998,7 +2042,7 @@ class MailThread(models.AbstractModel): :param mail_auto_delete: delete notification emails once sent; """ model = msg_vals.get('model') if msg_vals else message.model - model_name = model_description or (self.with_lang().env['ir.model']._get(model).display_name if model else False) + model_name = model_description or (self.with_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name recipients_groups_data = self._notify_classify_recipients(partners_data, model_name) if not recipients_groups_data: @@ -2006,21 +2050,25 @@ class MailThread(models.AbstractModel): force_send = self.env.context.get('mail_notify_force_send', True) - base_template_ctx = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) + template_values = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) # 10 queries - template_xmlid = message.email_layout_xmlid if message.email_layout_xmlid else 'mail.message_notification_email' + 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' try: - base_template = self.env.ref(template_xmlid, raise_if_not_found=True).with_context(lang=base_template_ctx['lang']) + base_template = self.env.ref(template_xmlid, raise_if_not_found=True).with_context(lang=template_values['lang']) # 1 query except ValueError: _logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid)) base_template = False + + mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries # prepare notification mail values base_mail_values = { 'mail_message_id': message.id, - 'mail_server_id': message.mail_server_id.id, + 'mail_server_id': message.mail_server_id.id, # 2 query, check acces + read, may be useless, Falsy, when will it be used? 'auto_delete': mail_auto_delete, - 'references': message.parent_id.message_id if message.parent_id else False + 'references': message.parent_id.message_id if message.parent_id else False, + 'subject': mail_subject, } headers = self._notify_email_headers() if headers: @@ -2028,24 +2076,24 @@ class MailThread(models.AbstractModel): Mail = self.env['mail.mail'].sudo() emails = self.env['mail.mail'].sudo() - email_pids = set() # loop on groups (customer, portal, user, ... + model specific like group_sale_salesman) recipients_max = 50 for recipients_group_data in recipients_groups_data: # generate notification email content - template_ctx = {**base_template_ctx, **recipients_group_data} + recipients_ids = recipients_group_data.pop('recipients') + render_values = {**template_values, **recipients_group_data} # {company, is_discussion, lang, message, model_description, record, record_name, signature, subtype, tracking_values, website_url} # {actions, button_access, has_button_access, recipients} + if base_template: - mail_body = base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True) + mail_body = base_template.render(render_values, engine='ir.qweb', minimal_qcontext=True) else: mail_body = message.body mail_body = self._replace_local_links(mail_body) - mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # send email - for email_chunk in split_every(recipients_max, recipients_group_data['recipients']): - recipient_values = self._notify_email_recipient_values(email_chunk) + for recipients_ids_chunk in split_every(recipients_max, recipients_ids): + recipient_values = self._notify_email_recipient_values(recipients_ids_chunk) email_to = recipient_values['email_to'] recipient_ids = recipient_values['recipient_ids'] @@ -2062,7 +2110,11 @@ class MailThread(models.AbstractModel): if email and recipient_ids: notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', '=', email.mail_message_id.id), - ('res_partner_id', 'in', list(recipient_ids)) + ('res_partner_id', 'in', list(recipient_ids)) # not sure to check. + # TODO XDO what if recipient_ids are empty because of _notify_email_recipient_values + # should we use recipients_ids_chunk? + # should we unlink recipients_ids_chunk - recipient_ids ? + # should we avoid to create needation? by calling _notify_email_recipient_values at the same place _notify_customize_recipients does? (but no chubnk at this step) ]) notifications.write({ 'is_email': True, @@ -2071,7 +2123,6 @@ class MailThread(models.AbstractModel): 'email_status': 'ready', }) emails |= email - email_pids.update(recipient_ids) # NOTE: # 1. for more than 50 followers, use the queue system @@ -2080,13 +2131,12 @@ class MailThread(models.AbstractModel): # using the command-line. test_mode = getattr(threading.currentThread(), 'testing', False) if force_send and len(emails) < recipients_max and (not self.pool._init or test_mode): - email_ids = emails.ids - dbname = self.env.cr.dbname - _context = self._context - # unless asked specifically, send emails after the transaction to # avoid side effects due to emails being sent while the transaction fails if not test_mode and send_after_commit: + email_ids = emails.ids + dbname = self.env.cr.dbname + _context = self._context def send_notifications(): db_registry = registry(dbname) with api.Environment.manage(), db_registry.cursor() as cr: @@ -2103,13 +2153,22 @@ class MailThread(models.AbstractModel): # compute send user and its related signature signature = '' user = self.env.user - if message.author_id and message.author_id.user_ids: - user = message.author_id.user_ids[0] - if message.add_sign: + 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. + if author_user: + user = author_user + if add_sign: signature = user.signature else: - if message.add_sign: - signature = "<p>-- <br/>%s</p>" % message.author_id.name + if add_sign: + signature = "<p>-- <br/>%s</p>" % author.name company = self.company_id.sudo() if self and 'company_id' in self else user.company_id if company.website: @@ -2125,11 +2184,11 @@ class MailThread(models.AbstractModel): if template and template.lang: lang = template._render_template(template.lang, self.env.context['default_model'], self.env.context['default_res_id']) - if not model_description and message.model: - model_description = self.env['ir.model'].with_context(lang=lang)._get(message.model).display_name + if not model_description and model: + model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name tracking = [] - if msg_vals.get('tracking_value_ids') if msg_vals else bool(self): # could be 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)]): groups = tracking_value.field_groups if not groups or self.user_has_groups(groups): @@ -2137,7 +2196,7 @@ class MailThread(models.AbstractModel): tracking_value.get_old_display_value()[0], tracking_value.get_new_display_value()[0])) - is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') + is_discussion = subtype_id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') return { 'message': message, @@ -2146,7 +2205,7 @@ class MailThread(models.AbstractModel): 'company': company, 'model_description': model_description, 'record': self, - 'record_name': message.record_name, + 'record_name': record_name, 'tracking_values': tracking, 'is_discussion': is_discussion, 'subtype': message.subtype_id, @@ -2452,6 +2511,9 @@ class MailThread(models.AbstractModel): 'email_to': False, 'recipient_ids': recipient_ids, } + @api.multi + def _notify_customize_recipients(self, message, msg_vals): + return {} # ------------------------------------------------------ # Followers API @@ -2558,19 +2620,16 @@ class MailThread(models.AbstractModel): :param default_subtype_ids: coming from ``_get_auto_subscription_subtypes`` """ fnames = [] - for name, field in self._fields.items(): - if name == 'user_id' and updated_values.get(name) and (getattr(field, 'track_visibility', False) or getattr(field, 'tracking', False)): - if field.comodel_name == 'res.users': - fnames.append(name) - - new_subscriptions = [] - user_ids = [updated_values[fname] for fname in fnames if updated_values[fname]] - if user_ids: - new_pids = self.env['res.partner'].sudo().search([('user_ids', 'in', user_ids), ('active', '=', True)]).ids - for new_pid in new_pids: - new_subscriptions.append((new_pid, default_subtype_ids, 'mail.message_user_assigned' if new_pid != self.env.user.partner_id.id else False)) - - return new_subscriptions + field = self._fields.get('user_id') + user_id = updated_values.get('user_id') + if field and user_id and field.comodel_name == 'res.users' and (getattr(field, 'track_visibility', False) or getattr(field, 'tracking', False)): + user = self.env['res.users'].sudo().browse(user_id) + try: # avoid to make an exists, lets be optimistic and try to read it. + if user.active: + return [(user.partner_id.id, default_subtype_ids, 'mail.message_user_assigned' if user != self.env.user else False)] + except: + pass + return [] @api.multi def _message_auto_subscribe_notify(self, partner_ids, template): diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 4cd62ac35c3..e2abaf7d75b 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -37,17 +37,6 @@ class Partner(models.Model): 'email_cc': False} for r in self} - @api.multi - def _notify_by_chat(self, message): - """ Broadcast the message to all the partner since """ - if not self: - return - message_values = message.message_format()[0] - notifications = [] - for partner in self: - notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)]) - self.env['bus.bus'].sendmany(notifications) - @api.model def get_needaction_count(self): """ compute the number of needaction of the current user """ diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 2db6a3d8324..1d54406b086 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -367,7 +367,7 @@ class MailComposer(models.TransientModel): mail_values.pop('attachments', []), attachment_ids, {'model': 'mail.message', 'res_id': 0} - ) + )['attachment_ids'] # Filter out the blacklisted records by setting the mail state to cancel -> Used for Mass Mailing stats if res_id in blacklisted_rec_ids: mail_values['state'] = 'cancel' |
