##############################################################################
import logging
-import openerp
import tools
from email.header import decode_header
from openerp import SUPERUSER_ID
-from operator import itemgetter
-from osv import osv, orm, fields
-from tools.translate import _
+from openerp.osv import osv, orm, fields
+from openerp.tools import html_email_clean
+from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
+try:
+ from mako.template import Template as MakoTemplate
+except ImportError:
+ _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
+
+
""" Some tools for parsing / creating email fields """
def decode(text):
"""Returns unicode() string conversion of the the given encoded smtp header text"""
_description = 'Message'
_inherit = ['ir.needaction_mixin']
_order = 'id desc'
+ _rec_name = 'record_name'
- _message_read_limit = 10
+ _message_read_limit = 30
+ _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
+ 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
_message_record_name_length = 18
+ _message_read_more_limit = 1024
+
+ def default_get(self, cr, uid, fields, context=None):
+ # protection for `default_type` values leaking from menu action context (e.g. for invoices)
+ if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
+ context = dict(context, default_type=None)
+ return super(mail_message, self).default_get(cr, uid, fields, context=context)
def _shorten_name(self, name):
if len(name) <= (self._message_record_name_length + 3):
return name[:self._message_record_name_length] + '...'
def _get_record_name(self, cr, uid, ids, name, arg, context=None):
- """ Return the related document name, using get_name. """
+ """ Return the related document name, using name_get. It is done using
+ SUPERUSER_ID, to be sure to have the record name correctly stored. """
+ # TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
- if not message['model'] or not message['res_id']:
+ if not message.get('model') or not message.get('res_id') or not self.pool.get(message['model']):
continue
- try:
- result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, uid, [message['res_id']], context=context)[0][1])
- except (orm.except_orm, osv.except_osv):
- pass
+ result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
return result
def _get_to_read(self, cr, uid, ids, name, arg, context=None):
notif_ids = notif_obj.search(cr, uid, [
('partner_id', 'in', [partner_id]),
('message_id', 'in', ids),
- ('read', '=', True)
+ ('read', '=', False),
], context=context)
for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
- res[notif.message_id.id] = not notif.read
+ res[notif.message_id.id] = True
return res
def _search_to_read(self, cr, uid, obj, name, domain, context=None):
""" Search for messages to read by the current user. Condition is
inversed because we search unread message on a read column. """
- if domain[0][2]:
- read_cond = "(read = False OR read IS NULL)"
- else:
- read_cond = "read = True"
+ return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.read', '=', not domain[0][2])]
+
+ def _get_starred(self, cr, uid, ids, name, arg, context=None):
+ """ Compute if the message is unread by the current user. """
+ res = dict((id, False) for id in ids)
partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
- cr.execute("SELECT message_id FROM mail_notification "\
- "WHERE partner_id = %%s AND %s" % read_cond,
- (partner_id,))
- return [('id', 'in', [r[0] for r in cr.fetchall()])]
+ notif_obj = self.pool.get('mail.notification')
+ notif_ids = notif_obj.search(cr, uid, [
+ ('partner_id', 'in', [partner_id]),
+ ('message_id', 'in', ids),
+ ('starred', '=', True),
+ ], context=context)
+ for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
+ res[notif.message_id.id] = True
+ return res
+
+ def _search_starred(self, cr, uid, obj, name, domain, context=None):
+ """ Search for messages to read by the current user. Condition is
+ inversed because we search unread message on a read column. """
+ return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
def name_get(self, cr, uid, ids, context=None):
# name_get may receive int id instead of an id list
], 'Type',
help="Message type: email for email message, notification for system "\
"message, comment for other messages such as user replies"),
- 'author_id': fields.many2one('res.partner', 'Author', required=True),
- 'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
+ 'email_from': fields.char('From',
+ help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
+ 'author_id': fields.many2one('res.partner', 'Author', select=1,
+ ondelete='set null',
+ help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
+ 'partner_ids': fields.many2many('res.partner', string='Recipients'),
+ 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
+ 'message_id', 'partner_id', 'Notified partners',
+ help='Partners that have a notification pushing this message in their mailboxes'),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
'message_id', 'attachment_id', 'Attachments'),
- 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
+ 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
+ ondelete='set null', help="Initial thread message."),
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
- 'record_name': fields.function(_get_record_name, type='string',
- string='Message Record Name',
+ 'record_name': fields.function(_get_record_name, type='char',
+ store=True, string='Message Record Name',
help="Name get of the related document."),
- 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
+ 'notification_ids': fields.one2many('mail.notification', 'message_id',
+ string='Notifications', auto_join=True,
+ help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
'subject': fields.char('Subject'),
'date': fields.datetime('Date'),
'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
type='boolean', string='To read',
- help='Functional field to search for messages the current user has to read'),
- 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
- 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes',
+ help='Current user has an unread notification linked to this message'),
+ 'starred': fields.function(_get_starred, fnct_search=_search_starred,
+ type='boolean', string='Starred',
+ help='Current user has a starred notification linked to this message'),
+ 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
+ ondelete='set null', select=1,),
+ 'vote_user_ids': fields.many2many('res.users', 'mail_vote',
+ 'message_id', 'user_id', string='Votes',
help='Users that voted for this message'),
- 'star_user_ids': fields.many2many('res.users', 'mail_star', 'message_id', 'user_id', string='Stared',
- help='Users that stared this message'),
}
def _needaction_domain_get(self, cr, uid, context=None):
- if self._needaction:
- return [('to_read', '=', True)]
- return []
+ return [('to_read', '=', True)]
def _get_default_author(self, cr, uid, context=None):
- # remove context to avoid possible hack in browse with superadmin using context keys that could trigger a specific behavior
- return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
+ return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
_defaults = {
'type': 'email',
# Vote/Like
#------------------------------------------------------
- def vote_toggle(self, cr, uid, ids, user_ids=None, context=None):
- ''' Toggles voting. Done as SUPERUSER_ID because of write access on
- mail.message not always granted. '''
- if not user_ids:
- user_ids = [uid]
- for message in self.read(cr, SUPERUSER_ID, ids, ['vote_user_ids'], context=context):
- for user_id in user_ids:
- has_voted = user_id in message.get('vote_user_ids')
- if not has_voted:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, user_id)]}, context=context)
- else:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, user_id)]}, context=context)
- return not(has_voted) or False
+ def vote_toggle(self, cr, uid, ids, context=None):
+ ''' Toggles vote. Performed using read to avoid access rights issues.
+ Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
+ for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
+ new_has_voted = not (uid in message.get('vote_user_ids'))
+ if new_has_voted:
+ self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
+ else:
+ self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
+ return new_has_voted or False
#------------------------------------------------------
- # Stared/unstared
+ # download an attachment
#------------------------------------------------------
- def star_toggle(self, cr, uid, ids, user_ids=None, context=None):
- ''' Toggles voting. Done as SUPERUSER_ID because of write access on
- mail.message not always granted. '''
- if not user_ids:
- user_ids = [uid]
- for message in self.read(cr, SUPERUSER_ID, ids, ['star_user_ids'], context=context):
- for user_id in user_ids:
- has_stared = user_id in message.get('star_user_ids')
- if not has_stared:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'star_user_ids': [(4, user_id)]}, context=context)
- else:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'star_user_ids': [(3, user_id)]}, context=context)
- return not(has_stared) or False
+ def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
+ """ Return the content of linked attachments. """
+ message = self.browse(cr, uid, id_message, context=context)
+ if attachment_id in [attachment.id for attachment in message.attachment_ids]:
+ attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
+ if attachment.datas and attachment.datas_fname:
+ return {
+ 'base64': attachment.datas,
+ 'filename': attachment.datas_fname,
+ }
+ return False
#------------------------------------------------------
- # Message loading for web interface
+ # Notification API
#------------------------------------------------------
- def _message_get_dict(self, cr, uid, message, context=None):
- """ Return a dict representation of the message.
+ def set_message_read(self, cr, uid, msg_ids, read, context=None):
+ """ Set messages as (un)read. Technically, the notifications related
+ to uid are set to (un)read. If for some msg_ids there are missing
+ notifications (i.e. due to load more or thread parent fetching),
+ they are created.
- :param dict message: read result of a mail.message
+ :param bool read: set notification as (un)read
"""
- if uid in message['vote_user_ids']:
- has_voted = True
- else:
- has_voted = False
- has_stared = False
- if uid in message['star_user_ids']:
- has_stared = True
- else:
- has_stared = False
+ notification_obj = self.pool.get('mail.notification')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ notif_ids = notification_obj.search(cr, uid, [
+ ('partner_id', '=', user_pid),
+ ('message_id', 'in', msg_ids)
+ ], context=context)
- try:
- attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)]
- except (orm.except_orm, osv.except_osv):
- attachment_ids = []
+ # all message have notifications: already set them as (un)read
+ if len(notif_ids) == len(msg_ids):
+ return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
- try:
- partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context)
- except (orm.except_orm, osv.except_osv):
- partner_ids = []
+ # some messages do not have notifications: find which one, create notification, update read status
+ notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
+ to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
+ for msg_id in to_create_msg_ids:
+ notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
+ return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
- try:
- record_name = msg.record_name
- except (orm.except_orm, osv.except_osv):
- record_name = False
-
- return {
- 'id': message['id'],
- 'type': message['type'],
- 'attachment_ids': attachment_ids,
- 'body': message['body'],
- 'model': message['model'],
- 'res_id': message['res_id'],
- 'record_name': message['record_name'],
- 'subject': message['subject'],
- 'date': message['date'],
- 'author_id': message['author_id'],
- 'is_author': message['author_id'] and message['author_id'][0] == uid,
- # TDE note: is this useful ? to check
- 'partner_ids': partner_ids,
- 'parent_id': message['parent_id'] and message['parent_id'][0] or False,
- # TDE note: see with CHM about votes, how they are displayed (only number, or name_get ?)
- # vote: should only use number of votes
- # 'vote_user_ids': vote_ids,
- 'has_voted': has_voted,
- 'is_private': message['model'] and message['res_id'],
- 'has_stared': has_stared,
- 'to_read': message['to_read'],
- }
-
- def _message_read_expandable(self, cr, uid, tree, result, dict_tree, message_loaded_ids, domain, context, parent_id, limit):
- """ Create the expandable message for all parent message read
- this function is used by message_read
- TDE note: add default values for args, add some comments
- tree: id tree
- dict_tree: [id]: message
- :param dict tree: tree of message ids
+ def set_message_starred(self, cr, uid, msg_ids, starred, context=None):
+ """ Set messages as (un)starred. Technically, the notifications related
+ to uid are set to (un)starred. If for some msg_ids there are missing
+ notifications (i.e. due to load more or thread parent fetching),
+ they are created.
+
+ :param bool starred: set notification as (un)starred
"""
- # sort for group items / TDE: move to message_read
- tree = sorted(tree, key=lambda k: k['id'])
- tree_not = []
- # expandable for not show message
- for msg_id in tree:
- message = dict_tree[msg_id]
- # get all childs
- # SHOULD NOT BE SUPERUSED_ID -> check search is correctly implemented in mail.message
- not_loaded_ids = self.search(cr, uid, [
- ('parent_id', '=', message['id']),
- (['id', 'not in', message_loaded_ids),
- ], context=context, limit=1000)
- # group childs not read
- id_min = None
- id_max = None
- nb = 0
-
- for not_loaded_id in not_loaded_ids:
- if not_loaded_id not in tree:
- nb += 1
- if id_min == None or id_min > not_loaded_id:
- id_min = not_loaded_id
- if id_max == None or id_max < not_loaded_id:
- id_max = not_loaded_id
- tree_not.append(not_loaded_id)
- else:
- if nb > 0:
- result.append({
- 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)],
- 'nb_messages': nb,
- 'type': 'expandable',
- 'parent_id': msg_id,
- 'id': id_min,
- 'model': message['model']
- })
- id_min = None
- id_max = None
- nb = 0
- if nb > 0:
- result.append({
- 'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', msg_id)],
- 'nb_messages': nb,
- 'type': 'expandable',
- 'parent_id': msg_id,
- 'id': id_min,
- 'model': message['model'],
+ notification_obj = self.pool.get('mail.notification')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ notif_ids = notification_obj.search(cr, uid, [
+ ('partner_id', '=', user_pid),
+ ('message_id', 'in', msg_ids)
+ ], context=context)
+
+ # all message have notifications: already set them as (un)starred
+ if len(notif_ids) == len(msg_ids):
+ notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+ return starred
+
+ # some messages do not have notifications: find which one, create notification, update starred status
+ notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
+ to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
+ for msg_id in to_create_msg_ids:
+ notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
+ notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+ return starred
+
+ #------------------------------------------------------
+ # Message loading for web interface
+ #------------------------------------------------------
+
+ def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
+ """ 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}
+ """
+ res_partner_obj = self.pool.get('res.partner')
+ ir_attachment_obj = self.pool.get('ir.attachment')
+ pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
+
+ # 1. Aggregate partners (author_id and partner_ids) and attachments
+ partner_ids = set()
+ attachment_ids = set()
+ for key, message in message_tree.iteritems():
+ if message.author_id:
+ partner_ids |= set([message.author_id.id])
+ if message.partner_ids:
+ partner_ids |= set([partner.id for partner in message.partner_ids])
+ if message.attachment_ids:
+ attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
+ # Read partners as SUPERUSER -> display the names like classic m2o even if no access
+ partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
+ partner_tree = dict((partner[0], partner) for partner in partners)
+
+ # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
+ attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname'], context=context)
+ attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments)
+
+ # 3. 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 = []
+ for partner in message.partner_ids:
+ if partner.id in partner_tree:
+ partner_ids.append(partner_tree[partner.id])
+ attachment_ids = []
+ for attachment in message.attachment_ids:
+ if attachment.id in attachments_tree:
+ attachment_ids.append(attachments_tree[attachment.id])
+ message_dict.update({
+ 'is_author': pid == author[0],
+ 'author_id': author,
+ 'partner_ids': partner_ids,
+ 'attachment_ids': attachment_ids,
})
+ return True
- for msg_id in tree+tree_not:
- message_loaded_ids.append(msg_id)
+ def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
+ """ Return a dict representation of the message. This representation is
+ used in the JS client code, to display the messages. Partners and
+ attachments related stuff will be done in post-processing in batch.
- # expandable for limit max
- ids = self.search(cr, uid, domain + [('id', 'not in', message_loaded_ids)], context=context, limit=1)
- if len(ids) > 0:
- result.append({
+ :param dict message: mail.message browse record
+ """
+ # private message: no model, no res_id
+ is_private = False
+ if not message.model or not message.res_id:
+ is_private = True
+ # votes and favorites: res.users ids, no prefetching should be done
+ vote_nb = len(message.vote_user_ids)
+ has_voted = uid in [user.id for user in message.vote_user_ids]
+
+ return {'id': message.id,
+ 'type': message.type,
+ 'body': html_email_clean(message.body or ''),
+ 'model': message.model,
+ 'res_id': message.res_id,
+ 'record_name': message.record_name,
+ 'subject': message.subject,
+ 'date': message.date,
+ 'to_read': message.to_read,
+ 'parent_id': parent_id,
+ 'is_private': is_private,
+ 'author_id': False,
+ 'is_author': False,
+ 'partner_ids': [],
+ 'vote_nb': vote_nb,
+ 'has_voted': has_voted,
+ 'is_favorite': message.starred,
+ 'attachment_ids': [],
+ }
+
+ def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
+ message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
+ """ Create expandables for message_read, to load new messages.
+ 1. get the expandable for new threads
+ if display is flat (thread_level == 0):
+ fetch message_ids < min(already displayed ids), because we
+ want a flat display, ordered by id
+ else:
+ fetch message_ids that are not childs of already displayed
+ messages
+ 2. get the expandables for new messages inside threads if display
+ is not flat
+ for each thread header, search for its childs
+ for each hole in the child list based on message displayed,
+ create an expandable
+
+ :param list messages: list of message structure for the Chatter
+ widget to which expandables are added
+ :param dict message_tree: dict [id]: browse record of this message
+ :param dict parent_tree: dict [parent_id]: [child_ids]
+ :param list message_unload_ids: list of message_ids we do not want
+ to load
+ :return bool: True
+ """
+ def _get_expandable(domain, message_nb, parent_id, max_limit):
+ return {
'domain': domain,
- 'nb_messages': 0,
- 'type': 'expandable',
- 'parent_id': parent_id,
- 'id': -1,
- 'max_limit': True
- });
+ 'nb_messages': message_nb,
+ 'type': 'expandable',
+ 'parent_id': parent_id,
+ 'max_limit': max_limit,
+ }
- return result
+ if not messages:
+ return True
+ message_ids = sorted(message_tree.keys())
- _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read',
- 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'star_user_ids']
+ # 1. get the expandable for new threads
+ if thread_level == 0:
+ exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
+ else:
+ exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
+ ids = self.search(cr, uid, exp_domain, context=context, limit=1)
+ if ids:
+ # inside a thread: prepend
+ if parent_id:
+ messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
+ # new threads: append
+ else:
+ messages.append(_get_expandable(exp_domain, -1, parent_id, True))
+ # 2. get the expandables for new messages inside threads if display is not flat
+ if thread_level == 0:
+ return True
+ for message_id in message_ids:
+ message = message_tree[message_id]
- def _get_parent(self, cr, uid, message, context=None):
- """ Tools method that tries to get the parent of a mail.message. If
- no parent, or if uid has no access right on the parent, False
- is returned.
+ # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
+ if message.parent_id:
+ continue
- :param dict message: read result of a mail.message
- """
- if not message['parent_id']:
- return False
- parent_id = message['parent_id'][0]
- try:
- return self.read(cr, uid, parent_id, self._message_read_fields, context=context)
- except (orm.except_orm, osv.except_osv):
- return False
+ # check there are message for expandable
+ child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
+ child_ids = sorted(list(child_ids), reverse=True)
+ if not child_ids:
+ continue
- def message_read(self, cr, uid, ids=False, domain=[], message_loaded_ids=[], context=None, parent_id=False, limit=None):
- """ Read messages from mail.message, and get back a structured tree
- of messages to be displayed as discussion threads. If IDs is set,
- fetch these records. Otherwise use the domain to fetch messages.
- After having fetch messages, their parents & child will be added to obtain
- well formed threads.
+ # make groups of unread messages
+ id_min, id_max, nb = max(child_ids), 0, 0
+ for child_id in child_ids:
+ if not child_id in message_ids:
+ nb += 1
+ if id_min > child_id:
+ id_min = child_id
+ if id_max < child_id:
+ id_max = child_id
+ elif nb > 0:
+ exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
+ idx = [msg.get('id') for msg in messages].index(child_id) + 1
+ # messages.append(_get_expandable(exp_domain, nb, message_id, False))
+ messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
+ id_min, id_max, nb = max(child_ids), 0, 0
+ else:
+ id_min, id_max, nb = max(child_ids), 0, 0
+ if nb > 0:
+ exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
+ idx = [msg.get('id') for msg in messages].index(message_id) + 1
+ # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
+ messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
- TDE note: update this comment after final method implementation
+ return True
- :param domain: optional domain for searching ids
- :param limit: number of messages to fetch
- :param parent_id: if parent_id reached, stop searching for
- further parents
- :return list: list of trees of messages
+ def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
+ thread_level=0, context=None, parent_id=False, limit=None):
+ """ Read messages from mail.message, and get back a list of structured
+ messages to be displayed as discussion threads. If IDs is set,
+ fetch these records. Otherwise use the domain to fetch messages.
+ After having fetch messages, their ancestors will be added to obtain
+ well formed threads, if uid has access to them.
+
+ After reading the messages, expandable messages are added in the
+ message list (see ``_message_read_add_expandables``). It consists
+ in messages holding the 'read more' data: number of messages to
+ read, domain to apply.
+
+ :param list ids: optional IDs to fetch
+ :param list domain: optional domain for searching ids if ids not set
+ :param list message_unload_ids: optional ids we do not want to fetch,
+ because i.e. they are already displayed somewhere
+ :param int parent_id: context of parent_id
+ - if parent_id reached when adding ancestors, stop going further
+ in the ancestor search
+ - if set in flat mode, ancestor_id is set to parent_id
+ :param int limit: number of messages to fetch, before adding the
+ ancestors and expandables
+ :return list: list of message structure for the Chatter widget
"""
- if message_loaded_ids:
- domain += [('id', 'not in', message_loaded_ids)]
+ assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
+ domain = domain if domain is not None else []
+ message_unload_ids = message_unload_ids if message_unload_ids is not None else []
+ if message_unload_ids:
+ domain += [('id', 'not in', message_unload_ids)]
limit = limit or self._message_read_limit
- tree = []
- # TDE note: better name ?
- dict_tree = {}
- message_ids = []
- record = None
+ message_tree = {}
message_list = []
+ parent_tree = {}
- # select ids
- if ids:
- for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
- message_list.append(self._message_get_dict(cr, uid, message, context=context))
-
- # key: ID, value: tree
- if not ids:
- # TDE: check access rights on search are implemented for mail.message
+ # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
+ if ids is None:
ids = self.search(cr, uid, domain, context=context, limit=limit)
- for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
- # if not in tree and not in message_loded list
- if message['id'] not in message_ids and message['id'] not in tree and message['id'] not in message_loaded_ids :
- message_ids.append(message['id'])
- tree.append(message['id'])
- dict_tree[message['id']] = message
- message_list.append(self._message_get_dict(cr, uid, message, context=context))
-
- parent = self._get_parent(cr, uid, message, context=context)
-
- # get all parented message if the user have the access
- while parent and parent['id'] != parent_id:
- if parent['id'] not in tree and parent['id'] not in message_loaded_ids:
- tree.append(parent['id'])
- dict_tree[message['id']] = message
- message_list.append(self._message_get_dict(cr, uid, message, context=context))
- # if not in tree and not in message_loded list
- if parent['id'] not in message_ids and parent['id'] not in message_loaded_ids :
- message_ids.append(parent['id'])
- parent = self._get_parent(cr, uid, parent, context=context)
- else:
- parent = False
-
- # record the dic of message
- # for msg in self.browse(cr, uid, message_ids, context=context):
- # record = self._message_dict_get(cr, uid, msg, context=context)
- # result.append( record )
-
- # get the child expandable messages for the tree
- message_list = self._message_read_expandable(cr, uid, tree, message_list, dict_tree, message_loaded_ids, domain, context, parent_id, limit)
-
- result = sorted(result, key=lambda k: k['id'])
- return result
- # TDE Note: do we need this ?
- def user_free_attachment(self, cr, uid, context=None):
- attachment_list = []
-
- attachment = self.pool.get('ir.attachment')
- attachment_ids = attachment.search(cr, uid, [('res_model','=',''),('create_uid','=',uid)])
- if len(attachment_ids):
- attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)]
+ # fetch parent if threaded, sort messages
+ for message in self.browse(cr, uid, ids, context=context):
+ message_id = message.id
+ if message_id in message_tree:
+ continue
+ message_tree[message_id] = message
- return attachment_list
+ # find parent_id
+ if thread_level == 0:
+ tree_parent_id = parent_id
+ else:
+ tree_parent_id = message_id
+ parent = message
+ while parent.parent_id and parent.parent_id.id != parent_id:
+ parent = parent.parent_id
+ tree_parent_id = parent.id
+ if not parent.id in message_tree:
+ message_tree[parent.id] = parent
+ # newest messages first
+ parent_tree.setdefault(tree_parent_id, [])
+ if tree_parent_id != message_id:
+ parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
+
+ if thread_level:
+ for key, message_id_list in parent_tree.iteritems():
+ message_id_list.sort(key=lambda item: item['id'])
+ message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
+
+ parent_list = parent_tree.items()
+ parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True)
+ message_list = [message for (key, msg_list) in parent_list for message in msg_list]
+
+ # get the child expandable messages for the tree
+ self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
+ self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
+ thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
+ return message_list
#------------------------------------------------------
- # Email api
+ # mail_message internals
#------------------------------------------------------
def init(self, cr):
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
+ def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
+ doc_ids = doc_dict.keys()
+ allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
+ return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
+
+ def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
+ model_access_obj = self.pool.get('ir.model.access')
+ allowed_ids = set()
+ for doc_model, doc_dict in model_ids.iteritems():
+ if not model_access_obj.check(cr, uid, doc_model, 'read', False):
+ continue
+ allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
+ return allowed_ids
+
+ def _search(self, cr, uid, args, offset=0, limit=None, order=None,
+ context=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.
+
+ After having received ids of a classic search, keep only:
+ - if author_id == pid, uid is the author, OR
+ - a notification (id, pid) exists, uid has been notified, OR
+ - uid have read access to the related document is model, res_id
+ - otherwise: remove the id
+ """
+ # Rules do not apply to administrator
+ if uid == SUPERUSER_ID:
+ return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
+ context=context, count=count, access_rights_uid=access_rights_uid)
+ # Perform a super with count as False, to have the ids, not a counter
+ ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
+ context=context, count=False, access_rights_uid=access_rights_uid)
+ if not ids and count:
+ return 0
+ elif not ids:
+ return ids
+
+ pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
+ author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
+ model_ids = {}
+
+ messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
+ for message in messages:
+ if message.get('author_id') and message.get('author_id')[0] == pid:
+ author_ids.add(message.get('id'))
+ elif pid in message.get('notified_partner_ids'):
+ partner_ids.add(message.get('id'))
+ elif message.get('model') and message.get('res_id'):
+ model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
+
+ allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
+
+ final_ids = author_ids | partner_ids | allowed_ids
+ if count:
+ return len(final_ids)
+ else:
+ return list(final_ids)
+
def check_access_rule(self, cr, uid, ids, operation, context=None):
""" Access rules of mail.message:
- read: if
- - notification exist (I receive pushed message) OR
- - author_id = pid (I am the author) OR
- - I can read the related document if res_model, res_id
- - Otherwise: raise
+ - author_id == pid, uid is the author, OR
+ - mail_notification (id, pid) exists, uid has been notified, OR
+ - uid have read access to the related document if model, res_id
+ - otherwise: raise
- create: if
- - I am in the document message_follower_ids OR
- - I can write on the related document if res_model, res_id OR
- - I write a mail to another user (no res_model)
- - Otherwise: raise
+ - no model, no res_id, I create a private message OR
+ - pid in message_follower_ids if model, res_id OR
+ - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
+ - uid have write access on the related document if model, res_id, OR
+ - otherwise: raise
- write: if
- - I can write on the related document if res_model, res_id
- - Otherwise: raise
+ - author_id == pid, uid is the author, OR
+ - uid has write access on the related document if model, res_id
+ - otherwise: raise
- unlink: if
- - I can write on the related document if res_model, res_id
- - Otherwise: raise
+ - uid has write access on the related document if model, res_id
+ - otherwise: raise
"""
+ 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:
+ if msg_val[id]['model'] and msg_val[id]['res_id']:
+ model_record_ids.setdefault(msg_val[id]['model'], dict()).setdefault(msg_val[id]['res_id'], set()).add(msg_val[id]['res_id'])
+ return model_record_ids
+
if uid == SUPERUSER_ID:
return
if isinstance(ids, (int, long)):
ids = [ids]
+ not_obj = self.pool.get('mail.notification')
+ fol_obj = self.pool.get('mail.followers')
partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
# Read mail_message.ids to have their values
- model_record_ids = {}
message_values = dict.fromkeys(ids)
- cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
- for id, rmod, rid, author_id in cr.fetchall():
- message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
- if rmod:
- model_record_ids.setdefault(rmod, set()).add(rid)
+ cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
+ for id, rmod, rid, author_id, parent_id in cr.fetchall():
+ message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
+
+ # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
+ author_ids = []
+ if operation == 'read' or operation == 'write':
+ author_ids = [mid for mid, message in message_values.iteritems()
+ if message.get('author_id') and message.get('author_id') == partner_id]
+ elif operation == 'create':
+ author_ids = [mid for mid, message in message_values.iteritems()
+ if not message.get('model') and not message.get('res_id')]
- # Read: Check for received notifications -> could become an ir.rule, but not till we do not have a many2one variable field
+ # Parent condition, for create (check for received notifications for the created message parent)
+ notified_ids = []
+ if operation == 'create':
+ parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
+ if message.get('parent_id')]
+ not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
+ not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
+ notified_ids += [mid for mid, message in message_values.iteritems()
+ if message.get('parent_id') in not_parent_ids]
+
+ # Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field
+ other_ids = set(ids).difference(set(author_ids), set(notified_ids))
+ model_record_ids = _generate_model_record_ids(message_values, other_ids)
if operation == 'read':
- not_obj = self.pool.get('mail.notification')
not_ids = not_obj.search(cr, SUPERUSER_ID, [
('partner_id', '=', partner_id),
('message_id', 'in', ids),
], context=context)
notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
- else:
- notified_ids = []
- # Read: Check messages you are author -> could become an ir.rule, but not till we do not have a many2one variable field
- if operation == 'read':
- author_ids = [mid for mid, message in message_values.iteritems()
- if message.get('author_id') and message.get('author_id') == partner_id]
- else:
- author_ids = []
-
- # Create: Check message_follower_ids and author_ids
- if operation == 'create':
- doc_follower_ids = []
- for model, mids in model_record_ids.items():
- fol_obj = self.pool.get('mail.followers')
+ elif operation == 'create':
+ for doc_model, doc_dict in model_record_ids.items():
fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
- ('res_model', '=', model),
- ('res_id', 'in', list(mids)),
+ ('res_model', '=', doc_model),
+ ('res_id', 'in', list(doc_dict.keys())),
('partner_id', '=', partner_id),
], context=context)
fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
- doc_follower_ids += [mid for mid, message in message_values.iteritems()
- if message.get('res_model') == model and message.get('res_id') in fol_mids]
- else:
- doc_follower_ids = []
-
- # Create/write: Check author_ids
- if operation == 'create' or operation == 'write':
- author_ids = author_ids+[mid for mid, message in message_values.iteritems()
- if message.get('author_id') and message.get('author_id') == partner_id and not message.get('res_model')]
-
-
- # Calculate remaining ids, and related model/res_ids
- model_record_ids = {}
- other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids))
- for id in other_ids:
- if message_values[id]['res_model']:
- model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
+ notified_ids += [mid for mid, message in message_values.iteritems()
+ 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, mids in model_record_ids.items():
+ for model, doc_dict in model_record_ids.items():
model_obj = self.pool.get(model)
- mids = model_obj.exists(cr, uid, mids)
+ mids = model_obj.exists(cr, uid, doc_dict.keys())
if operation in ['create', 'write', 'unlink']:
model_obj.check_access_rights(cr, uid, 'write')
model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
model_obj.check_access_rights(cr, uid, operation)
model_obj.check_access_rule(cr, uid, mids, operation, context=context)
document_related_ids += [mid for mid, message in message_values.iteritems()
- if message.get('res_model') == model and message.get('res_id') in mids]
+ if message.get('model') == model and message.get('res_id') in mids]
# Calculate remaining ids: if not void, raise an error
- other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids), set(document_related_ids))
+ other_ids = other_ids.difference(set(document_related_ids))
if not other_ids:
return
raise orm.except_orm(_('Access Denied'),
(self._description, operation))
def create(self, cr, uid, values, context=None):
+ if context is None:
+ context = {}
+ default_starred = context.pop('default_starred', False)
if not values.get('message_id') and values.get('res_id') and values.get('model'):
values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
+ elif not values.get('message_id'):
+ values['message_id'] = tools.generate_tracking_message_id('private')
newid = super(mail_message, self).create(cr, uid, values, context)
- self._notify(cr, SUPERUSER_ID, newid, context=context)
+ self._notify(cr, uid, newid, context=context)
+ # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
+ # Because starred will call set_message_starred, that looks for notifications.
+ # When creating a new mail_message, it will create a notification to a message
+ # that does not exist, leading to an error (key not existing). Also this
+ # this means unread notifications will be created, yet we can not assure
+ # this is what we want.
+ if default_starred:
+ self.set_message_starred(cr, uid, [newid], True, context=context)
return newid
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
return super(mail_message, self).unlink(cr, uid, ids, context=context)
- def _notify_followers(self, cr, uid, newid, message, context=None):
- """ Add the related record followers to the destination partner_ids.
+ def copy(self, cr, uid, id, default=None, context=None):
+ """ Overridden to avoid duplicating fields that are unique to each email """
+ if default is None:
+ default = {}
+ default.update(message_id=False, headers=False)
+ return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
+
+ #------------------------------------------------------
+ # Messaging API
+ #------------------------------------------------------
+
+ # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
+ # will be added to email send for notifications. Currently only WIP.
+ MAIL_TEMPLATE = """<div>
+ % if message:
+ ${display_message(message)}
+ % endif
+ % for ctx_msg in context_messages:
+ ${display_message(ctx_msg)}
+ % endfor
+ % if add_expandable:
+ ${display_expandable()}
+ % endif
+ ${display_message(header_message)}
+ </div>
+
+ <%def name="display_message(message)">
+ <div>
+ Subject: ${message.subject}<br />
+ Body: ${message.body}
+ </div>
+ </%def>
+
+ <%def name="display_expandable()">
+ <div>This is an expandable.</div>
+ </%def>
+ """
+
+ def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
"""
+ 1. message.parent_id = False: new thread, no quote_context
+ 2. get the lasts messages in the thread before message
+ 3. get the message header
+ 4. add an expandable between them
+
+ :param dict quote_context: options for quoting
+ :return string: html quote
+ """
+ add_expandable = False
+
+ message = self.browse(cr, uid, id, context=context)
+ if not message.parent_id:
+ return ''
+ context_ids = self.search(cr, uid, [
+ ('parent_id', '=', message.parent_id.id),
+ ('id', '<', message.id),
+ ], limit=limit, context=context)
+
+ if len(context_ids) >= limit:
+ add_expandable = True
+ context_ids = context_ids[0:-1]
+
+ context_ids.append(message.parent_id.id)
+ context_messages = self.browse(cr, uid, context_ids, context=context)
+ header_message = context_messages.pop()
+
+ try:
+ if not add_original:
+ message = False
+ result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
+ context_messages=context_messages,
+ header_message=header_message,
+ add_expandable=add_expandable,
+ # context kw would clash with mako internals
+ ctx=context,
+ format_exceptions=True)
+ result = result.strip()
+ return result
+ except Exception:
+ _logger.exception("failed to render mako template for quoting message")
+ return ''
+ return result
+
+ def _notify(self, cr, uid, newid, context=None):
+ """ Add the related record followers to the destination partner_ids if is not a private message.
+ Call mail_notification.notify to manage the email sending
+ """
+ notification_obj = self.pool.get('mail.notification')
+ message = self.browse(cr, uid, newid, context=context)
+
partners_to_notify = set([])
# message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id:
- message.write({'partner_ids': [5]})
return True
# all partner_ids of the mail.message have to be notified
if message.partner_ids:
- partners_to_notify |= set(partner.id for partner in message.partner_ids)
+ partners_to_notify |= set(message.partner_ids)
# all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
- fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context)
- fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context)
- extra_notified = set(fol.partner_id.id for fol in fol_objs)
- missing_notified = extra_notified - partners_to_notify
- if missing_notified:
- self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context)
-
- def _notify(self, cr, uid, newid, context=None):
- """ Add the related record followers to the destination partner_ids if is not a private message.
- Call mail_notification.notify to manage the email sending
- """
- message = self.browse(cr, uid, newid, context=context)
- if message.model and message.res_id:
- self._notify_followers(cr, uid, newid, message, context=context)
-
- # add myself if I wrote on my wall, otherwise remove myself author
- if ((message.model == "res.partner" and message.res_id == message.author_id.id)):
- self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context)
- else:
- self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context)
-
- self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
-
- def copy(self, cr, uid, id, default=None, context=None):
- """Overridden to avoid duplicating fields that are unique to each email"""
- if default is None:
- default = {}
- default.update(message_id=False, headers=False)
- return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
+ # browse as SUPERUSER because rules could restrict the search results
+ fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
+ ('res_model', '=', message.model),
+ ('res_id', '=', message.res_id),
+ ('subtype_ids', 'in', message.subtype_id.id)
+ ], context=context)
+ partners_to_notify |= set(fo.partner_id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
+ # remove me from notified partners, unless the message is written on my own wall
+ if message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
+ partners_to_notify |= set([message.author_id])
+ elif message.author_id:
+ partners_to_notify = partners_to_notify - set([message.author_id])
+
+ # notify
+ if partners_to_notify:
+ self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
+ notification_obj._notify(cr, uid, newid, context=context)
+ message.refresh()
+
+ # An error appear when a user receive a notification without notifying
+ # the parent message -> add a read notification for the parent
+ if message.parent_id:
+ # all notified_partner_ids of the mail.message have to be notified for the parented messages
+ partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
+ for partner in partners_to_parent_notify:
+ notification_obj.create(cr, uid, {
+ 'message_id': message.parent_id.id,
+ 'partner_id': partner.id,
+ 'read': True,
+ }, context=context)
#------------------------------------------------------
# Tools