[IMP] mail: reduce message_post query count and clean code

Purpose of this commit is to improve performance of post and notify through
various code improvements.

Containing
  * making of _notify_customize_recipients a mail.thread method;
  * _message_auto_subscribe_followers optimized;
  * renaming and optimisation of _notify_partners;
  * message_post_attachements optization;
  * remove unused mail_post_autofollow_partner_ids context key;
  * minimize message create queries;
  * only check record related access right for thread messages not in pending
    moderation;
  * get attachements from values instead of db;
  * storing lang in ctx from the beginning;
  * don't track on write on attachments;

Related to task 1943901
Linked to PR #32404
This commit is contained in:
Xavier-Do
2019-05-29 13:34:32 +00:00
committed by Thibault Delavallée
parent 2fcb3c658d
commit de7c119e66
12 changed files with 363 additions and 277 deletions
+2 -2
View File
@@ -48,7 +48,7 @@ class MailChannel(models.Model):
record.is_chat = True
@api.multi
def _channel_message_notifications(self, message):
def _channel_message_notifications(self, message, message_format=False):
""" When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when
clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header
should be added to the notification, since the user cannot be listining to the channel.
@@ -56,7 +56,7 @@ class MailChannel(models.Model):
livechat_channels = self.filtered(lambda x: x.channel_type == 'livechat')
other_channels = self.filtered(lambda x: x.channel_type != 'livechat')
notifications = super(MailChannel, livechat_channels)._channel_message_notifications(message.with_context(im_livechat_use_username=True)) + \
super(MailChannel, other_channels)._channel_message_notifications(message)
super(MailChannel, other_channels)._channel_message_notifications(message, message_format)
for channel in self:
# add uuid for private livechat channels to allow anonymous to listen
if channel.channel_type == 'livechat' and channel.public == 'private':
+4 -18
View File
@@ -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
+85 -42
View File
@@ -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()
# --------------------------------------------------
+220 -161
View File
@@ -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))
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:
body = lxml.html.tostring(root, pretty_print=False, encoding='UTF-8')
message_data['body'] = body
return m2m_attachment_ids
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):
-11
View File
@@ -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 """
+1 -1
View File
@@ -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'
-7
View File
@@ -28,13 +28,6 @@ class TestChatterTweaks(BaseFunctionalTest, TestRecipients):
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_1 | self.partner_2)
self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
def test_post_subscribe_recipients_partial(self):
original = self.test_record.message_follower_ids
self.test_record.sudo(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True, 'mail_post_autofollow_partner_ids': [self.partner_2.id]}).message_post(
body='Test Body', message_type='comment', subtype='mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id])
self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_2)
self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id'))
def test_chatter_mail_create_nolog(self):
""" Test disable of automatic chatter message at create """
rec = self.env['mail.test.simple'].sudo(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'})
@@ -218,6 +218,7 @@ class TestMessagePost(BaseFunctionalTest, MockEmails, TestRecipients):
self.assertEqual(new_notification.email_from, formataddr((self.env.user.name, self.env.user.email)))
self.assertEqual(new_notification.needaction_partner_ids, self.partner_1 | self.user_employee.partner_id)
self.assertNotIn(new_notification, self.test_record.message_ids)
# todo xdo add test message_notify on thread with followers and stuff
class TestComposer(BaseFunctionalTest, MockEmails, TestRecipients):
+24 -25
View File
@@ -83,7 +83,7 @@ class TestMailPerformance(TransactionCase):
'partner_id': self.env.ref('base.res_partner_12').id,
})
with self.assertQueryCount(__system__=5, demo=5): # test_mail only: 5 - 5
with self.assertQueryCount(__system__=4, demo=4): # test_mail only: 4 - 4
record.track = 'X'
@users('__system__', 'demo')
@@ -99,13 +99,13 @@ class TestMailPerformance(TransactionCase):
@warmup
def test_create_mail_with_tracking(self):
""" Create records inheriting from 'mail.thread' (with field tracking). """
with self.assertQueryCount(__system__=10, demo=10): # test_mail only: 10 - 10
with self.assertQueryCount(__system__=9, demo=9): # test_mail only: 9 - 9
self.env['test_performance.mail'].create({'name': 'X'})
@users('__system__', 'emp')
@warmup
def test_create_mail_simple(self):
with self.assertQueryCount(__system__=8, emp=8): # test_mail only: 8 - 8
with self.assertQueryCount(__system__=7, emp=7): # test_mail only: 7 - 7
self.env['mail.test.simple'].create({'name': 'Test'})
@users('__system__', 'emp')
@@ -161,7 +161,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_adv_activity(self):
model = self.env['mail.test.activity']
with self.assertQueryCount(__system__=9, emp=8): # test_mail only: 9 - 8
with self.assertQueryCount(__system__=8, emp=7): # test_mail only: 8 - 7
model.create({'name': 'Test'})
@users('__system__', 'emp')
@@ -173,7 +173,7 @@ class TestAdvMailPerformance(TransactionCase):
'default_res_model': 'mail.test.activity',
})
with self.assertQueryCount(__system__=10, emp=17): # com runbot: 10 - 16 // test_mail only: 10 - 15
with self.assertQueryCount(__system__=10, emp=13): # com runbot: 10 - 13 // test_mail only: 10 - 13
activity = MailActivity.create({
'summary': 'Test Activity',
'res_id': record.id,
@@ -183,7 +183,7 @@ class TestAdvMailPerformance(TransactionCase):
#voip module read activity_type during create leading to one less query in enterprise on action_feedback
category = activity.activity_type_id.category
with self.assertQueryCount(__system__=24, emp=41): # com runbot: 24 - 41 // test_mail only: 24 - 39
with self.assertQueryCount(__system__=24, emp=31): # com runbot: 24 - 31 // test_mail only: 24 - 31
activity.action_feedback(feedback='Zizisse Done !')
@users('__system__', 'emp')
@@ -192,7 +192,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_adv_activity_mixin(self):
record = self.env['mail.test.activity'].create({'name': 'Test'})
with self.assertQueryCount(__system__=10, emp=17): # com runbot: 10 - 16 // test_mail only: 10 - 15
with self.assertQueryCount(__system__=10, emp=14): # com runbot: 10 - 14 // test_mail only: 10 - 14
activity = record.action_start('Test Start')
#read activity_type to normalize cache between enterprise and community
#voip module read activity_type during create leading to one less query in enterprise on action_close
@@ -200,7 +200,7 @@ class TestAdvMailPerformance(TransactionCase):
record.write({'name': 'Dupe write'})
with self.assertQueryCount(__system__=26, emp=42): # com runbot: 26 - 42 // test_mail only: 26 - 40
with self.assertQueryCount(__system__=26, emp=31): # com runbot: 26 - 31 // test_mail only: 26 - 31
record.action_close('Dupe feedback')
self.assertEqual(record.activity_ids, self.env['mail.activity'])
@@ -211,7 +211,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_assignation_email(self):
self.user_test.write({'notification_type': 'email'})
record = self.env['mail.test.track'].create({'name': 'Test'})
with self.assertQueryCount(__system__=61, emp=76): # com runbot: 61 - 76 // test_mail only: 61 - 71
with self.assertQueryCount(__system__=56, emp=59): # com runbot: 56 - 59 // test_mail only: 56 - 59
record.write({
'user_id': self.user_test.id,
})
@@ -220,7 +220,7 @@ class TestAdvMailPerformance(TransactionCase):
@warmup
def test_message_assignation_inbox(self):
record = self.env['mail.test.track'].create({'name': 'Test'})
with self.assertQueryCount(__system__=37, emp=44): # test_mail only: 37 - 44
with self.assertQueryCount(__system__=34, emp=40): # test_mail only: 34 - 40
record.write({
'user_id': self.user_test.id,
})
@@ -240,7 +240,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_log_with_post(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=7, emp=13): # test_mail only: 7 - 13
with self.assertQueryCount(__system__=6, emp=7): # test_mail only: 6 - 7
record.message_post(
body='<p>Test message_post as log</p>',
subtype='mail.mt_note',
@@ -251,7 +251,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_no_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=7, emp=13): # test_mail only: 7 - 13
with self.assertQueryCount(__system__=6, emp=7): # test_mail only: 6 - 7
record.message_post(
body='<p>Test Post Performances basic</p>',
partner_ids=[],
@@ -264,7 +264,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_one_email_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=52, emp=72): # com runbot: 49 - 69 // test_mail only: 52 - 69
with self.assertQueryCount(__system__=47, emp=51): # com runbot: 47 - 51 // test_mail only: 47 - 51
record.message_post(
body='<p>Test Post Performances with an email ping</p>',
partner_ids=self.customer.ids,
@@ -276,7 +276,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_one_inbox_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
with self.assertQueryCount(__system__=32, emp=44): # com runbot 31 - 43 // test_mail only: 32 - 44
with self.assertQueryCount(__system__=30, emp=36): # com runbot 30 - 36 // test_mail only: 30 - 36
record.message_post(
body='<p>Test Post Performances with an inbox ping</p>',
partner_ids=self.user_test.partner_id.ids,
@@ -386,8 +386,7 @@ class TestHeavyMailPerformance(TransactionCase):
'recipient_ids': [(4, pid) for pid in self.partners.ids],
})
mail_ids = mail.ids
with self.assertQueryCount(__system__=14, emp=21): # test_mail only: 14 - 21
with self.assertQueryCount(__system__=16, emp=22): # test_mail only: 16 - 22
self.env['mail.mail'].browse(mail_ids).send()
self.assertEqual(mail.body_html, '<p>Test</p>')
@@ -400,7 +399,7 @@ class TestHeavyMailPerformance(TransactionCase):
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
record = self.umbrella.sudo(self.env.user)
with self.assertQueryCount(__system__=85, emp=108): # com runbot: 82 - 105 // test_mail only: 85 - 105
with self.assertQueryCount(__system__=78, emp=82): # com runbot: 78 - 82 // test_mail only: 78 - 82
record.message_post(
body='<p>Test Post Performances</p>',
message_type='comment',
@@ -417,7 +416,7 @@ class TestHeavyMailPerformance(TransactionCase):
record = self.umbrella.sudo(self.env.user)
template_id = self.env.ref('test_mail.mail_test_tpl').id
with self.assertQueryCount(__system__=102, emp=126): # com runbot: 99 - 123 // test_mail only: 102 - 123
with self.assertQueryCount(__system__=95, emp=101): # com runbot: 95 - 101 // test_mail only: 95 - 101
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
@@ -486,7 +485,7 @@ class TestHeavyMailPerformance(TransactionCase):
'user_id': self.env.uid,
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
with self.assertQueryCount(__system__=61, emp=75): # com runbot: 60 - 75 // test_mail only: 60 - 72
with self.assertQueryCount(__system__=55, emp=58): # com runbot: 55 - 58 // test_mail only: 55 - 58
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
# write tracking message
@@ -506,7 +505,7 @@ class TestHeavyMailPerformance(TransactionCase):
customer_id = self.customer.id
user_id = self.user_portal.id
with self.assertQueryCount(__system__=155, emp=172): # com runbot: 155 - 172 // test_mail only: 154 - 168
with self.assertQueryCount(__system__=147, emp=155): # com runbot: 147 - 155 // test_mail only: 147 - 155
rec = self.env['mail.test.full'].create({
'name': 'Test',
'umbrella_id': umbrella_id,
@@ -533,7 +532,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
self.assertEqual(len(rec.message_ids), 1)
with self.assertQueryCount(__system__=99, emp=122): # com runbot: 99 - 122 // test_mail only: 99 - 119
with self.assertQueryCount(__system__=96, emp=101): # com runbot: 96 - 101 // test_mail only: 96 - 101
rec.write({
'name': 'Test2',
'umbrella_id': self.umbrella.id,
@@ -569,7 +568,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
with self.assertQueryCount(__system__=105, emp=127): # test_mail only: 104 - 123
with self.assertQueryCount(__system__=101, emp=106): # test_mail only: 101 - 106
rec.write({
'name': 'Test2',
'umbrella_id': umbrella_id,
@@ -601,7 +600,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
with self.assertQueryCount(__system__=52, emp=66): # test_mail only: 51 - 66
with self.assertQueryCount(__system__=50, emp=62): # test_mail only: 50 - 62
rec.write({
'name': 'Test2',
'customer_id': customer_id,
@@ -620,7 +619,7 @@ class TestHeavyMailPerformance(TransactionCase):
self.assertEqual(len(rec.message_ids), 3)
@tagged('mail_performance_post')
@tagged('mail_performance')
class TestMailPerformancePost(TransactionCase):
def setUp(self):
@@ -753,7 +752,7 @@ class TestMailPerformancePost(TransactionCase):
]
self.attachements = self.env['ir.attachment'].sudo(self.env.user).create(self.vals) #-> 163-> 165 query
attachement_ids = self.attachements.ids
with self.assertQueryCount(emp=248): # test_mail only: 210
with self.assertQueryCount(emp=163): # com runbot 156 // test_mail only: 133
self.cr.sql_log = self.warm and self.cr.sql_log_count
record.with_context({}).message_post(
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
+2 -2
View File
@@ -259,14 +259,14 @@ class BlogPost(models.Model):
return groups
@api.multi
def _notify_customize_recipients(self, message, msg_vals, recipients_vals):
def _notify_customize_recipients(self, message, msg_vals):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
msg_type = msg_vals.get('message_type') or message.message_type
if msg_type == 'comment':
return {'needaction_partner_ids': []}
return {}
return super(BlogPost, self)._notify_customize_recipients(message, msg_vals)
def _default_website_meta(self):
res = super(BlogPost, self)._default_website_meta()
+5 -4
View File
@@ -422,12 +422,13 @@ class Post(models.Model):
return post
@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):
# XDO FIXME: to be correctly fixed with new get_mail_message_access and filter access rule
if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'):
# Make sure only author or moderator can edit/delete messages
if any(not post.can_edit for post in self.browse(res_ids)):
raise KarmaError('Not enough karma to edit a post.')
return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name)
return super(Post, self).get_mail_message_access(res_ids, operation, model_name=model_name)
@api.multi
def write(self, vals):
@@ -805,14 +806,14 @@ class Post(models.Model):
return super(Post, self).message_post(message_type=message_type, **kwargs)
@api.multi
def _notify_customize_recipients(self, message, msg_vals, recipients_vals):
def _notify_customize_recipients(self, message, msg_vals):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
msg_type = msg_vals.get('message_type') or message.message_type
if msg_type == 'comment':
return {'needaction_partner_ids': [], 'partner_ids': []}
return {}
return super(Post, self)._notify_customize_recipients(message, msg_vals)
class PostReason(models.Model):
+19 -4
View File
@@ -11,7 +11,7 @@ from collections import defaultdict
import uuid
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessError, ValidationError
from odoo.exceptions import AccessError, ValidationError, MissingError
from odoo.tools import config, human_size, ustr, html_escape
from odoo.tools.mimetypes import guess_mimetype
@@ -313,6 +313,8 @@ class IrAttachment(models.Model):
def _check_serving_attachments(self):
# restrict writing on attachments that could be served by the
# ir.http's dispatch exception handling
# XDO note: this should be done in check(write), constraints for access rights?
# XDO note: if read on sudo, read twice, one for constraints, one for _inverse_datas as user
if self.env.user._is_admin():
return
if self.type == 'binary' and self.url:
@@ -326,13 +328,15 @@ class IrAttachment(models.Model):
In the 'document' module, it is overriden to relax this hard rule, since
more complex ones apply there.
"""
if self.env.user._is_superuser():
return True
# collect the records to check (by model)
model_ids = defaultdict(set) # {model_name: set(ids)}
require_employee = False
if self:
self._cr.execute('SELECT res_model, res_id, create_uid, public, res_field FROM ir_attachment WHERE id IN %s', [tuple(self.ids)])
for res_model, res_id, create_uid, public, res_field in self._cr.fetchall():
if self.env.user.id != SUPERUSER_ID and not self.env.user._is_system() and res_field:
if not self.env.user._is_system() and res_field:
raise AccessError(_("Sorry, you are not allowed to access this document."))
if public and mode == 'read':
continue
@@ -488,12 +492,20 @@ class IrAttachment(models.Model):
@api.model_create_multi
def create(self, vals_list):
record_tuple_set = set()
for values in vals_list:
# remove computed field depending of datas
for field in ('file_size', 'checksum'):
values.pop(field, False)
values = self._check_contents(values)
self.browse().check('write', values=values)
# 'check()' only uses res_model and res_id from values, and make an exists.
# We can group the values by model, res_id to make only one query when
# creating multiple attachments on a single record.
record_tuple = (values.get('res_model'), values.get('res_id'))
record_tuple_set.add(record_tuple)
for record_tuple in record_tuple_set:
(res_model, res_id) = record_tuple
self.check('write', values={'res_model':res_model, 'res_id':res_id})
return super(IrAttachment, self).create(vals_list)
@api.multi
@@ -504,10 +516,13 @@ class IrAttachment(models.Model):
def generate_access_token(self):
if self.access_token:
return self.access_token
access_token = str(uuid.uuid4())
access_token = self._generate_access_token()
self.write({'access_token': access_token})
return access_token
def _generate_access_token(self):
return str(uuid.uuid4())
@api.model
def action_get(self):
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')