[IMP] improve code
[odoo/odoo.git] / addons / mail / mail_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-today OpenERP SA (<http://www.openerp.com>)
6 #
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
11 #
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
16 #
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/>
19 #
20 ##############################################################################
21
22 import logging
23 import tools
24
25 from email.header import decode_header
26 from openerp import SUPERUSER_ID
27 from openerp.osv import osv, orm, fields
28 from openerp.tools.translate import _
29
30 _logger = logging.getLogger(__name__)
31
32 try:
33     from mako.template import Template as MakoTemplate
34 except ImportError:
35     _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
36
37
38 """ Some tools for parsing / creating email fields """
39 def decode(text):
40     """Returns unicode() string conversion of the the given encoded smtp header text"""
41     if text:
42         text = decode_header(text.replace('\r', ''))
43         return ''.join([tools.ustr(x[0], x[1]) for x in text])
44
45
46 class mail_message(osv.Model):
47     """ Messages model: system notification (replacing res.log notifications),
48         comments (OpenChatter discussion) and incoming emails. """
49     _name = 'mail.message'
50     _description = 'Message'
51     _inherit = ['ir.needaction_mixin']
52     _order = 'id desc'
53
54     _message_read_limit = 10
55     _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
56         'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids']
57     _message_record_name_length = 18
58     _message_read_more_limit = 1024
59
60     def _shorten_name(self, name):
61         if len(name) <= (self._message_record_name_length + 3):
62             return name
63         return name[:self._message_record_name_length] + '...'
64
65     def _get_record_name(self, cr, uid, ids, name, arg, context=None):
66         """ Return the related document name, using name_get. It is done using
67             SUPERUSER_ID, to be sure to have the record name correctly stored. """
68         # TDE note: regroup by model/ids, to have less queries to perform
69         result = dict.fromkeys(ids, False)
70         for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
71             if not message.get('model') or not message.get('res_id'):
72                 continue
73             result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
74         return result
75
76     def _get_to_read(self, cr, uid, ids, name, arg, context=None):
77         """ Compute if the message is unread by the current user. """
78         res = dict((id, False) for id in ids)
79         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
80         notif_obj = self.pool.get('mail.notification')
81         notif_ids = notif_obj.search(cr, uid, [
82             ('partner_id', 'in', [partner_id]),
83             ('message_id', 'in', ids),
84             ('read', '=', False),
85         ], context=context)
86         for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
87             res[notif.message_id.id] = True
88         return res
89
90     def _search_to_read(self, cr, uid, obj, name, domain, context=None):
91         """ Search for messages to read by the current user. Condition is
92             inversed because we search unread message on a read column. """
93         if domain[0][2]:
94             read_cond = "(read = False OR read IS NULL)"
95         else:
96             read_cond = "read = True"
97         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
98         cr.execute("SELECT message_id FROM mail_notification "\
99                         "WHERE partner_id = %%s AND %s" % read_cond,
100                     (partner_id,))
101         return [('id', 'in', [r[0] for r in cr.fetchall()])]
102
103     def name_get(self, cr, uid, ids, context=None):
104         # name_get may receive int id instead of an id list
105         if isinstance(ids, (int, long)):
106             ids = [ids]
107         res = []
108         for message in self.browse(cr, uid, ids, context=context):
109             name = '%s: %s' % (message.subject or '', message.body or '')
110             res.append((message.id, self._shorten_name(name.lstrip(' :'))))
111         return res
112
113     _columns = {
114         'type': fields.selection([
115                         ('email', 'Email'),
116                         ('comment', 'Comment'),
117                         ('notification', 'System notification'),
118                         ], 'Type',
119             help="Message type: email for email message, notification for system "\
120                  "message, comment for other messages such as user replies"),
121         'email_from': fields.char('From',
122             help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
123         'author_id': fields.many2one('res.partner', 'Author',
124             help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
125         'partner_ids': fields.many2many('res.partner', string='Recipients'),
126         'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
127             'message_id', 'partner_id', 'Recipients'),
128         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
129             'message_id', 'attachment_id', 'Attachments'),
130         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
131         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
132         'model': fields.char('Related Document Model', size=128, select=1),
133         'res_id': fields.integer('Related Document ID', select=1),
134         'record_name': fields.function(_get_record_name, type='char',
135             store=True, string='Message Record Name',
136             help="Name get of the related document."),
137         'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
138         'subject': fields.char('Subject'),
139         'date': fields.datetime('Date'),
140         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
141         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
142         'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
143             type='boolean', string='To read',
144             help='Functional field to search for messages the current user has to read'),
145         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
146         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
147             'message_id', 'user_id', string='Votes',
148             help='Users that voted for this message'),
149         'favorite_user_ids': fields.many2many('res.users', 'mail_favorite',
150             'message_id', 'user_id', string='Favorite',
151             help='Users that set this message in their favorites'),
152     }
153
154     def _needaction_domain_get(self, cr, uid, context=None):
155         if self._needaction:
156             return [('to_read', '=', True)]
157         return []
158
159     def _get_default_author(self, cr, uid, context=None):
160         return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
161
162     _defaults = {
163         'type': 'email',
164         'date': lambda *a: fields.datetime.now(),
165         'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
166         'body': '',
167     }
168
169     #------------------------------------------------------
170     # Vote/Like
171     #------------------------------------------------------
172
173     def vote_toggle(self, cr, uid, ids, context=None):
174         ''' Toggles vote. Performed using read to avoid access rights issues.
175             Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
176         for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
177             new_has_voted = not (uid in message.get('vote_user_ids'))
178             if new_has_voted:
179                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
180             else:
181                 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
182         return new_has_voted or False
183
184     #------------------------------------------------------
185     # Favorite
186     #------------------------------------------------------
187
188     def favorite_toggle(self, cr, uid, ids, context=None):
189         ''' Toggles favorite. Performed using read to avoid access rights issues.
190             Done as SUPERUSER_ID because uid may star a message he cannot modify. '''
191         for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context):
192             new_is_favorite = not (uid in message.get('favorite_user_ids'))
193             if new_is_favorite:
194                 self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context)
195             else:
196                 self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context)
197         return new_is_favorite or False
198
199     #------------------------------------------------------
200     # Message loading for web interface
201     #------------------------------------------------------
202
203     def _message_get_dict(self, cr, uid, message, context=None):
204         """ Return a dict representation of the message. This representation is
205             used in the JS client code, to display the messages.
206
207             :param dict message: read result of a mail.message
208         """
209         # TDE note: this method should be optimized, to lessen the number of queries, will be done ASAP
210         is_author = False
211         if message['author_id']:
212             is_author = message['author_id'][0] == self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
213             author_id = message['author_id']
214         elif message['email_from']:
215             author_id = (0, message['email_from'])
216
217         has_voted = False
218         if uid in message.get('vote_user_ids'):
219             has_voted = True
220
221         is_favorite = False
222         if uid in message.get('favorite_user_ids'):
223             is_favorite = True
224
225         is_private = True
226         if message.get('model') and message.get('res_id'):
227             is_private = False
228
229         try:
230             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)]
231         except (orm.except_orm, osv.except_osv):
232             attachment_ids = []
233
234         # TDE note: should we send partner_ids ?
235         # TDE note: shouldn't we separated followers and other partners ? costly to compute maybe ,
236         try:
237             partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context)
238         except (orm.except_orm, osv.except_osv):
239             partner_ids = []
240
241         return {
242             'id': message['id'],
243             'type': message['type'],
244             'attachment_ids': attachment_ids,
245             'body': message['body'],
246             'model': message['model'],
247             'res_id': message['res_id'],
248             'record_name': message['record_name'],
249             'subject': message['subject'],
250             'date': message['date'],
251             'author_id': author_id,
252             'is_author': is_author,
253             'partner_ids': partner_ids,
254             'parent_id': False,
255             'vote_nb': len(message['vote_user_ids']),
256             'has_voted': has_voted,
257             'is_private': is_private,
258             'is_favorite': is_favorite,
259             'to_read': message['to_read'],
260         }
261
262     def _message_read_add_expandables(self, cr, uid, message_list, read_messages,
263             thread_level=0, message_loaded_ids=[], domain=[], parent_id=False, context=None, limit=None):
264         """ Create expandables for message_read, to load new messages.
265             1. get the expandable for new threads
266                 if display is flat (thread_level == 0):
267                     fetch message_ids < min(already displayed ids), because we
268                     want a flat display, ordered by id
269                 else:
270                     fetch message_ids that are not childs of already displayed
271                     messages
272             2. get the expandables for new messages inside threads if display
273                is not flat
274                 for each thread header, search for its childs
275                     for each hole in the child list based on message displayed,
276                     create an expandable
277
278             :param list message_list:list of message structure for the Chatter
279                 widget to which expandables are added
280             :param dict read_messages: dict [id]: read result of the messages to
281                 easily have access to their values, given their ID
282             :return bool: True
283         """
284         def _get_expandable(domain, message_nb, parent_id, id, model):
285             return {
286                 'domain': domain,
287                 'nb_messages': message_nb,
288                 'type': 'expandable',
289                 'parent_id': parent_id,
290                 'id':  id,
291                 # TDE note: why do we need model sometimes, and sometimes not ???
292                 'model': model,
293             }
294
295         # all_not_loaded_ids = []
296         id_list = sorted(read_messages.keys())
297         if not id_list:
298             return message_list
299
300         # 1. get the expandable for new threads
301         if thread_level == 0:
302             exp_domain = domain + [('id', '<', min(message_loaded_ids + id_list))]
303         else:
304             exp_domain = domain + ['!', ('id', 'child_of', message_loaded_ids + id_list)]
305         ids = self.search(cr, uid, exp_domain, context=context, limit=1)
306         if ids:
307             message_list.append(_get_expandable(exp_domain, -1, parent_id, -1, None))
308
309         # 2. get the expandables for new messages inside threads if display is not flat
310         if thread_level == 0:
311             return True
312         for message_id in id_list:
313             message = read_messages[message_id]
314
315             # message is not a thread header (has a parent_id)
316             # TDE note: parent_id is false is there is a parent we can not see -> ok
317             if message.get('parent_id'):
318                 continue
319
320             # TDE note: check search is correctly implemented in mail.message
321             not_loaded_ids = self.search(cr, uid, [
322                 ('id', 'child_of', message['id']),
323                 ('id', 'not in', message_loaded_ids),
324                 ], context=context, limit=self._message_read_more_limit)
325             if not not_loaded_ids:
326                 continue
327
328             # all_not_loaded_ids += not_loaded_ids
329             # group childs not read
330             id_min, id_max, nb = max(not_loaded_ids), 0, 0
331             for not_loaded_id in not_loaded_ids:
332                 if not read_messages.get(not_loaded_id):
333                     nb += 1
334                     if id_min > not_loaded_id:
335                         id_min = not_loaded_id
336                     if id_max < not_loaded_id:
337                         id_max = not_loaded_id
338                 elif nb > 0:
339                     exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
340                     message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model')))
341                     id_min, id_max, nb = max(not_loaded_ids), 0, 0
342                 else:
343                     id_min, id_max, nb = max(not_loaded_ids), 0, 0
344             if nb > 0:
345                 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
346                 message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model')))
347
348         # message_loaded_ids = list(set(message_loaded_ids + read_messages.keys() + all_not_loaded_ids))
349
350         return True
351
352     def _get_parent(self, cr, uid, message, context=None):
353         """ Tools method that tries to get the parent of a mail.message. If
354             no parent, or if uid has no access right on the parent, False
355             is returned.
356
357             :param dict message: read result of a mail.message
358         """
359         if not message['parent_id']:
360             return False
361         parent_id = message['parent_id'][0]
362         try:
363             return self.read(cr, uid, parent_id, self._message_read_fields, context=context)
364         except (orm.except_orm, osv.except_osv):
365             return False
366
367     def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, thread_level=0, context=None, parent_id=False, limit=None):
368         """ Read messages from mail.message, and get back a list of structured
369             messages to be displayed as discussion threads. If IDs is set,
370             fetch these records. Otherwise use the domain to fetch messages.
371             After having fetch messages, their ancestors will be added to obtain
372             well formed threads, if uid has access to them.
373
374             After reading the messages, expandable messages are added in the
375             message list (see ``_message_read_add_expandables``). It consists
376             in messages holding the 'read more' data: number of messages to
377             read, domain to apply.
378
379             :param list ids: optional IDs to fetch
380             :param list domain: optional domain for searching ids if ids not set
381             :param list message_unload_ids: optional ids we do not want to fetch,
382                 because i.e. they are already displayed somewhere
383             :param int parent_id: context of parent_id
384                 - if parent_id reached when adding ancestors, stop going further
385                   in the ancestor search
386                 - if set in flat mode, ancestor_id is set to parent_id
387             :param int limit: number of messages to fetch, before adding the
388                 ancestors and expandables
389             :return list: list of message structure for the Chatter widget
390         """
391         # print 'message_read', ids, domain, message_unload_ids, thread_level, context, parent_id, limit
392         assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
393         domain = domain if domain is not None else []
394         message_unload_ids = message_unload_ids if message_unload_ids is not None else []
395         if message_unload_ids:
396             domain += [('id', 'not in', message_unload_ids)]
397         limit = limit or self._message_read_limit
398         read_messages = {}
399         message_list = []
400
401         # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
402         if ids is None:
403             ids = self.search(cr, uid, domain, context=context, limit=limit)
404         for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
405             message_id = message['id']
406
407             # if not in tree and not in message_loaded list
408             if not message_id in read_messages and not message_id in message_unload_ids:
409                 read_messages[message_id] = message
410                 message_list.append(self._message_get_dict(cr, uid, message, context=context))
411
412                 # get the older ancestor the user can read, update its ancestor field
413                 if not thread_level:
414                     message_list[-1]['parent_id'] = parent_id
415                     continue
416                 parent = self._get_parent(cr, uid, message, context=context)
417                 while parent and parent.get('id') != parent_id:
418                     message_list[-1]['parent_id'] = parent.get('id')
419                     message = parent
420                     parent = self._get_parent(cr, uid, message, context=context)
421                 # if in thread: add its ancestor to the list of messages
422                 if not message['id'] in read_messages and not message['id'] in message_unload_ids:
423                     read_messages[message['id']] = message
424                     message_list.append(self._message_get_dict(cr, uid, message, context=context))
425
426         # get the child expandable messages for the tree
427         message_list = sorted(message_list, key=lambda k: k['id'])
428         self._message_read_add_expandables(cr, uid, message_list, read_messages, thread_level=thread_level,
429             message_loaded_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context, limit=limit)
430
431         return message_list
432
433     # TDE Note: do we need this ?
434     # def user_free_attachment(self, cr, uid, context=None):
435     #     attachment = self.pool.get('ir.attachment')
436     #     attachment_list = []
437     #     attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('create_uid', '=', uid)])
438     #     if len(attachment_ids):
439     #         attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)]
440     #     return attachment_list
441
442     #------------------------------------------------------
443     # mail_message internals
444     #------------------------------------------------------
445
446     def init(self, cr):
447         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
448         if not cr.fetchone():
449             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
450
451     def _search(self, cr, uid, args, offset=0, limit=None, order=None,
452         context=None, count=False, access_rights_uid=None):
453         """ Override that adds specific access rights of mail.message, to remove
454             ids uid could not see according to our custom rules. Please refer
455             to check_access_rule for more details about those rules.
456
457             After having received ids of a classic search, keep only:
458             - if author_id == pid, uid is the author, OR
459             - a notification (id, pid) exists, uid has been notified, OR
460             - uid have read access to the related document is model, res_id
461             - otherwise: remove the id
462         """
463         # Rules do not apply to administrator
464         # print '_search', uid, args
465         if uid == SUPERUSER_ID:
466             return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
467                 context=context, count=count, access_rights_uid=access_rights_uid)
468         # Perform a super with count as False, to have the ids, not a counter
469         ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
470             context=context, count=False, access_rights_uid=access_rights_uid)
471         if not ids and count:
472             return 0
473         elif not ids:
474             return ids
475
476         pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
477         author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
478         model_ids = {}
479
480         messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
481         for message in messages:
482             if message.get('author_id') and message.get('author_id')[0] == pid:
483                 author_ids.add(message.get('id'))
484             elif pid in message.get('notified_partner_ids'):
485                 partner_ids.add(message.get('id'))
486             elif message.get('model') and message.get('res_id'):
487                 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
488
489         model_access_obj = self.pool.get('ir.model.access')
490         for doc_model, doc_dict in model_ids.iteritems():
491             if not model_access_obj.check(cr, uid, doc_model, 'read', False):
492                 continue
493             doc_ids = doc_dict.keys()
494             allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
495             allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
496
497         final_ids = author_ids | partner_ids | allowed_ids
498         if count:
499             return len(final_ids)
500         else:
501             return list(final_ids)
502
503     def check_access_rule(self, cr, uid, ids, operation, context=None):
504         """ Access rules of mail.message:
505             - read: if
506                 - author_id == pid, uid is the author, OR
507                 - mail_notification (id, pid) exists, uid has been notified, OR
508                 - uid have read access to the related document if model, res_id
509                 - otherwise: raise
510             - create: if
511                 - no model, no res_id, I create a private message
512                 - pid in message_follower_ids if model, res_id OR
513                 - uid have write access on the related document if model, res_id, OR
514                 - otherwise: raise
515             - write: if
516                 - uid has write access on the related document if model, res_id
517                 - Otherwise: raise
518             - unlink: if
519                 - uid has write access on the related document if model, res_id
520                 - Otherwise: raise
521         """
522         if uid == SUPERUSER_ID:
523             return
524         if isinstance(ids, (int, long)):
525             ids = [ids]
526         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
527
528         # Read mail_message.ids to have their values
529         message_values = dict.fromkeys(ids)
530         model_record_ids = {}
531         cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
532         for id, rmod, rid, author_id in cr.fetchall():
533             message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
534             if rmod:
535                 model_record_ids.setdefault(rmod, dict()).setdefault(rid, set()).add(id)
536
537         # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
538         if operation == 'read':
539             author_ids = [mid for mid, message in message_values.iteritems()
540                 if message.get('author_id') and message.get('author_id') == partner_id]
541         elif operation == 'create':
542             author_ids = [mid for mid, message in message_values.iteritems()
543                 if not message.get('model') and not message.get('res_id')]
544         else:
545             author_ids = []
546
547         # 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
548         if operation == 'read':
549             not_obj = self.pool.get('mail.notification')
550             not_ids = not_obj.search(cr, SUPERUSER_ID, [
551                 ('partner_id', '=', partner_id),
552                 ('message_id', 'in', ids),
553             ], context=context)
554             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
555         elif operation == 'create':
556             notified_ids = []
557             for doc_model, doc_dict in model_record_ids.items():
558                 fol_obj = self.pool.get('mail.followers')
559                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
560                     ('res_model', '=', doc_model),
561                     ('res_id', 'in', list(doc_dict.keys())),
562                     ('partner_id', '=', partner_id),
563                     ], context=context)
564                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
565                 notified_ids += [mid for mid, message in message_values.iteritems()
566                     if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
567         else:
568             notified_ids = []
569
570         # Calculate remaining ids, and related model/res_ids
571         model_record_ids = {}
572         other_ids = set(ids).difference(set(author_ids), set(notified_ids))
573         for id in other_ids:
574             if message_values[id]['res_model']:
575                 model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
576
577         # CRUD: Access rights related to the document
578         document_related_ids = []
579         for model, mids in model_record_ids.items():
580             model_obj = self.pool.get(model)
581             mids = model_obj.exists(cr, uid, mids)
582             if operation in ['create', 'write', 'unlink']:
583                 model_obj.check_access_rights(cr, uid, 'write')
584                 model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
585             else:
586                 model_obj.check_access_rights(cr, uid, operation)
587                 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
588             document_related_ids += [mid for mid, message in message_values.iteritems()
589                 if message.get('res_model') == model and message.get('res_id') in mids]
590
591         # Calculate remaining ids: if not void, raise an error
592         other_ids = other_ids - set(document_related_ids)
593         if not other_ids:
594             return
595         raise orm.except_orm(_('Access Denied'),
596                             _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
597                             (self._description, operation))
598
599     def create(self, cr, uid, values, context=None):
600         if not values.get('message_id') and values.get('res_id') and values.get('model'):
601             values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
602         newid = super(mail_message, self).create(cr, uid, values, context)
603         self._notify(cr, SUPERUSER_ID, newid, context=context)
604         return newid
605
606     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
607         """ Override to explicitely call check_access_rule, that is not called
608             by the ORM. It instead directly fetches ir.rules and apply them. """
609         self.check_access_rule(cr, uid, ids, 'read', context=context)
610         res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
611         return res
612
613     def unlink(self, cr, uid, ids, context=None):
614         # cascade-delete attachments that are directly attached to the message (should only happen
615         # for mail.messages that act as parent for a standalone mail.mail record).
616         self.check_access_rule(cr, uid, ids, 'unlink', context=context)
617         attachments_to_delete = []
618         for message in self.browse(cr, uid, ids, context=context):
619             for attach in message.attachment_ids:
620                 if attach.res_model == self._name and attach.res_id == message.id:
621                     attachments_to_delete.append(attach.id)
622         if attachments_to_delete:
623             self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
624         return super(mail_message, self).unlink(cr, uid, ids, context=context)
625
626     def copy(self, cr, uid, id, default=None, context=None):
627         """ Overridden to avoid duplicating fields that are unique to each email """
628         if default is None:
629             default = {}
630         default.update(message_id=False, headers=False)
631         return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
632
633     #------------------------------------------------------
634     # Messaging API
635     #------------------------------------------------------
636
637     # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
638     # will be added to email send for notifications. Currently only WIP.
639     MAIL_TEMPLATE = """<div>
640     % if message:
641         ${display_message(message)}
642     % endif
643     % for ctx_msg in context_messages:
644         ${display_message(ctx_msg)}
645     % endfor
646     % if add_expandable:
647         ${display_expandable()}
648     % endif
649     ${display_message(header_message)}
650     </div>
651
652     <%def name="display_message(message)">
653         <div>
654             Subject: ${message.subject}<br />
655             Body: ${message.body}
656         </div>
657     </%def>
658
659     <%def name="display_expandable()">
660         <div>This is an expandable.</div>
661     </%def>
662     """
663
664     def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
665         """
666             1. message.parent_id = False: new thread, no quote_context
667             2. get the lasts messages in the thread before message
668             3. get the message header
669             4. add an expandable between them
670
671             :param dict quote_context: options for quoting
672             :return string: html quote
673         """
674         add_expandable = False
675
676         message = self.browse(cr, uid, id, context=context)
677         if not message.parent_id:
678             return ''
679         context_ids = self.search(cr, uid, [
680             ('parent_id', '=', message.parent_id.id),
681             ('id', '<', message.id),
682             ], limit=limit, context=context)
683
684         if len(context_ids) >= limit:
685             add_expandable = True
686             context_ids = context_ids[0:-1]
687
688         context_ids.append(message.parent_id.id)
689         context_messages = self.browse(cr, uid, context_ids, context=context)
690         header_message = context_messages.pop()
691
692         try:
693             if not add_original:
694                 message = False
695             result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
696                                                         context_messages=context_messages,
697                                                         header_message=header_message,
698                                                         add_expandable=add_expandable,
699                                                         # context kw would clash with mako internals
700                                                         ctx=context,
701                                                         format_exceptions=True)
702             result = result.strip()
703             return result
704         except Exception:
705             _logger.exception("failed to render mako template for quoting message")
706             return ''
707         return result
708
709     def _notify(self, cr, uid, newid, context=None):
710         """ Add the related record followers to the destination partner_ids if is not a private message.
711             Call mail_notification.notify to manage the email sending
712         """
713         message = self.read(cr, uid, newid, ['model', 'res_id', 'author_id', 'subtype_id', 'partner_ids'], context=context)
714
715         partners_to_notify = set([])
716         # message has no subtype_id: pure log message -> no partners, no one notified
717         if not message.get('subtype_id'):
718             return True
719         # all partner_ids of the mail.message have to be notified
720         if message.get('partner_ids'):
721             partners_to_notify |= set(message.get('partner_ids'))
722         # all followers of the mail.message document have to be added as partners and notified
723         if message.get('model') and message.get('res_id'):
724             fol_obj = self.pool.get("mail.followers")
725             fol_ids = fol_obj.search(cr, uid, [
726                 ('res_model', '=', message.get('model')),
727                 ('res_id', '=', message.get('res_id')),
728                 ('subtype_ids', 'in', message.get('subtype_id')[0])
729                 ], context=context)
730             fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context)
731             partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs)
732         # when writing to a wall
733         if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]:
734             partners_to_notify |= set([message.get('author_id')[0]])
735         elif message.get('author_id'):
736             partners_to_notify = partners_to_notify - set([message.get('author_id')[0]])
737
738         if partners_to_notify:
739             self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p_id) for p_id in partners_to_notify]}, context=context)
740
741         self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
742
743     #------------------------------------------------------
744     # Tools
745     #------------------------------------------------------
746
747     def check_partners_email(self, cr, uid, partner_ids, context=None):
748         """ Verify that selected partner_ids have an email_address defined.
749             Otherwise throw a warning. """
750         partner_wo_email_lst = []
751         for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
752             if not partner.email:
753                 partner_wo_email_lst.append(partner)
754         if not partner_wo_email_lst:
755             return {}
756         warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
757         for partner in partner_wo_email_lst:
758             warning_msg += '\n- %s' % (partner.name)
759         return {'warning': {
760                     'title': _('Partners email addresses not found'),
761                     'message': warning_msg,
762                     }
763                 }