1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
24 from openerp import tools
26 from email.header import decode_header
27 from openerp import SUPERUSER_ID, api
28 from openerp.osv import osv, orm, fields
29 from openerp.tools import html_email_clean
30 from openerp.tools.translate import _
31 from HTMLParser import HTMLParser
33 _logger = logging.getLogger(__name__)
36 from mako.template import Template as MakoTemplate
38 _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
41 """ Some tools for parsing / creating email fields """
43 """Returns unicode() string conversion of the the given encoded smtp header text"""
45 text = decode_header(text.replace('\r', ''))
46 return ''.join([tools.ustr(x[0], x[1]) for x in text])
48 class MLStripper(HTMLParser):
52 def handle_data(self, d):
55 return ''.join(self.fed)
62 class mail_message(osv.Model):
63 """ Messages model: system notification (replacing res.log notifications),
64 comments (OpenChatter discussion) and incoming emails. """
65 _name = 'mail.message'
66 _description = 'Message'
67 _inherit = ['ir.needaction_mixin']
69 _rec_name = 'record_name'
71 _message_read_limit = 30
72 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
73 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
74 _message_record_name_length = 18
75 _message_read_more_limit = 1024
77 def default_get(self, cr, uid, fields, context=None):
78 # protection for `default_type` values leaking from menu action context (e.g. for invoices)
79 if context and context.get('default_type') and context.get('default_type') not in [
80 val[0] for val in self._columns['type'].selection]:
81 context = dict(context, default_type=None)
82 return super(mail_message, self).default_get(cr, uid, fields, context=context)
84 def _get_to_read(self, cr, uid, ids, name, arg, context=None):
85 """ Compute if the message is unread by the current user. """
86 res = dict((id, False) for id in ids)
87 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
88 notif_obj = self.pool.get('mail.notification')
89 notif_ids = notif_obj.search(cr, uid, [
90 ('partner_id', 'in', [partner_id]),
91 ('message_id', 'in', ids),
92 ('is_read', '=', False),
94 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
95 res[notif.message_id.id] = True
98 def _search_to_read(self, cr, uid, obj, name, domain, context=None):
99 """ Search for messages to read by the current user. Condition is
100 inversed because we search unread message on a is_read column. """
101 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.is_read', '=', not domain[0][2])]
103 def _get_starred(self, cr, uid, ids, name, arg, context=None):
104 """ Compute if the message is unread by the current user. """
105 res = dict((id, False) for id in ids)
106 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
107 notif_obj = self.pool.get('mail.notification')
108 notif_ids = notif_obj.search(cr, uid, [
109 ('partner_id', 'in', [partner_id]),
110 ('message_id', 'in', ids),
111 ('starred', '=', True),
113 for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
114 res[notif.message_id.id] = True
117 def _search_starred(self, cr, uid, obj, name, domain, context=None):
118 """ Search for starred messages by the current user."""
119 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
122 'type': fields.selection([
124 ('comment', 'Comment'),
125 ('notification', 'System notification'),
127 help="Message type: email for email message, notification for system "\
128 "message, comment for other messages such as user replies"),
129 'email_from': fields.char('From',
130 help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
131 'reply_to': fields.char('Reply-To',
132 help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
133 'no_auto_thread': fields.boolean('No threading for answers',
134 help='Answers do not go in the original document\' discussion thread. This has an impact on the generated message-id.'),
135 'author_id': fields.many2one('res.partner', 'Author', select=1,
137 help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
138 'author_avatar': fields.related('author_id', 'image_small', type="binary", string="Author's Avatar"),
139 'partner_ids': fields.many2many('res.partner', string='Recipients'),
140 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
141 'message_id', 'partner_id', 'Notified partners',
142 help='Partners that have a notification pushing this message in their mailboxes'),
143 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
144 'message_id', 'attachment_id', 'Attachments'),
145 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
146 ondelete='set null', help="Initial thread message."),
147 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
148 'model': fields.char('Related Document Model', size=128, select=1),
149 'res_id': fields.integer('Related Document ID', select=1),
150 'record_name': fields.char('Message Record Name', help="Name get of the related document."),
151 'notification_ids': fields.one2many('mail.notification', 'message_id',
152 string='Notifications', auto_join=True,
153 help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
154 'subject': fields.char('Subject'),
155 'date': fields.datetime('Date'),
156 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False),
157 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
158 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
159 type='boolean', string='To read',
160 help='Current user has an unread notification linked to this message'),
161 'starred': fields.function(_get_starred, fnct_search=_search_starred,
162 type='boolean', string='Starred',
163 help='Current user has a starred notification linked to this message'),
164 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
165 ondelete='set null', select=1,),
166 'vote_user_ids': fields.many2many('res.users', 'mail_vote',
167 'message_id', 'user_id', string='Votes',
168 help='Users that voted for this message'),
169 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
172 def _needaction_domain_get(self, cr, uid, context=None):
173 return [('to_read', '=', True)]
175 def _get_default_from(self, cr, uid, context=None):
176 this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
177 if this.alias_name and this.alias_domain:
178 return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
180 return '%s <%s>' % (this.name, this.email)
181 raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
183 def _get_default_author(self, cr, uid, context=None):
184 return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
188 'date': fields.datetime.now,
189 'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx),
191 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
194 #------------------------------------------------------
196 #------------------------------------------------------
198 def vote_toggle(self, cr, uid, ids, context=None):
199 ''' Toggles vote. Performed using read to avoid access rights issues.
200 Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
201 for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
202 new_has_voted = not (uid in message.get('vote_user_ids'))
204 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
206 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
207 return new_has_voted or False
209 #------------------------------------------------------
210 # download an attachment
211 #------------------------------------------------------
213 def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
214 """ Return the content of linked attachments. """
215 # this will fail if you cannot read the message
216 message_values = self.read(cr, uid, [id_message], ['attachment_ids'], context=context)[0]
217 if attachment_id in message_values['attachment_ids']:
218 attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
219 if attachment.datas and attachment.datas_fname:
221 'base64': attachment.datas,
222 'filename': attachment.datas_fname,
226 #------------------------------------------------------
228 #------------------------------------------------------
230 @api.cr_uid_ids_context
231 def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, context=None):
232 """ Set messages as (un)read. Technically, the notifications related
233 to uid are set to (un)read. If for some msg_ids there are missing
234 notifications (i.e. due to load more or thread parent fetching),
237 :param bool read: set notification as (un)read
238 :param bool create_missing: create notifications for missing entries
239 (i.e. when acting on displayed messages not notified)
241 :return number of message mark as read
243 notification_obj = self.pool.get('mail.notification')
244 user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
245 domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
246 if not create_missing:
247 domain += [('is_read', '=', not read)]
248 notif_ids = notification_obj.search(cr, uid, domain, context=context)
250 # all message have notifications: already set them as (un)read
251 if len(notif_ids) == len(msg_ids) or not create_missing:
252 notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
253 return len(notif_ids)
255 # some messages do not have notifications: find which one, create notification, update read status
256 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
257 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
258 for msg_id in to_create_msg_ids:
259 notification_obj.create(cr, uid, {'partner_id': user_pid, 'is_read': read, 'message_id': msg_id}, context=context)
260 notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
261 return len(notif_ids)
263 @api.cr_uid_ids_context
264 def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
265 """ Set messages as (un)starred. Technically, the notifications related
266 to uid are set to (un)starred.
268 :param bool starred: set notification as (un)starred
269 :param bool create_missing: create notifications for missing entries
270 (i.e. when acting on displayed messages not notified)
272 notification_obj = self.pool.get('mail.notification')
273 user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
274 domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
275 if not create_missing:
276 domain += [('starred', '=', not starred)]
281 values['is_read'] = False
283 notif_ids = notification_obj.search(cr, uid, domain, context=context)
285 # all message have notifications: already set them as (un)starred
286 if len(notif_ids) == len(msg_ids) or not create_missing:
287 notification_obj.write(cr, uid, notif_ids, values, context=context)
290 # some messages do not have notifications: find which one, create notification, update starred status
291 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
292 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
293 for msg_id in to_create_msg_ids:
294 notification_obj.create(cr, uid, dict(values, partner_id=user_pid, message_id=msg_id), context=context)
295 notification_obj.write(cr, uid, notif_ids, values, context=context)
298 #------------------------------------------------------
299 # Message loading for web interface
300 #------------------------------------------------------
302 def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
303 """ Post-processing on values given by message_read. This method will
304 handle partners in batch to avoid doing numerous queries.
306 :param list messages: list of message, as get_dict result
307 :param dict message_tree: {[msg.id]: msg browse record}
309 res_partner_obj = self.pool.get('res.partner')
310 ir_attachment_obj = self.pool.get('ir.attachment')
311 pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
313 # 1. Aggregate partners (author_id and partner_ids) and attachments
315 attachment_ids = set()
316 for key, message in message_tree.iteritems():
317 if message.author_id:
318 partner_ids |= set([message.author_id.id])
319 if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype
320 partner_ids |= set([partner.id for partner in message.notified_partner_ids])
321 elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
322 partner_ids |= set([partner.id for partner in message.partner_ids])
323 if message.attachment_ids:
324 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
325 # Read partners as SUPERUSER -> display the names like classic m2o even if no access
326 partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
327 partner_tree = dict((partner[0], partner) for partner in partners)
329 # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
330 attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
331 attachments_tree = dict((attachment['id'], {
332 'id': attachment['id'],
333 'filename': attachment['datas_fname'],
334 'name': attachment['name'],
335 'file_type_icon': attachment['file_type_icon'],
336 }) for attachment in attachments)
338 # 3. Update message dictionaries
339 for message_dict in messages:
340 message_id = message_dict.get('id')
341 message = message_tree[message_id]
342 if message.author_id:
343 author = partner_tree[message.author_id.id]
345 author = (0, message.email_from)
347 if message.subtype_id:
348 partner_ids = [partner_tree[partner.id] for partner in message.notified_partner_ids
349 if partner.id in partner_tree]
351 partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
352 if partner.id in partner_tree]
354 for attachment in message.attachment_ids:
355 if attachment.id in attachments_tree:
356 attachment_ids.append(attachments_tree[attachment.id])
357 message_dict.update({
358 'is_author': pid == author[0],
360 'partner_ids': partner_ids,
361 'attachment_ids': attachment_ids,
366 def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
367 """ Return a dict representation of the message. This representation is
368 used in the JS client code, to display the messages. Partners and
369 attachments related stuff will be done in post-processing in batch.
371 :param dict message: mail.message browse record
373 # private message: no model, no res_id
375 if not message.model or not message.res_id:
377 # votes and favorites: res.users ids, no prefetching should be done
378 vote_nb = len(message.vote_user_ids)
379 has_voted = uid in [user.id for user in message.vote_user_ids]
386 body_short = html_email_clean(message.body, remove=False, shorten=True, max_length=max_length)
389 body_short = '<p><b>Encoding Error : </b><br/>Unable to convert this message (id: %s).</p>' % message.id
390 _logger.exception(Exception)
392 return {'id': message.id,
393 'type': message.type,
394 'subtype': message.subtype_id.name if message.subtype_id else False,
395 'body': message.body,
396 'body_short': body_short,
397 'model': message.model,
398 'res_id': message.res_id,
399 'record_name': message.record_name,
400 'subject': message.subject,
401 'date': message.date,
402 'to_read': message.to_read,
403 'parent_id': parent_id,
404 'is_private': is_private,
406 'author_avatar': message.author_avatar,
410 'has_voted': has_voted,
411 'is_favorite': message.starred,
412 'attachment_ids': [],
415 def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
416 message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
417 """ Create expandables for message_read, to load new messages.
418 1. get the expandable for new threads
419 if display is flat (thread_level == 0):
420 fetch message_ids < min(already displayed ids), because we
421 want a flat display, ordered by id
423 fetch message_ids that are not childs of already displayed
425 2. get the expandables for new messages inside threads if display
427 for each thread header, search for its childs
428 for each hole in the child list based on message displayed,
431 :param list messages: list of message structure for the Chatter
432 widget to which expandables are added
433 :param dict message_tree: dict [id]: browse record of this message
434 :param dict parent_tree: dict [parent_id]: [child_ids]
435 :param list message_unload_ids: list of message_ids we do not want
439 def _get_expandable(domain, message_nb, parent_id, max_limit):
442 'nb_messages': message_nb,
443 'type': 'expandable',
444 'parent_id': parent_id,
445 'max_limit': max_limit,
450 message_ids = sorted(message_tree.keys())
452 # 1. get the expandable for new threads
453 if thread_level == 0:
454 exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
456 exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
457 ids = self.search(cr, uid, exp_domain, context=context, limit=1)
459 # inside a thread: prepend
461 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
462 # new threads: append
464 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
466 # 2. get the expandables for new messages inside threads if display is not flat
467 if thread_level == 0:
469 for message_id in message_ids:
470 message = message_tree[message_id]
472 # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
473 if message.parent_id:
476 # check there are message for expandable
477 child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
478 child_ids = sorted(list(child_ids), reverse=True)
482 # make groups of unread messages
483 id_min, id_max, nb = max(child_ids), 0, 0
484 for child_id in child_ids:
485 if not child_id in message_ids:
487 if id_min > child_id:
489 if id_max < child_id:
492 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
493 idx = [msg.get('id') for msg in messages].index(child_id) + 1
494 # messages.append(_get_expandable(exp_domain, nb, message_id, False))
495 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
496 id_min, id_max, nb = max(child_ids), 0, 0
498 id_min, id_max, nb = max(child_ids), 0, 0
500 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
501 idx = [msg.get('id') for msg in messages].index(message_id) + 1
502 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
503 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
508 def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
509 thread_level=0, context=None, parent_id=False, limit=None):
510 """ Read messages from mail.message, and get back a list of structured
511 messages to be displayed as discussion threads. If IDs is set,
512 fetch these records. Otherwise use the domain to fetch messages.
513 After having fetch messages, their ancestors will be added to obtain
514 well formed threads, if uid has access to them.
516 After reading the messages, expandable messages are added in the
517 message list (see ``_message_read_add_expandables``). It consists
518 in messages holding the 'read more' data: number of messages to
519 read, domain to apply.
521 :param list ids: optional IDs to fetch
522 :param list domain: optional domain for searching ids if ids not set
523 :param list message_unload_ids: optional ids we do not want to fetch,
524 because i.e. they are already displayed somewhere
525 :param int parent_id: context of parent_id
526 - if parent_id reached when adding ancestors, stop going further
527 in the ancestor search
528 - if set in flat mode, ancestor_id is set to parent_id
529 :param int limit: number of messages to fetch, before adding the
530 ancestors and expandables
531 :return list: list of message structure for the Chatter widget
533 assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
534 domain = domain if domain is not None else []
535 message_unload_ids = message_unload_ids if message_unload_ids is not None else []
536 if message_unload_ids:
537 domain += [('id', 'not in', message_unload_ids)]
538 limit = limit or self._message_read_limit
543 # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
545 ids = self.search(cr, uid, domain, context=context, limit=limit)
547 # fetch parent if threaded, sort messages
548 for message in self.browse(cr, uid, ids, context=context):
549 message_id = message.id
550 if message_id in message_tree:
552 message_tree[message_id] = message
555 if thread_level == 0:
556 tree_parent_id = parent_id
558 tree_parent_id = message_id
560 while parent.parent_id and parent.parent_id.id != parent_id:
561 parent = parent.parent_id
562 tree_parent_id = parent.id
563 if not parent.id in message_tree:
564 message_tree[parent.id] = parent
565 # newest messages first
566 parent_tree.setdefault(tree_parent_id, [])
567 if tree_parent_id != message_id:
568 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
571 for key, message_id_list in parent_tree.iteritems():
572 message_id_list.sort(key=lambda item: item['id'])
573 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
575 # create final ordered message_list based on parent_tree
576 parent_list = parent_tree.items()
577 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)
578 message_list = [message for (key, msg_list) in parent_list for message in msg_list]
580 # get the child expandable messages for the tree
581 self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
582 self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
583 thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
586 #------------------------------------------------------
587 # mail_message internals
588 #------------------------------------------------------
591 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
592 if not cr.fetchone():
593 cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
595 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
596 doc_ids = doc_dict.keys()
597 allowed_doc_ids = self.pool[doc_model].search(cr, uid, [('id', 'in', doc_ids)], context=context)
598 return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
600 def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
601 model_access_obj = self.pool.get('ir.model.access')
603 for doc_model, doc_dict in model_ids.iteritems():
604 if not model_access_obj.check(cr, uid, doc_model, 'read', False):
606 allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
609 def _search(self, cr, uid, args, offset=0, limit=None, order=None,
610 context=None, count=False, access_rights_uid=None):
611 """ Override that adds specific access rights of mail.message, to remove
612 ids uid could not see according to our custom rules. Please refer
613 to check_access_rule for more details about those rules.
615 After having received ids of a classic search, keep only:
616 - if author_id == pid, uid is the author, OR
617 - a notification (id, pid) exists, uid has been notified, OR
618 - uid have read access to the related document is model, res_id
619 - otherwise: remove the id
621 # Rules do not apply to administrator
622 if uid == SUPERUSER_ID:
623 return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
624 context=context, count=count, access_rights_uid=access_rights_uid)
625 # Perform a super with count as False, to have the ids, not a counter
626 ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
627 context=context, count=False, access_rights_uid=access_rights_uid)
628 if not ids and count:
633 pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
634 author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
637 messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
638 for message in messages:
639 if message.get('author_id') and message.get('author_id')[0] == pid:
640 author_ids.add(message.get('id'))
641 elif pid in message.get('notified_partner_ids'):
642 partner_ids.add(message.get('id'))
643 elif message.get('model') and message.get('res_id'):
644 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
646 allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
647 final_ids = author_ids | partner_ids | allowed_ids
650 return len(final_ids)
652 # re-construct a list based on ids, because set did not keep the original order
653 id_list = [id for id in ids if id in final_ids]
656 def check_access_rule(self, cr, uid, ids, operation, context=None):
657 """ Access rules of mail.message:
659 - author_id == pid, uid is the author, OR
660 - mail_notification (id, pid) exists, uid has been notified, OR
661 - uid have read access to the related document if model, res_id
664 - no model, no res_id, I create a private message OR
665 - pid in message_follower_ids if model, res_id OR
666 - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
667 - uid have write or create access on the related document if model, res_id, OR
670 - author_id == pid, uid is the author, OR
671 - uid has write or create access on the related document if model, res_id
674 - uid has write or create access on the related document if model, res_id
677 def _generate_model_record_ids(msg_val, msg_ids):
678 """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
679 :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
681 model_record_ids = {}
683 vals = msg_val.get(id, {})
684 if vals.get('model') and vals.get('res_id'):
685 model_record_ids.setdefault(vals['model'], set()).add(vals['res_id'])
686 return model_record_ids
688 if uid == SUPERUSER_ID:
690 if isinstance(ids, (int, long)):
692 not_obj = self.pool.get('mail.notification')
693 fol_obj = self.pool.get('mail.followers')
694 partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
696 # Read mail_message.ids to have their values
697 message_values = dict.fromkeys(ids, {})
698 cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
699 for id, rmod, rid, author_id, parent_id in cr.fetchall():
700 message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
702 # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
704 if operation == 'read' or operation == 'write':
705 author_ids = [mid for mid, message in message_values.iteritems()
706 if message.get('author_id') and message.get('author_id') == partner_id]
707 elif operation == 'create':
708 author_ids = [mid for mid, message in message_values.iteritems()
709 if not message.get('model') and not message.get('res_id')]
711 # Parent condition, for create (check for received notifications for the created message parent)
713 if operation == 'create':
714 parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
715 if message.get('parent_id')]
716 not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
717 not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
718 notified_ids += [mid for mid, message in message_values.iteritems()
719 if message.get('parent_id') in not_parent_ids]
721 # 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
722 other_ids = set(ids).difference(set(author_ids), set(notified_ids))
723 model_record_ids = _generate_model_record_ids(message_values, other_ids)
724 if operation == 'read':
725 not_ids = not_obj.search(cr, SUPERUSER_ID, [
726 ('partner_id', '=', partner_id),
727 ('message_id', 'in', ids),
729 notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
730 elif operation == 'create':
731 for doc_model, doc_ids in model_record_ids.items():
732 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
733 ('res_model', '=', doc_model),
734 ('res_id', 'in', list(doc_ids)),
735 ('partner_id', '=', partner_id),
737 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
738 notified_ids += [mid for mid, message in message_values.iteritems()
739 if message.get('model') == doc_model and message.get('res_id') in fol_mids]
741 # CRUD: Access rights related to the document
742 other_ids = other_ids.difference(set(notified_ids))
743 model_record_ids = _generate_model_record_ids(message_values, other_ids)
744 document_related_ids = []
745 for model, doc_ids in model_record_ids.items():
746 model_obj = self.pool[model]
747 mids = model_obj.exists(cr, uid, list(doc_ids))
748 if hasattr(model_obj, 'check_mail_message_access'):
749 model_obj.check_mail_message_access(cr, uid, mids, operation, context=context)
751 self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, context=context)
752 document_related_ids += [mid for mid, message in message_values.iteritems()
753 if message.get('model') == model and message.get('res_id') in mids]
755 # Calculate remaining ids: if not void, raise an error
756 other_ids = other_ids.difference(set(document_related_ids))
759 raise orm.except_orm(_('Access Denied'),
760 _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
761 (self._description, operation))
763 def _get_record_name(self, cr, uid, values, context=None):
764 """ Return the related document name, using name_get. It is done using
765 SUPERUSER_ID, to be sure to have the record name correctly stored. """
766 if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
768 return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
770 def _get_reply_to(self, cr, uid, values, context=None):
771 """ Return a specific reply_to: alias of the document through message_get_reply_to
772 or take the email_from
774 model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
775 ctx = dict(context, thread_model=model)
776 return self.pool['mail.thread'].message_get_reply_to(cr, uid, [res_id], default=email_from, context=ctx)[res_id]
778 def _get_message_id(self, cr, uid, values, context=None):
779 if values.get('no_auto_thread', False) is True:
780 message_id = tools.generate_tracking_message_id('reply_to')
781 elif values.get('res_id') and values.get('model'):
782 message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
784 message_id = tools.generate_tracking_message_id('private')
787 def create(self, cr, uid, values, context=None):
788 context = dict(context or {})
789 default_starred = context.pop('default_starred', False)
791 if 'email_from' not in values: # needed to compute reply_to
792 values['email_from'] = self._get_default_from(cr, uid, context=context)
793 if 'message_id' not in values:
794 values['message_id'] = self._get_message_id(cr, uid, values, context=context)
795 if 'reply_to' not in values:
796 values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
797 if 'record_name' not in values and 'default_record_name' not in context:
798 values['record_name'] = self._get_record_name(cr, uid, values, context=context)
800 newid = super(mail_message, self).create(cr, uid, values, context)
802 self._notify(cr, uid, newid, context=context,
803 force_send=context.get('mail_notify_force_send', True),
804 user_signature=context.get('mail_notify_user_signature', True))
805 # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
806 # Because starred will call set_message_starred, that looks for notifications.
807 # When creating a new mail_message, it will create a notification to a message
808 # that does not exist, leading to an error (key not existing). Also this
809 # this means unread notifications will be created, yet we can not assure
810 # this is what we want.
812 self.set_message_starred(cr, uid, [newid], True, context=context)
815 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
816 """ Override to explicitely call check_access_rule, that is not called
817 by the ORM. It instead directly fetches ir.rules and apply them. """
818 self.check_access_rule(cr, uid, ids, 'read', context=context)
819 res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
822 def unlink(self, cr, uid, ids, context=None):
823 # cascade-delete attachments that are directly attached to the message (should only happen
824 # for mail.messages that act as parent for a standalone mail.mail record).
825 self.check_access_rule(cr, uid, ids, 'unlink', context=context)
826 attachments_to_delete = []
827 for message in self.browse(cr, uid, ids, context=context):
828 for attach in message.attachment_ids:
829 if attach.res_model == self._name and (attach.res_id == message.id or attach.res_id == 0):
830 attachments_to_delete.append(attach.id)
831 if attachments_to_delete:
832 self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
833 return super(mail_message, self).unlink(cr, uid, ids, context=context)
835 #------------------------------------------------------
837 #------------------------------------------------------
839 def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
840 """ Add the related record followers to the destination partner_ids if is not a private message.
841 Call mail_notification.notify to manage the email sending
843 notification_obj = self.pool.get('mail.notification')
844 message = self.browse(cr, uid, newid, context=context)
845 partners_to_notify = set([])
847 # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message)
848 if message.subtype_id and message.model and message.res_id:
849 fol_obj = self.pool.get("mail.followers")
850 # browse as SUPERUSER because rules could restrict the search results
851 fol_ids = fol_obj.search(
853 ('res_model', '=', message.model),
854 ('res_id', '=', message.res_id),
856 partners_to_notify |= set(
857 fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
858 if message.subtype_id.id in [st.id for st in fo.subtype_ids]
860 # remove me from notified partners, unless the message is written on my own wall
861 if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
862 partners_to_notify |= set([message.author_id.id])
863 elif message.author_id:
864 partners_to_notify -= set([message.author_id.id])
866 # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
867 if message.partner_ids:
868 partners_to_notify |= set([p.id for p in message.partner_ids])
871 notification_obj._notify(
872 cr, uid, newid, partners_to_notify=list(partners_to_notify), context=context,
873 force_send=force_send, user_signature=user_signature
877 # An error appear when a user receive a notification without notifying
878 # the parent message -> add a read notification for the parent
879 if message.parent_id:
880 # all notified_partner_ids of the mail.message have to be notified for the parented messages
881 partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
882 for partner in partners_to_parent_notify:
883 notification_obj.create(cr, uid, {
884 'message_id': message.parent_id.id,
885 'partner_id': partner.id,