Previously to this commit needaction used by JS was not taking into account
read notifications. It means notably Inbox counter could be higher than
expected as taking into account read messages while Inbox displays only
unread messages. This commit fixes that by checking notification state when
giving needaction_partner_ids to message_format result.
This fix has a small impact on performances as message_format is used in
notification process for chat or push. A better implementation of message
format and its postprocess could probably lessen this fix impact. As we
target stable version we avoid rewriting other part of the code. Optimization
will be done in development version.
Related to task ID 1841243. This is a manual forward port of commit 69714fbeee
done in saas-11.2.
865 lines
44 KiB
Python
865 lines
44 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import re
|
|
|
|
from email.utils import formataddr
|
|
|
|
from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
|
|
from odoo.exceptions import UserError, AccessError
|
|
from odoo.osv import expression
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])', re.I)
|
|
|
|
|
|
class Message(models.Model):
|
|
""" Messages model: system notification (replacing res.log notifications),
|
|
comments (OpenChatter discussion) and incoming emails. """
|
|
_name = 'mail.message'
|
|
_description = 'Message'
|
|
_order = 'id desc'
|
|
_rec_name = 'record_name'
|
|
|
|
_message_read_limit = 30
|
|
|
|
@api.model
|
|
def _get_default_from(self):
|
|
if self.env.user.email:
|
|
return formataddr((self.env.user.name, self.env.user.email))
|
|
raise UserError(_("Unable to post message, please configure the sender's email address."))
|
|
|
|
@api.model
|
|
def _get_default_author(self):
|
|
return self.env.user.partner_id
|
|
|
|
# content
|
|
subject = fields.Char('Subject')
|
|
date = fields.Datetime('Date', default=fields.Datetime.now)
|
|
body = fields.Html('Contents', default='', sanitize_style=True, strip_classes=True)
|
|
attachment_ids = fields.Many2many(
|
|
'ir.attachment', 'message_attachment_rel',
|
|
'message_id', 'attachment_id',
|
|
string='Attachments',
|
|
help='Attachments are linked to a document through model / res_id and to the message '
|
|
'through this field.')
|
|
parent_id = fields.Many2one(
|
|
'mail.message', 'Parent Message', index=True, ondelete='set null',
|
|
help="Initial thread message.")
|
|
child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages')
|
|
# related document
|
|
model = fields.Char('Related Document Model', index=True)
|
|
res_id = fields.Integer('Related Document ID', index=True)
|
|
record_name = fields.Char('Message Record Name', help="Name get of the related document.")
|
|
# characteristics
|
|
message_type = fields.Selection([
|
|
('email', 'Email'),
|
|
('comment', 'Comment'),
|
|
('notification', 'System notification')],
|
|
'Type', required=True, default='email',
|
|
help="Message type: email for email message, notification for system "
|
|
"message, comment for other messages such as user replies",
|
|
oldname='type')
|
|
subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True)
|
|
mail_activity_type_id = fields.Many2one(
|
|
'mail.activity.type', 'Mail Activity Type',
|
|
index=True, ondelete='set null')
|
|
# origin
|
|
email_from = fields.Char(
|
|
'From', default=_get_default_from,
|
|
help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.")
|
|
author_id = fields.Many2one(
|
|
'res.partner', 'Author', index=True,
|
|
ondelete='set null', default=_get_default_author,
|
|
help="Author of the message. If not set, email_from may hold an email address that did not match any partner.")
|
|
author_avatar = fields.Binary("Author's avatar", related='author_id.image_small')
|
|
# recipients
|
|
partner_ids = fields.Many2many('res.partner', string='Recipients')
|
|
needaction_partner_ids = fields.Many2many(
|
|
'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action')
|
|
needaction = fields.Boolean(
|
|
'Need Action', compute='_get_needaction', search='_search_needaction',
|
|
help='Need Action')
|
|
channel_ids = fields.Many2many(
|
|
'mail.channel', 'mail_message_mail_channel_rel', string='Channels')
|
|
# notifications
|
|
notification_ids = fields.One2many(
|
|
'mail.notification', 'mail_message_id', 'Notifications',
|
|
auto_join=True, copy=False)
|
|
# user interface
|
|
starred_partner_ids = fields.Many2many(
|
|
'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By')
|
|
starred = fields.Boolean(
|
|
'Starred', compute='_get_starred', search='_search_starred',
|
|
help='Current user has a starred notification linked to this message')
|
|
# tracking
|
|
tracking_value_ids = fields.One2many(
|
|
'mail.tracking.value', 'mail_message_id',
|
|
string='Tracking values',
|
|
groups="base.group_no_one",
|
|
help='Tracked values are stored in a separate model. This field allow to reconstruct '
|
|
'the tracking and to generate statistics on the model.')
|
|
# mail gateway
|
|
no_auto_thread = fields.Boolean(
|
|
'No threading for answers',
|
|
help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.')
|
|
message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
|
|
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
|
|
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
|
|
|
|
@api.multi
|
|
def _get_needaction(self):
|
|
""" Need action on a mail.message = notified on my channel """
|
|
my_messages = self.env['mail.notification'].sudo().search([
|
|
('mail_message_id', 'in', self.ids),
|
|
('res_partner_id', '=', self.env.user.partner_id.id),
|
|
('is_read', '=', False)]).mapped('mail_message_id')
|
|
for message in self:
|
|
message.needaction = message in my_messages
|
|
|
|
@api.model
|
|
def _search_needaction(self, operator, operand):
|
|
if operator == '=' and operand:
|
|
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)]
|
|
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)]
|
|
|
|
@api.depends('starred_partner_ids')
|
|
def _get_starred(self):
|
|
""" Compute if the message is starred by the current user. """
|
|
# TDE FIXME: use SQL
|
|
starred = self.sudo().filtered(lambda msg: self.env.user.partner_id in msg.starred_partner_ids)
|
|
for message in self:
|
|
message.starred = message in starred
|
|
|
|
@api.model
|
|
def _search_starred(self, operator, operand):
|
|
if operator == '=' and operand:
|
|
return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])]
|
|
return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])]
|
|
|
|
#------------------------------------------------------
|
|
# Notification API
|
|
#------------------------------------------------------
|
|
|
|
@api.model
|
|
def mark_all_as_read(self, channel_ids=None, domain=None):
|
|
""" Remove all needactions of the current partner. If channel_ids is
|
|
given, restrict to messages written in one of those channels. """
|
|
partner_id = self.env.user.partner_id.id
|
|
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
|
if not domain and delete_mode:
|
|
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
|
args = [(partner_id,)]
|
|
if channel_ids:
|
|
query += """
|
|
AND mail_message_id in
|
|
(SELECT mail_message_id
|
|
FROM mail_message_mail_channel_rel
|
|
WHERE mail_channel_id in %s)"""
|
|
args += [tuple(channel_ids)]
|
|
query += " RETURNING mail_message_id as id"
|
|
self._cr.execute(query, args)
|
|
self.invalidate_cache()
|
|
|
|
ids = [m['id'] for m in self._cr.dictfetchall()]
|
|
else:
|
|
# not really efficient method: it does one db request for the
|
|
# search, and one for each message in the result set to remove the
|
|
# current user from the relation.
|
|
msg_domain = [('needaction_partner_ids', 'in', partner_id)]
|
|
if channel_ids:
|
|
msg_domain += [('channel_ids', 'in', channel_ids)]
|
|
unread_messages = self.search(expression.AND([msg_domain, domain]))
|
|
notifications = self.env['mail.notification'].sudo().search([
|
|
('mail_message_id', 'in', unread_messages.ids),
|
|
('res_partner_id', '=', self.env.user.partner_id.id),
|
|
('is_read', '=', False)])
|
|
if delete_mode:
|
|
notifications.unlink()
|
|
else:
|
|
notifications.write({'is_read': True})
|
|
ids = unread_messages.mapped('id')
|
|
|
|
notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids}
|
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
|
|
|
|
return ids
|
|
|
|
@api.multi
|
|
def set_message_done(self):
|
|
""" Remove the needaction from messages for the current partner. """
|
|
partner_id = self.env.user.partner_id
|
|
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
|
|
|
notifications = self.env['mail.notification'].sudo().search([
|
|
('mail_message_id', 'in', self.ids),
|
|
('res_partner_id', '=', partner_id.id),
|
|
('is_read', '=', False)])
|
|
|
|
if not notifications:
|
|
return
|
|
|
|
# notifies changes in messages through the bus. To minimize the number of
|
|
# notifications, we need to group the messages depending on their channel_ids
|
|
groups = []
|
|
messages = notifications.mapped('mail_message_id')
|
|
current_channel_ids = messages[0].channel_ids
|
|
current_group = []
|
|
for record in messages:
|
|
if record.channel_ids == current_channel_ids:
|
|
current_group.append(record.id)
|
|
else:
|
|
groups.append((current_group, current_channel_ids))
|
|
current_group = [record.id]
|
|
current_channel_ids = record.channel_ids
|
|
|
|
groups.append((current_group, current_channel_ids))
|
|
current_group = [record.id]
|
|
current_channel_ids = record.channel_ids
|
|
|
|
if delete_mode:
|
|
notifications.unlink()
|
|
else:
|
|
notifications.write({'is_read': True})
|
|
|
|
for (msg_ids, channel_ids) in groups:
|
|
notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]}
|
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id.id), notification)
|
|
|
|
@api.model
|
|
def unstar_all(self):
|
|
""" Unstar messages for the current partner. """
|
|
partner_id = self.env.user.partner_id.id
|
|
|
|
starred_messages = self.search([('starred_partner_ids', 'in', partner_id)])
|
|
starred_messages.write({'starred_partner_ids': [(3, partner_id)]})
|
|
|
|
ids = [m.id for m in starred_messages]
|
|
notification = {'type': 'toggle_star', 'message_ids': ids, 'starred': False}
|
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
|
|
|
|
@api.multi
|
|
def toggle_message_starred(self):
|
|
""" Toggle messages as (un)starred. Technically, the notifications related
|
|
to uid are set to (un)starred.
|
|
"""
|
|
# a user should always be able to star a message he can read
|
|
self.check_access_rule('read')
|
|
starred = not self.starred
|
|
if starred:
|
|
self.sudo().write({'starred_partner_ids': [(4, self.env.user.partner_id.id)]})
|
|
else:
|
|
self.sudo().write({'starred_partner_ids': [(3, self.env.user.partner_id.id)]})
|
|
|
|
notification = {'type': 'toggle_star', 'message_ids': [self.id], 'starred': starred}
|
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
|
|
|
|
#------------------------------------------------------
|
|
# Message loading for web interface
|
|
#------------------------------------------------------
|
|
|
|
@api.model
|
|
def _message_read_dict_postprocess(self, messages, message_tree):
|
|
""" Post-processing on values given by message_read. This method will
|
|
handle partners in batch to avoid doing numerous queries.
|
|
|
|
:param list messages: list of message, as get_dict result
|
|
:param dict message_tree: {[msg.id]: msg browse record as super user}
|
|
"""
|
|
# 1. Aggregate partners (author_id and partner_ids), attachments and tracking values
|
|
partners = self.env['res.partner'].sudo()
|
|
attachments = self.env['ir.attachment']
|
|
message_ids = list(message_tree.keys())
|
|
for message in message_tree.values():
|
|
if message.author_id:
|
|
partners |= message.author_id
|
|
if message.subtype_id and message.partner_ids: # take notified people of message with a subtype
|
|
partners |= message.partner_ids
|
|
elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
|
|
partners |= message.partner_ids
|
|
if message.needaction_partner_ids: # notified
|
|
partners |= message.needaction_partner_ids
|
|
if message.attachment_ids:
|
|
attachments |= message.attachment_ids
|
|
# Read partners as SUPERUSER -> message being browsed as SUPERUSER it is already the case
|
|
partners_names = partners.name_get()
|
|
partner_tree = dict((partner[0], partner) for partner in partners_names)
|
|
|
|
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
|
|
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
|
|
attachments_tree = dict((attachment['id'], {
|
|
'id': attachment['id'],
|
|
'filename': attachment['datas_fname'],
|
|
'name': attachment['name'],
|
|
'mimetype': attachment['mimetype'],
|
|
}) for attachment in attachments_data)
|
|
|
|
# 3. Tracking values
|
|
tracking_values = self.env['mail.tracking.value'].sudo().search([('mail_message_id', 'in', message_ids)])
|
|
message_to_tracking = dict()
|
|
tracking_tree = dict.fromkeys(tracking_values.ids, False)
|
|
for tracking in tracking_values:
|
|
message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id)
|
|
tracking_tree[tracking.id] = {
|
|
'id': tracking.id,
|
|
'changed_field': tracking.field_desc,
|
|
'old_value': tracking.get_old_display_value()[0],
|
|
'new_value': tracking.get_new_display_value()[0],
|
|
'field_type': tracking.field_type,
|
|
}
|
|
|
|
# 4. Update message dictionaries
|
|
for message_dict in messages:
|
|
message_id = message_dict.get('id')
|
|
message = message_tree[message_id]
|
|
if message.author_id:
|
|
author = partner_tree[message.author_id.id]
|
|
else:
|
|
author = (0, message.email_from)
|
|
partner_ids = []
|
|
if message.subtype_id:
|
|
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
|
if partner.id in partner_tree]
|
|
else:
|
|
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
|
if partner.id in partner_tree]
|
|
|
|
customer_email_data = []
|
|
for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share and notif.res_partner_id.active):
|
|
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
|
|
|
|
attachment_ids = []
|
|
for attachment in message.attachment_ids:
|
|
if attachment.id in attachments_tree:
|
|
attachment_ids.append(attachments_tree[attachment.id])
|
|
tracking_value_ids = []
|
|
for tracking_value_id in message_to_tracking.get(message_id, list()):
|
|
if tracking_value_id in tracking_tree:
|
|
tracking_value_ids.append(tracking_tree[tracking_value_id])
|
|
|
|
message_dict.update({
|
|
'author_id': author,
|
|
'partner_ids': partner_ids,
|
|
'customer_email_status': (all(d[2] == 'sent' for d in customer_email_data) and 'sent') or
|
|
(any(d[2] == 'exception' for d in customer_email_data) and 'exception') or
|
|
(any(d[2] == 'bounce' for d in customer_email_data) and 'bounce') or 'ready',
|
|
'customer_email_data': customer_email_data,
|
|
'attachment_ids': attachment_ids,
|
|
'tracking_value_ids': tracking_value_ids,
|
|
})
|
|
|
|
return True
|
|
|
|
@api.model
|
|
def message_fetch(self, domain, limit=20):
|
|
return self.search(domain, limit=limit).message_format()
|
|
|
|
@api.multi
|
|
def message_format(self):
|
|
""" Get the message values in the format for web client. Since message values can be broadcasted,
|
|
computed fields MUST NOT BE READ and broadcasted.
|
|
:returns list(dict).
|
|
Example :
|
|
{
|
|
'body': HTML content of the message
|
|
'model': u'res.partner',
|
|
'record_name': u'Agrolait',
|
|
'attachment_ids': [
|
|
{
|
|
'file_type_icon': u'webimage',
|
|
'id': 45,
|
|
'name': u'sample.png',
|
|
'filename': u'sample.png'
|
|
}
|
|
],
|
|
'needaction_partner_ids': [], # list of partner ids
|
|
'res_id': 7,
|
|
'tracking_value_ids': [
|
|
{
|
|
'old_value': "",
|
|
'changed_field': "Customer",
|
|
'id': 2965,
|
|
'new_value': "Axelor"
|
|
}
|
|
],
|
|
'author_id': (3, u'Administrator'),
|
|
'email_from': 'sacha@pokemon.com' # email address or False
|
|
'subtype_id': (1, u'Discussions'),
|
|
'channel_ids': [], # list of channel ids
|
|
'date': '2015-06-30 08:22:33',
|
|
'partner_ids': [[7, "Sacha Du Bourg-Palette"]], # list of partner name_get
|
|
'message_type': u'comment',
|
|
'id': 59,
|
|
'subject': False
|
|
'is_note': True # only if the message is a note (subtype == note)
|
|
'is_discussion': False # only if the message is a discussion (subtype == discussion)
|
|
'is_notification': False # only if the message is a note but is a notification aka not linked to a document like assignation
|
|
}
|
|
"""
|
|
message_values = self.read([
|
|
'id', 'body', 'date', 'author_id', 'email_from', # base message fields
|
|
'message_type', 'subtype_id', 'subject', # message specific
|
|
'model', 'res_id', 'record_name', # document related
|
|
'channel_ids', 'partner_ids', # recipients
|
|
'starred_partner_ids', # list of partner ids for whom the message is starred
|
|
])
|
|
message_tree = dict((m.id, m) for m in self.sudo())
|
|
self._message_read_dict_postprocess(message_values, message_tree)
|
|
|
|
# add subtype data (is_note flag, is_discussion flag , subtype_description). Do it as sudo
|
|
# because portal / public may have to look for internal subtypes
|
|
subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']]
|
|
subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description','id'])
|
|
subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes)
|
|
|
|
com_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
|
|
note_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
|
|
|
|
for message in message_values:
|
|
message['needaction_partner_ids'] = self.env['mail.notification'].sudo().search([('mail_message_id', '=', message['id']), ('is_read', '=', False)]).mapped('res_partner_id').ids
|
|
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == note_id
|
|
message['is_discussion'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['id'] == com_id
|
|
message['is_notification'] = message['is_note'] and not message['model'] and not message['res_id']
|
|
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
|
|
if message['model'] and self.env[message['model']]._original_module:
|
|
message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
|
|
return message_values
|
|
|
|
#------------------------------------------------------
|
|
# mail_message internals
|
|
#------------------------------------------------------
|
|
|
|
@api.model_cr
|
|
def init(self):
|
|
self._cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
|
|
if not self._cr.fetchone():
|
|
self._cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
|
|
|
|
@api.model
|
|
def _find_allowed_model_wise(self, doc_model, doc_dict):
|
|
doc_ids = list(doc_dict)
|
|
allowed_doc_ids = self.env[doc_model].with_context(active_test=False).search([('id', 'in', doc_ids)]).ids
|
|
return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
|
|
|
|
@api.model
|
|
def _find_allowed_doc_ids(self, model_ids):
|
|
IrModelAccess = self.env['ir.model.access']
|
|
allowed_ids = set()
|
|
for doc_model, doc_dict in model_ids.items():
|
|
if not IrModelAccess.check(doc_model, 'read', False):
|
|
continue
|
|
allowed_ids |= self._find_allowed_model_wise(doc_model, doc_dict)
|
|
return allowed_ids
|
|
|
|
@api.model
|
|
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
|
""" Override that adds specific access rights of mail.message, to remove
|
|
ids uid could not see according to our custom rules. Please refer to
|
|
check_access_rule for more details about those rules.
|
|
|
|
Non employees users see only message with subtype (aka do not see
|
|
internal logs).
|
|
|
|
After having received ids of a classic search, keep only:
|
|
- if author_id == pid, uid is the author, OR
|
|
- uid belongs to a notified channel, OR
|
|
- uid is in the specified recipients, OR
|
|
- uid has a notification on the message, OR
|
|
- uid have read access to the related document is model, res_id
|
|
- otherwise: remove the id
|
|
"""
|
|
# Rules do not apply to administrator
|
|
if self._uid == SUPERUSER_ID:
|
|
return super(Message, self)._search(
|
|
args, offset=offset, limit=limit, order=order,
|
|
count=count, access_rights_uid=access_rights_uid)
|
|
# Non-employee see only messages with a subtype (aka, no internal logs)
|
|
if not self.env['res.users'].has_group('base.group_user'):
|
|
args = ['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)] + list(args)
|
|
# Perform a super with count as False, to have the ids, not a counter
|
|
ids = super(Message, self)._search(
|
|
args, offset=offset, limit=limit, order=order,
|
|
count=False, access_rights_uid=access_rights_uid)
|
|
if not ids and count:
|
|
return 0
|
|
elif not ids:
|
|
return ids
|
|
|
|
pid = self.env.user.partner_id.id
|
|
author_ids, partner_ids, channel_ids, allowed_ids = set([]), set([]), set([]), set([])
|
|
model_ids = {}
|
|
|
|
# check read access rights before checking the actual rules on the given ids
|
|
super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read')
|
|
|
|
self._cr.execute("""
|
|
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id,
|
|
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
|
channel_partner.channel_id as channel_id
|
|
FROM "%s" m
|
|
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
|
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
|
|
LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
|
|
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
|
|
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
|
ON channel_rel.mail_message_id = m.id
|
|
LEFT JOIN "mail_channel" channel
|
|
ON channel.id = channel_rel.mail_channel_id
|
|
LEFT JOIN "mail_channel_partner" channel_partner
|
|
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
|
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
|
|
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
|
|
if author_id == pid:
|
|
author_ids.add(id)
|
|
elif partner_id == pid:
|
|
partner_ids.add(id)
|
|
elif channel_id:
|
|
channel_ids.add(id)
|
|
elif rmod and rid:
|
|
model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id)
|
|
|
|
allowed_ids = self._find_allowed_doc_ids(model_ids)
|
|
|
|
final_ids = author_ids | partner_ids | channel_ids | allowed_ids
|
|
|
|
if count:
|
|
return len(final_ids)
|
|
else:
|
|
# re-construct a list based on ids, because set did not keep the original order
|
|
id_list = [id for id in ids if id in final_ids]
|
|
return id_list
|
|
|
|
@api.multi
|
|
def check_access_rule(self, operation):
|
|
""" Access rules of mail.message:
|
|
- read: if
|
|
- author_id == pid, uid is the author OR
|
|
- uid is in the recipients (partner_ids) OR
|
|
- uid has been notified (needaction) OR
|
|
- uid is member of a listern channel (channel_ids.partner_ids) OR
|
|
- uid have read access to the related document if model, res_id
|
|
- otherwise: raise
|
|
- create: if
|
|
- no model, no res_id (private message) OR
|
|
- pid in message_follower_ids if model, res_id OR
|
|
- uid can read the parent OR
|
|
- uid have write or create access on the related document if model, res_id, OR
|
|
- otherwise: raise
|
|
- write: if
|
|
- author_id == pid, uid is the author, OR
|
|
- uid is in the recipients (partner_ids) OR
|
|
- uid has write or create access on the related document if model, res_id
|
|
- otherwise: raise
|
|
- unlink: if
|
|
- uid has write or create access on the related document if model, res_id
|
|
- otherwise: raise
|
|
|
|
Specific case: non employee users see only messages with subtype (aka do
|
|
not see internal logs).
|
|
"""
|
|
def _generate_model_record_ids(msg_val, msg_ids):
|
|
""" :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
|
|
:param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
|
|
"""
|
|
model_record_ids = {}
|
|
for id in msg_ids:
|
|
vals = msg_val.get(id, {})
|
|
if vals.get('model') and vals.get('res_id'):
|
|
model_record_ids.setdefault(vals['model'], set()).add(vals['res_id'])
|
|
return model_record_ids
|
|
|
|
if self._uid == SUPERUSER_ID:
|
|
return
|
|
# Non employees see only messages with a subtype (aka, not internal logs)
|
|
if not self.env['res.users'].has_group('base.group_user'):
|
|
self._cr.execute('''SELECT DISTINCT message.id, message.subtype_id, subtype.internal
|
|
FROM "%s" AS message
|
|
LEFT JOIN "mail_message_subtype" as subtype
|
|
ON message.subtype_id = subtype.id
|
|
WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
|
|
if self._cr.fetchall():
|
|
raise AccessError(
|
|
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
|
|
(self._description, operation))
|
|
|
|
# Read mail_message.ids to have their values
|
|
message_values = dict((res_id, {}) for res_id in self.ids)
|
|
|
|
if operation in ['read', 'write']:
|
|
self._cr.execute("""
|
|
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
|
|
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
|
channel_partner.channel_id as channel_id
|
|
FROM "%s" m
|
|
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
|
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
|
|
LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
|
|
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
|
|
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
|
ON channel_rel.mail_message_id = m.id
|
|
LEFT JOIN "mail_channel" channel
|
|
ON channel.id = channel_rel.mail_channel_id
|
|
LEFT JOIN "mail_channel_partner" channel_partner
|
|
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
|
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
|
|
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
|
|
message_values[mid] = {
|
|
'model': rmod,
|
|
'res_id': rid,
|
|
'author_id': author_id,
|
|
'parent_id': parent_id,
|
|
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
|
|
}
|
|
else:
|
|
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
|
|
for mid, rmod, rid, author_id, parent_id in self._cr.fetchall():
|
|
message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
|
|
|
|
# Author condition (READ, WRITE, CREATE (private))
|
|
author_ids = []
|
|
if operation == 'read' or operation == 'write':
|
|
author_ids = [mid for mid, message in message_values.items()
|
|
if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id]
|
|
elif operation == 'create':
|
|
author_ids = [mid for mid, message in message_values.items()
|
|
if not message.get('model') and not message.get('res_id')]
|
|
|
|
# Parent condition, for create (check for received notifications for the created message parent)
|
|
notified_ids = []
|
|
if operation == 'create':
|
|
# TDE: probably clean me
|
|
parent_ids = [message.get('parent_id') for message in message_values.values()
|
|
if message.get('parent_id')]
|
|
self._cr.execute("""SELECT DISTINCT m.id, partner_rel.res_partner_id, channel_partner.partner_id FROM "%s" m
|
|
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
|
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
|
|
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
|
ON channel_rel.mail_message_id = m.id
|
|
LEFT JOIN "mail_channel" channel
|
|
ON channel.id = channel_rel.mail_channel_id
|
|
LEFT JOIN "mail_channel_partner" channel_partner
|
|
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
|
|
WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, parent_ids,))
|
|
not_parent_ids = [mid[0] for mid in self._cr.fetchall() if any([mid[1], mid[2]])]
|
|
notified_ids += [mid for mid, message in message_values.items()
|
|
if message.get('parent_id') in not_parent_ids]
|
|
|
|
# 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))
|
|
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':
|
|
for doc_model, doc_ids in model_record_ids.items():
|
|
followers = self.env['mail.followers'].sudo().search([
|
|
('res_model', '=', doc_model),
|
|
('res_id', 'in', list(doc_ids)),
|
|
('partner_id', '=', self.env.user.partner_id.id),
|
|
])
|
|
fol_mids = [follower.res_id for follower in followers]
|
|
notified_ids += [mid for mid, message in message_values.items()
|
|
if message.get('model') == doc_model and message.get('res_id') in fol_mids]
|
|
|
|
# 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]
|
|
|
|
# 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()):
|
|
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)') %
|
|
(self._description, operation))
|
|
|
|
@api.model
|
|
def _get_record_name(self, values):
|
|
""" Return the related document name, using name_get. It is done using
|
|
SUPERUSER_ID, to be sure to have the record name correctly stored. """
|
|
model = values.get('model', self.env.context.get('default_model'))
|
|
res_id = values.get('res_id', self.env.context.get('default_res_id'))
|
|
if not model or not res_id or model not in self.env:
|
|
return False
|
|
return self.env[model].sudo().browse(res_id).name_get()[0][1]
|
|
|
|
@api.model
|
|
def _get_reply_to(self, values):
|
|
""" Return a specific reply_to for the document """
|
|
model, res_id, email_from = values.get('model', self._context.get('default_model')), values.get('res_id', self._context.get('default_res_id')), values.get('email_from') # ctx values / defualt_get res ?
|
|
records = self.env[model].browse([res_id]) if model and res_id else None
|
|
return self.env['mail.thread']._notify_get_reply_to_on_records(default=email_from, records=records)[res_id or False]
|
|
|
|
@api.model
|
|
def _get_message_id(self, values):
|
|
if values.get('no_auto_thread', False) is True:
|
|
message_id = tools.generate_tracking_message_id('reply_to')
|
|
elif values.get('res_id') and values.get('model'):
|
|
message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
|
|
else:
|
|
message_id = tools.generate_tracking_message_id('private')
|
|
return message_id
|
|
|
|
@api.multi
|
|
def _invalidate_documents(self):
|
|
""" Invalidate the cache of the documents followed by ``self``. """
|
|
for record in self:
|
|
if record.model and record.res_id and 'message_ids' in self.env[record.model]:
|
|
self.env[record.model].invalidate_cache(fnames=[
|
|
'message_ids',
|
|
'message_unread',
|
|
'message_unread_counter',
|
|
'message_needaction',
|
|
'message_needaction_counter',
|
|
], ids=[record.res_id])
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
# coming from mail.js that does not have pid in its values
|
|
if self.env.context.get('default_starred'):
|
|
self = self.with_context({'default_starred_partner_ids': [(4, self.env.user.partner_id.id)]})
|
|
|
|
if 'email_from' not in values: # needed to compute reply_to
|
|
values['email_from'] = self._get_default_from()
|
|
if not values.get('message_id'):
|
|
values['message_id'] = self._get_message_id(values)
|
|
if 'reply_to' not in values:
|
|
values['reply_to'] = self._get_reply_to(values)
|
|
if 'record_name' not in values and 'default_record_name' not in self.env.context:
|
|
values['record_name'] = self._get_record_name(values)
|
|
|
|
if 'attachment_ids' not in values:
|
|
values.setdefault('attachment_ids', [])
|
|
|
|
# extract base64 images
|
|
if 'body' in values:
|
|
Attachments = self.env['ir.attachment']
|
|
data_to_url = {}
|
|
def base64_to_boundary(match):
|
|
key = match.group(2)
|
|
if not data_to_url.get(key):
|
|
name = 'image%s' % len(data_to_url)
|
|
attachment = Attachments.create({
|
|
'name': name,
|
|
'datas': match.group(2),
|
|
'datas_fname': name,
|
|
'res_model': 'mail.message',
|
|
})
|
|
attachment.generate_access_token()
|
|
values['attachment_ids'].append((4, attachment.id))
|
|
data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
|
|
return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
|
|
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
|
|
|
|
# delegate creation of tracking after the create as sudo to avoid access rights issues
|
|
tracking_values_cmd = values.pop('tracking_value_ids', False)
|
|
message = super(Message, self).create(values)
|
|
if tracking_values_cmd:
|
|
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
|
|
|
|
if values.get('model') and values.get('res_id'):
|
|
message._invalidate_documents()
|
|
|
|
return message
|
|
|
|
@api.multi
|
|
def read(self, fields=None, load='_classic_read'):
|
|
""" Override to explicitely call check_access_rule, that is not called
|
|
by the ORM. It instead directly fetches ir.rules and apply them. """
|
|
self.check_access_rule('read')
|
|
return super(Message, self).read(fields=fields, load=load)
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
if 'model' in vals or 'res_id' in vals:
|
|
self._invalidate_documents()
|
|
res = super(Message, self).write(vals)
|
|
if 'notification_ids' in vals or 'model' in vals or 'res_id' in vals:
|
|
self._invalidate_documents()
|
|
return res
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
# cascade-delete attachments that are directly attached to the message (should only happen
|
|
# for mail.messages that act as parent for a standalone mail.mail record).
|
|
self.check_access_rule('unlink')
|
|
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()
|
|
return super(Message, self).unlink()
|
|
|
|
#------------------------------------------------------
|
|
# Messaging API
|
|
#------------------------------------------------------
|
|
|
|
@api.multi
|
|
def _notify(self, layout=False, force_send=False, send_after_commit=True, values=None):
|
|
""" Compute recipients to notify based on specified recipients and document
|
|
followers. Delegate notification to partners to send emails and bus notifications
|
|
and to channels to broadcast messages on channels """
|
|
group_user = self.env.ref('base.group_user')
|
|
# have a sudoed copy to manipulate partners (public can go here with website modules like forum / blog / ... )
|
|
self_sudo = self.sudo()
|
|
|
|
self.ensure_one()
|
|
partners_sudo = self_sudo.partner_ids
|
|
channels_sudo = self_sudo.channel_ids
|
|
|
|
# all followers of the mail.message document have to be added as partners and notified
|
|
# and filter to employees only if the subtype is internal
|
|
if self_sudo.subtype_id and self.model and self.res_id:
|
|
followers = self_sudo.env['mail.followers'].search([
|
|
('res_model', '=', self.model),
|
|
('res_id', '=', self.res_id),
|
|
('subtype_ids', 'in', self_sudo.subtype_id.id),
|
|
])
|
|
if self_sudo.subtype_id.internal:
|
|
followers = followers.filtered(lambda fol: fol.channel_id or (fol.partner_id.user_ids and group_user in fol.partner_id.user_ids[0].mapped('groups_id')))
|
|
channels_sudo |= followers.mapped('channel_id')
|
|
partners_sudo |= followers.mapped('partner_id')
|
|
|
|
# remove author from notified partners
|
|
if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
|
|
partners_sudo = partners_sudo - self_sudo.author_id
|
|
|
|
# update message, with maybe custom values
|
|
message_values = {}
|
|
if channels_sudo:
|
|
message_values['channel_ids'] = [(6, 0, channels_sudo.ids)]
|
|
if partners_sudo:
|
|
message_values['needaction_partner_ids'] = [(6, 0, partners_sudo.ids)]
|
|
if self.model and self.res_id and hasattr(self.env[self.model], 'message_get_message_notify_values'):
|
|
message_values.update(self.env[self.model].browse(self.res_id).message_get_message_notify_values(self, message_values))
|
|
if message_values:
|
|
self.write(message_values)
|
|
|
|
# notify partners and channels
|
|
# those methods are called as SUPERUSER because portal users posting messages
|
|
# have no access to partner model. Maybe propagating a real uid could be necessary.
|
|
email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
|
|
notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
|
|
if email_channels or partners_sudo - notif_partners:
|
|
partners_sudo.search([
|
|
'|',
|
|
('id', 'in', (partners_sudo - notif_partners).ids),
|
|
('channel_ids', 'in', email_channels.ids),
|
|
('email', '!=', self_sudo.author_id.email or self_sudo.email_from),
|
|
])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
|
|
|
|
notif_partners._notify_by_chat(self)
|
|
|
|
channels_sudo._notify(self)
|
|
|
|
return True
|