##############################################################################
import logging
-import tools
+from openerp import tools
from email.header import decode_header
from openerp import SUPERUSER_ID
from openerp.osv import osv, orm, fields
from openerp.tools import html_email_clean
from openerp.tools.translate import _
+from HTMLParser import HTMLParser
_logger = logging.getLogger(__name__)
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
+class MLStripper(HTMLParser):
+ def __init__(self):
+ self.reset()
+ self.fed = []
+ def handle_data(self, d):
+ self.fed.append(d)
+ def get_data(self):
+ return ''.join(self.fed)
+
+def strip_tags(html):
+ s = MLStripper()
+ s.feed(html)
+ return s.get_data()
class mail_message(osv.Model):
""" Messages model: system notification (replacing res.log notifications),
_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', 'favorite_user_ids']
+ 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
_message_record_name_length = 18
_message_read_more_limit = 1024
# 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)
+ 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):
# 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.get('model') or not message.get('res_id'):
+ if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
continue
- result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
+ result[message['id']] = self.pool[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):
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
ids = [ids]
res = []
for message in self.browse(cr, uid, ids, context=context):
- name = '%s: %s' % (message.subject or '', message.body or '')
+ name = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '')
res.append((message.id, self._shorten_name(name.lstrip(' :'))))
return res
"message, comment for other messages such as user replies"),
'email_from': fields.char('From',
help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
+ 'reply_to': fields.char('Reply-To',
+ help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
'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."),
+ 'author_avatar': fields.related('author_id', 'image_small', type="binary", string="Author's Avatar"),
'partner_ids': fields.many2many('res.partner', string='Recipients'),
'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
'message_id', 'partner_id', 'Notified partners',
store=True, string='Message Record Name',
help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id',
- string='Notifications',
+ 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'),
'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'),
+ 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'),
- 'favorite_user_ids': fields.many2many('res.users', 'mail_favorite',
- 'message_id', 'user_id', string='Favorite',
- help='Users that set this message in their favorites'),
}
def _needaction_domain_get(self, cr, uid, context=None):
- if self._needaction:
- return [('to_read', '=', True)]
- return []
+ return [('to_read', '=', True)]
+
+ def _get_default_from(self, cr, uid, context=None):
+ this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
+ if this.alias_domain:
+ return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
+ elif this.email:
+ return '%s <%s>' % (this.name, this.email)
+ raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None):
return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
_defaults = {
'type': 'email',
'date': lambda *a: fields.datetime.now(),
- 'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+ 'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
'body': '',
+ 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
}
#------------------------------------------------------
return new_has_voted or False
#------------------------------------------------------
- # Favorite
+ # download an attachment
#------------------------------------------------------
- def favorite_toggle(self, cr, uid, ids, context=None):
- ''' Toggles favorite. Performed using read to avoid access rights issues.
- Done as SUPERUSER_ID because uid may star a message he cannot modify. '''
- for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context):
- new_is_favorite = not (uid in message.get('favorite_user_ids'))
- if new_is_favorite:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context)
- # when setting a favorite, set the related notification as unread, or create an unread notification if not existing
- notification_obj = self.pool.get('mail.notification')
- pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
- notif_id = notification_obj.search(cr, SUPERUSER_ID, [('message_id', '=', message.get('id')), ('partner_id', '=', pid)], context=context)
- if notif_id:
- notification_obj.write(cr, SUPERUSER_ID, notif_id, {'read': False}, context=context)
- else:
- notification_obj.create(cr, SUPERUSER_ID, {'message_id': message.get('id'), 'partner_id': pid, 'read': False}, context=context)
- else:
- self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context)
- return new_is_favorite 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
+
+ #------------------------------------------------------
+ # Notification API
+ #------------------------------------------------------
+
+ def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, 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 bool read: set notification as (un)read
+ :param bool create_missing: create notifications for missing entries
+ (i.e. when acting on displayed messages not notified)
+
+ :return number of message mark as read
+ """
+ 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]
+ domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
+ if not create_missing:
+ domain += [('read', '=', not read)]
+ notif_ids = notification_obj.search(cr, uid, domain, context=context)
+
+ # all message have notifications: already set them as (un)read
+ if len(notif_ids) == len(msg_ids) or not create_missing:
+ notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+ return len(notif_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)
+ notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+ return len(notif_ids)
+
+ def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
+ """ Set messages as (un)starred. Technically, the notifications related
+ to uid are set to (un)starred.
+
+ :param bool starred: set notification as (un)starred
+ :param bool create_missing: create notifications for missing entries
+ (i.e. when acting on displayed messages not notified)
+ """
+ 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]
+ domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
+ if not create_missing:
+ domain += [('starred', '=', not starred)]
+ values = {
+ 'starred': starred
+ }
+ if starred:
+ values['read'] = False
+
+ notif_ids = notification_obj.search(cr, uid, domain, context=context)
+
+ # all message have notifications: already set them as (un)starred
+ if len(notif_ids) == len(msg_ids) or not create_missing:
+ notification_obj.write(cr, uid, notif_ids, values, 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, dict(values, partner_id=user_pid, message_id=msg_id), context=context)
+ notification_obj.write(cr, uid, notif_ids, values, context=context)
+ return starred
#------------------------------------------------------
# Message loading for web interface
for key, message in message_tree.iteritems():
if message.author_id:
partner_ids |= set([message.author_id.id])
- if message.partner_ids:
+ if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype
+ partner_ids |= set([partner.id for partner in message.notified_partner_ids])
+ elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
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])
-
- # Filter author_ids uid can see
- # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context)
- partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context)
+ # 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
- 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)
+ # 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', 'name'], context=context)
+ attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name']}) for attachment in attachments)
# 3. Update message dictionaries
for message_dict in messages:
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])
+ if message.subtype_id:
+ partner_ids = [partner_tree[partner.id] for partner in message.notified_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]
attachment_ids = []
for attachment in message.attachment_ids:
if attachment.id in attachments_tree:
# 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]
- is_favorite = uid in [user.id for user in message.favorite_user_ids]
+
+ try:
+ body_html = html_email_clean(message.body)
+ except Exception:
+ body_html = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
+ _logger.exception(Exception)
return {'id': message.id,
'type': message.type,
- 'body': html_email_clean(message.body),
+ 'subtype': message.subtype_id.name if message.subtype_id else False,
+ 'body': body_html,
'model': message.model,
'res_id': message.res_id,
'record_name': message.record_name,
'parent_id': parent_id,
'is_private': is_private,
'author_id': False,
+ 'author_avatar': message.author_avatar,
'is_author': False,
'partner_ids': [],
'vote_nb': vote_nb,
'has_voted': has_voted,
- 'is_favorite': is_favorite,
+ 'is_favorite': message.starred,
'attachment_ids': [],
}
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))
+ # create final ordered message_list based on parent_tree
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]
thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
return message_list
- # TDE Note: do we need this ?
- # def user_free_attachment(self, cr, uid, context=None):
- # attachment = self.pool.get('ir.attachment')
- # attachment_list = []
- # attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('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)]
- # return attachment_list
-
#------------------------------------------------------
# mail_message internals
#------------------------------------------------------
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[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
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'))
- model_access_obj = self.pool.get('ir.model.access')
- for doc_model, doc_dict in model_ids.iteritems():
- if not model_access_obj.check(cr, uid, doc_model, 'read', False):
- continue
- doc_ids = doc_dict.keys()
- allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
- allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_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)
+ # 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
def check_access_rule(self, cr, uid, ids, operation, context=None):
""" Access rules of mail.message:
- uid have read access to the related document if model, res_id
- otherwise: raise
- create: if
- - no model, no res_id, I create a private message
+ - 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
+ - author_id == pid, uid is the author, OR
- uid has write access on the related document if model, res_id
- - Otherwise: raise
+ - otherwise: raise
- unlink: if
- uid has write access on the related document if model, res_id
- - Otherwise: raise
+ - 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
message_values = dict.fromkeys(ids)
- model_record_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, dict()).setdefault(rid, set()).add(id)
-
- # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
- if operation == 'read':
+ 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')]
- else:
- author_ids = []
+
+ # 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)]
elif operation == 'create':
- notified_ids = []
for doc_model, doc_dict in model_record_ids.items():
- fol_obj = self.pool.get('mail.followers')
fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
('res_model', '=', doc_model),
('res_id', 'in', list(doc_dict.keys())),
], context=context)
fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
notified_ids += [mid for mid, message in message_values.iteritems()
- if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
- else:
- notified_ids = []
-
- # Calculate remaining ids, and related model/res_ids
- model_record_ids = {}
- other_ids = set(ids).difference(set(author_ids), set(notified_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'])
+ 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():
- model_obj = self.pool.get(model)
- mids = model_obj.exists(cr, uid, mids)
- 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)
+ for model, doc_dict in model_record_ids.items():
+ model_obj = self.pool[model]
+ mids = model_obj.exists(cr, uid, doc_dict.keys())
+ if hasattr(model_obj, 'check_mail_message_access'):
+ model_obj.check_mail_message_access(cr, uid, mids, operation, context=context)
else:
- model_obj.check_access_rights(cr, uid, operation)
- model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+ self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, 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 = other_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 not values.get('message_id') and values.get('res_id') and values.get('model'):
+ if context is None:
+ context = {}
+ default_starred = context.pop('default_starred', False)
+ # generate message_id, to redirect answers to the right discussion thread
+ if not values.get('message_id') and values.get('reply_to'):
+ values['message_id'] = tools.generate_tracking_message_id('reply_to')
+ elif 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,
+ force_send=context.get('mail_notify_force_send', True),
+ user_signature=context.get('mail_notify_user_signature', True))
+ # 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'):
attachments_to_delete = []
for message in self.browse(cr, uid, ids, context=context):
for attach in message.attachment_ids:
- if attach.res_model == self._name and attach.res_id == message.id:
+ if attach.res_model == self._name and (attach.res_id == message.id or attach.res_id == 0):
attachments_to_delete.append(attach.id)
if attachments_to_delete:
self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
return ''
return result
- def _notify(self, cr, uid, newid, context=None):
+ def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
""" 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:
- return True
- # all partner_ids of the mail.message have to be notified
- if 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:
+
+ # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message)
+ if message.subtype_id and message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
- fol_ids = fol_obj.search(cr, uid, [
+ # 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, uid, fol_ids, 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:
+ if message.subtype_id and 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])
+ partners_to_notify -= set([message.author_id])
- if partners_to_notify:
- self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
+ # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
+ if message.partner_ids:
+ partners_to_notify |= set(message.partner_ids)
- notification_obj = self.pool.get('mail.notification')
- notification_obj._notify(cr, uid, newid, context=context)
+ # notify
+ if partners_to_notify:
+ notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context,
+ force_send=force_send, user_signature=user_signature)
+ message.refresh()
- # An error appear when a user receive a notify to a message without notify to his parent message.
- # Add a notification with read = true to the parented message if there are no notification
+ # 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:
- partners_to_parent_notify = set(partners_to_notify) - set(message.parent_id.notified_partner_ids)
+ # 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,
'read': True,
}, context=context)
-
#------------------------------------------------------
# Tools
#------------------------------------------------------