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 ##############################################################################
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 import html_email_clean
29 from openerp.tools.translate import _
31 _logger = logging.getLogger(__name__)
34 from mako.template import Template as MakoTemplate
36 _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
39 """ Some tools for parsing / creating email fields """
41 """Returns unicode() string conversion of the the given encoded smtp header text"""
43 text = decode_header(text.replace('\r', ''))
44 return ''.join([tools.ustr(x[0], x[1]) for x in text])
47 class mail_message(osv.Model):
48 """ Messages model: system notification (replacing res.log notifications),
49 comments (OpenChatter discussion) and incoming emails. """
50 _name = 'mail.message'
51 _description = 'Message'
52 _inherit = ['ir.needaction_mixin']
54 _rec_name = 'record_name'
56 _message_read_limit = 30
57 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
58 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
59 _message_record_name_length = 18
60 _message_read_more_limit = 1024
62 def default_get(self, cr, uid, fields, context=None):
63 # protection for `default_type` values leaking from menu action context (e.g. for invoices)
64 if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
65 context = dict(context, default_type=None)
66 return super(mail_message, self).default_get(cr, uid, fields, context=context)
68 def _shorten_name(self, name):
69 if len(name) <= (self._message_record_name_length + 3):
71 return name[:self._message_record_name_length] + '...'
73 def _get_record_name(self, cr, uid, ids, name, arg, context=None):
74 """ Return the related document name, using name_get. It is done using
75 SUPERUSER_ID, to be sure to have the record name correctly stored. """
76 # TDE note: regroup by model/ids, to have less queries to perform
77 result = dict.fromkeys(ids, False)
78 for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
79 if not message.get('model') or not message.get('res_id'):
81 result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
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.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
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),
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 read column. """
101 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.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.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
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 messages to read by the current user. Condition is
119 inversed because we search unread message on a read column. """
120 return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
122 def name_get(self, cr, uid, ids, context=None):
123 # name_get may receive int id instead of an id list
124 if isinstance(ids, (int, long)):
127 for message in self.browse(cr, uid, ids, context=context):
128 name = '%s: %s' % (message.subject or '', message.body or '')
129 res.append((message.id, self._shorten_name(name.lstrip(' :'))))
133 'type': fields.selection([
135 ('comment', 'Comment'),
136 ('notification', 'System notification'),
138 help="Message type: email for email message, notification for system "\
139 "message, comment for other messages such as user replies"),
140 'email_from': fields.char('From',
141 help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
142 'author_id': fields.many2one('res.partner', 'Author', select=1,
144 help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
145 'partner_ids': fields.many2many('res.partner', string='Recipients'),
146 'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
147 'message_id', 'partner_id', 'Notified partners',
148 help='Partners that have a notification pushing this message in their mailboxes'),
149 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
150 'message_id', 'attachment_id', 'Attachments'),
151 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True,
152 ondelete='set null', help="Initial thread message."),
153 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
154 'model': fields.char('Related Document Model', size=128, select=1),
155 'res_id': fields.integer('Related Document ID', select=1),
156 'record_name': fields.function(_get_record_name, type='char',
157 store=True, string='Message Record Name',
158 help="Name get of the related document."),
159 'notification_ids': fields.one2many('mail.notification', 'message_id',
160 string='Notifications', auto_join=True,
161 help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
162 'subject': fields.char('Subject'),
163 'date': fields.datetime('Date'),
164 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
165 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
166 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
167 type='boolean', string='To read',
168 help='Current user has an unread notification linked to this message'),
169 'starred': fields.function(_get_starred, fnct_search=_search_starred,
170 type='boolean', string='Starred',
171 help='Current user has a starred notification linked to this message'),
172 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
173 ondelete='set null', select=1,),
174 'vote_user_ids': fields.many2many('res.users', 'mail_vote',
175 'message_id', 'user_id', string='Votes',
176 help='Users that voted for this message'),
179 def _needaction_domain_get(self, cr, uid, context=None):
180 return [('to_read', '=', True)]
182 def _get_default_author(self, cr, uid, context=None):
183 return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
187 'date': lambda *a: fields.datetime.now(),
188 'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
192 #------------------------------------------------------
194 #------------------------------------------------------
196 def vote_toggle(self, cr, uid, ids, context=None):
197 ''' Toggles vote. Performed using read to avoid access rights issues.
198 Done as SUPERUSER_ID because uid may vote for a message he cannot modify. '''
199 for message in self.read(cr, uid, ids, ['vote_user_ids'], context=context):
200 new_has_voted = not (uid in message.get('vote_user_ids'))
202 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, uid)]}, context=context)
204 self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, uid)]}, context=context)
205 return new_has_voted or False
207 #------------------------------------------------------
209 #------------------------------------------------------
211 def set_message_read(self, cr, uid, msg_ids, read, context=None):
212 """ Set messages as (un)read. Technically, the notifications related
213 to uid are set to (un)read. If for some msg_ids there are missing
214 notifications (i.e. due to load more or thread parent fetching),
217 :param bool read: set notification as (un)read
219 notification_obj = self.pool.get('mail.notification')
220 user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
221 notif_ids = notification_obj.search(cr, uid, [
222 ('partner_id', '=', user_pid),
223 ('message_id', 'in', msg_ids)
226 # all message have notifications: already set them as (un)read
227 if len(notif_ids) == len(msg_ids):
228 return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
230 # some messages do not have notifications: find which one, create notification, update read status
231 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
232 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
233 for msg_id in to_create_msg_ids:
234 notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
235 return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
237 def set_message_starred(self, cr, uid, msg_ids, starred, context=None):
238 """ Set messages as (un)starred. Technically, the notifications related
239 to uid are set to (un)starred. If for some msg_ids there are missing
240 notifications (i.e. due to load more or thread parent fetching),
243 :param bool starred: set notification as (un)starred
245 notification_obj = self.pool.get('mail.notification')
246 user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
247 notif_ids = notification_obj.search(cr, uid, [
248 ('partner_id', '=', user_pid),
249 ('message_id', 'in', msg_ids)
252 # all message have notifications: already set them as (un)starred
253 if len(notif_ids) == len(msg_ids):
254 notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
257 # some messages do not have notifications: find which one, create notification, update starred status
258 notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
259 to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
260 for msg_id in to_create_msg_ids:
261 notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
262 notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
265 #------------------------------------------------------
266 # Message loading for web interface
267 #------------------------------------------------------
269 def _message_read_dict_postprocess(self, cr, uid, messages, message_tree, context=None):
270 """ Post-processing on values given by message_read. This method will
271 handle partners in batch to avoid doing numerous queries.
273 :param list messages: list of message, as get_dict result
274 :param dict message_tree: {[msg.id]: msg browse record}
276 res_partner_obj = self.pool.get('res.partner')
277 ir_attachment_obj = self.pool.get('ir.attachment')
278 pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
280 # 1. Aggregate partners (author_id and partner_ids) and attachments
282 attachment_ids = set()
283 for key, message in message_tree.iteritems():
284 if message.author_id:
285 partner_ids |= set([message.author_id.id])
286 if message.partner_ids:
287 partner_ids |= set([partner.id for partner in message.partner_ids])
288 if message.attachment_ids:
289 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
291 # Filter author_ids uid can see
292 # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context)
293 partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context)
294 partner_tree = dict((partner[0], partner) for partner in partners)
297 attachments = ir_attachment_obj.read(cr, uid, list(attachment_ids), ['id', 'datas_fname'], context=context)
298 attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments)
300 # 3. Update message dictionaries
301 for message_dict in messages:
302 message_id = message_dict.get('id')
303 message = message_tree[message_id]
304 if message.author_id:
305 author = partner_tree[message.author_id.id]
307 author = (0, message.email_from)
309 for partner in message.partner_ids:
310 if partner.id in partner_tree:
311 partner_ids.append(partner_tree[partner.id])
313 for attachment in message.attachment_ids:
314 if attachment.id in attachments_tree:
315 attachment_ids.append(attachments_tree[attachment.id])
316 message_dict.update({
317 'is_author': pid == author[0],
319 'partner_ids': partner_ids,
320 'attachment_ids': attachment_ids,
324 def _message_read_dict(self, cr, uid, message, parent_id=False, context=None):
325 """ Return a dict representation of the message. This representation is
326 used in the JS client code, to display the messages. Partners and
327 attachments related stuff will be done in post-processing in batch.
329 :param dict message: mail.message browse record
331 # private message: no model, no res_id
333 if not message.model or not message.res_id:
335 # votes and favorites: res.users ids, no prefetching should be done
336 vote_nb = len(message.vote_user_ids)
337 has_voted = uid in [user.id for user in message.vote_user_ids]
339 return {'id': message.id,
340 'type': message.type,
341 'body': html_email_clean(message.body or ''),
342 'model': message.model,
343 'res_id': message.res_id,
344 'record_name': message.record_name,
345 'subject': message.subject,
346 'date': message.date,
347 'to_read': message.to_read,
348 'parent_id': parent_id,
349 'is_private': is_private,
354 'has_voted': has_voted,
355 'is_favorite': message.starred,
356 'attachment_ids': [],
359 def _message_read_add_expandables(self, cr, uid, messages, message_tree, parent_tree,
360 message_unload_ids=[], thread_level=0, domain=[], parent_id=False, context=None):
361 """ Create expandables for message_read, to load new messages.
362 1. get the expandable for new threads
363 if display is flat (thread_level == 0):
364 fetch message_ids < min(already displayed ids), because we
365 want a flat display, ordered by id
367 fetch message_ids that are not childs of already displayed
369 2. get the expandables for new messages inside threads if display
371 for each thread header, search for its childs
372 for each hole in the child list based on message displayed,
375 :param list messages: list of message structure for the Chatter
376 widget to which expandables are added
377 :param dict message_tree: dict [id]: browse record of this message
378 :param dict parent_tree: dict [parent_id]: [child_ids]
379 :param list message_unload_ids: list of message_ids we do not want
383 def _get_expandable(domain, message_nb, parent_id, max_limit):
386 'nb_messages': message_nb,
387 'type': 'expandable',
388 'parent_id': parent_id,
389 'max_limit': max_limit,
394 message_ids = sorted(message_tree.keys())
396 # 1. get the expandable for new threads
397 if thread_level == 0:
398 exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
400 exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
401 ids = self.search(cr, uid, exp_domain, context=context, limit=1)
403 # inside a thread: prepend
405 messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
406 # new threads: append
408 messages.append(_get_expandable(exp_domain, -1, parent_id, True))
410 # 2. get the expandables for new messages inside threads if display is not flat
411 if thread_level == 0:
413 for message_id in message_ids:
414 message = message_tree[message_id]
416 # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok)
417 if message.parent_id:
420 # check there are message for expandable
421 child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids)
422 child_ids = sorted(list(child_ids), reverse=True)
426 # make groups of unread messages
427 id_min, id_max, nb = max(child_ids), 0, 0
428 for child_id in child_ids:
429 if not child_id in message_ids:
431 if id_min > child_id:
433 if id_max < child_id:
436 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
437 idx = [msg.get('id') for msg in messages].index(child_id) + 1
438 # messages.append(_get_expandable(exp_domain, nb, message_id, False))
439 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
440 id_min, id_max, nb = max(child_ids), 0, 0
442 id_min, id_max, nb = max(child_ids), 0, 0
444 exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
445 idx = [msg.get('id') for msg in messages].index(message_id) + 1
446 # messages.append(_get_expandable(exp_domain, nb, message_id, id_min))
447 messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False))
451 def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
452 thread_level=0, context=None, parent_id=False, limit=None):
453 """ Read messages from mail.message, and get back a list of structured
454 messages to be displayed as discussion threads. If IDs is set,
455 fetch these records. Otherwise use the domain to fetch messages.
456 After having fetch messages, their ancestors will be added to obtain
457 well formed threads, if uid has access to them.
459 After reading the messages, expandable messages are added in the
460 message list (see ``_message_read_add_expandables``). It consists
461 in messages holding the 'read more' data: number of messages to
462 read, domain to apply.
464 :param list ids: optional IDs to fetch
465 :param list domain: optional domain for searching ids if ids not set
466 :param list message_unload_ids: optional ids we do not want to fetch,
467 because i.e. they are already displayed somewhere
468 :param int parent_id: context of parent_id
469 - if parent_id reached when adding ancestors, stop going further
470 in the ancestor search
471 - if set in flat mode, ancestor_id is set to parent_id
472 :param int limit: number of messages to fetch, before adding the
473 ancestors and expandables
474 :return list: list of message structure for the Chatter widget
476 assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
477 domain = domain if domain is not None else []
478 message_unload_ids = message_unload_ids if message_unload_ids is not None else []
479 if message_unload_ids:
480 domain += [('id', 'not in', message_unload_ids)]
481 limit = limit or self._message_read_limit
486 # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
488 ids = self.search(cr, uid, domain, context=context, limit=limit)
490 # fetch parent if threaded, sort messages
491 for message in self.browse(cr, uid, ids, context=context):
492 message_id = message.id
493 if message_id in message_tree:
495 message_tree[message_id] = message
498 if thread_level == 0:
499 tree_parent_id = parent_id
501 tree_parent_id = message_id
503 while parent.parent_id and parent.parent_id.id != parent_id:
504 parent = parent.parent_id
505 tree_parent_id = parent.id
506 if not parent.id in message_tree:
507 message_tree[parent.id] = parent
508 # newest messages first
509 parent_tree.setdefault(tree_parent_id, [])
510 if tree_parent_id != message_id:
511 parent_tree[tree_parent_id].append(self._message_read_dict(cr, uid, message_tree[message_id], parent_id=tree_parent_id, context=context))
514 for key, message_id_list in parent_tree.iteritems():
515 message_id_list.sort(key=lambda item: item['id'])
516 message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context))
518 parent_list = parent_tree.items()
519 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)
520 message_list = [message for (key, msg_list) in parent_list for message in msg_list]
522 # get the child expandable messages for the tree
523 self._message_read_dict_postprocess(cr, uid, message_list, message_tree, context=context)
524 self._message_read_add_expandables(cr, uid, message_list, message_tree, parent_tree,
525 thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
528 #------------------------------------------------------
529 # mail_message internals
530 #------------------------------------------------------
533 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
534 if not cr.fetchone():
535 cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
537 def _search(self, cr, uid, args, offset=0, limit=None, order=None,
538 context=None, count=False, access_rights_uid=None):
539 """ Override that adds specific access rights of mail.message, to remove
540 ids uid could not see according to our custom rules. Please refer
541 to check_access_rule for more details about those rules.
543 After having received ids of a classic search, keep only:
544 - if author_id == pid, uid is the author, OR
545 - a notification (id, pid) exists, uid has been notified, OR
546 - uid have read access to the related document is model, res_id
547 - otherwise: remove the id
549 # Rules do not apply to administrator
550 if uid == SUPERUSER_ID:
551 return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
552 context=context, count=count, access_rights_uid=access_rights_uid)
553 # Perform a super with count as False, to have the ids, not a counter
554 ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
555 context=context, count=False, access_rights_uid=access_rights_uid)
556 if not ids and count:
561 pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
562 author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
565 messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
566 for message in messages:
567 if message.get('author_id') and message.get('author_id')[0] == pid:
568 author_ids.add(message.get('id'))
569 elif pid in message.get('notified_partner_ids'):
570 partner_ids.add(message.get('id'))
571 elif message.get('model') and message.get('res_id'):
572 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
574 model_access_obj = self.pool.get('ir.model.access')
575 for doc_model, doc_dict in model_ids.iteritems():
576 if not model_access_obj.check(cr, uid, doc_model, 'read', False):
578 doc_ids = doc_dict.keys()
579 allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
580 allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
582 final_ids = author_ids | partner_ids | allowed_ids
584 return len(final_ids)
586 return list(final_ids)
588 def check_access_rule(self, cr, uid, ids, operation, context=None):
589 """ Access rules of mail.message:
591 - author_id == pid, uid is the author, OR
592 - mail_notification (id, pid) exists, uid has been notified, OR
593 - uid have read access to the related document if model, res_id
596 - no model, no res_id, I create a private message
597 - pid in message_follower_ids if model, res_id OR
598 - uid have write access on the related document if model, res_id, OR
601 - uid has write access on the related document if model, res_id
604 - uid has write access on the related document if model, res_id
607 if uid == SUPERUSER_ID:
609 if isinstance(ids, (int, long)):
611 partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
613 # Read mail_message.ids to have their values
614 message_values = dict.fromkeys(ids)
615 model_record_ids = {}
616 cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
617 for id, rmod, rid, author_id in cr.fetchall():
618 message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
620 model_record_ids.setdefault(rmod, dict()).setdefault(rid, set()).add(id)
622 # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
623 if operation == 'read':
624 author_ids = [mid for mid, message in message_values.iteritems()
625 if message.get('author_id') and message.get('author_id') == partner_id]
626 elif operation == 'create':
627 author_ids = [mid for mid, message in message_values.iteritems()
628 if not message.get('model') and not message.get('res_id')]
632 # 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
633 if operation == 'read':
634 not_obj = self.pool.get('mail.notification')
635 not_ids = not_obj.search(cr, SUPERUSER_ID, [
636 ('partner_id', '=', partner_id),
637 ('message_id', 'in', ids),
639 notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
640 elif operation == 'create':
642 for doc_model, doc_dict in model_record_ids.items():
643 fol_obj = self.pool.get('mail.followers')
644 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
645 ('res_model', '=', doc_model),
646 ('res_id', 'in', list(doc_dict.keys())),
647 ('partner_id', '=', partner_id),
649 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
650 notified_ids += [mid for mid, message in message_values.iteritems()
651 if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
655 # Calculate remaining ids, and related model/res_ids
656 model_record_ids = {}
657 other_ids = set(ids).difference(set(author_ids), set(notified_ids))
659 if message_values[id]['res_model']:
660 model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
662 # CRUD: Access rights related to the document
663 document_related_ids = []
664 for model, mids in model_record_ids.items():
665 model_obj = self.pool.get(model)
666 mids = model_obj.exists(cr, uid, mids)
667 if operation in ['create', 'write', 'unlink']:
668 model_obj.check_access_rights(cr, uid, 'write')
669 model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
671 model_obj.check_access_rights(cr, uid, operation)
672 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
673 document_related_ids += [mid for mid, message in message_values.iteritems()
674 if message.get('res_model') == model and message.get('res_id') in mids]
676 # Calculate remaining ids: if not void, raise an error
677 other_ids = other_ids - set(document_related_ids)
680 raise orm.except_orm(_('Access Denied'),
681 _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
682 (self._description, operation))
684 def create(self, cr, uid, values, context=None):
687 default_starred = context.pop('default_starred', False)
688 if not values.get('message_id') and values.get('res_id') and values.get('model'):
689 values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
690 elif not values.get('message_id'):
691 values['message_id'] = tools.generate_tracking_message_id('private')
692 newid = super(mail_message, self).create(cr, uid, values, context)
693 self._notify(cr, SUPERUSER_ID, newid, context=context)
694 # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
695 # Because starred will call set_message_starred, that looks for notifications.
696 # When creating a new mail_message, it will create a notification to a message
697 # that does not exist, leading to an error (key not existing). Also this
698 # this means unread notifications will be created, yet we can not assure
699 # this is what we want.
701 self.set_message_starred(cr, uid, [newid], True, context=context)
704 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
705 """ Override to explicitely call check_access_rule, that is not called
706 by the ORM. It instead directly fetches ir.rules and apply them. """
707 self.check_access_rule(cr, uid, ids, 'read', context=context)
708 res = super(mail_message, self).read(cr, uid, ids, fields=fields, context=context, load=load)
711 def unlink(self, cr, uid, ids, context=None):
712 # cascade-delete attachments that are directly attached to the message (should only happen
713 # for mail.messages that act as parent for a standalone mail.mail record).
714 self.check_access_rule(cr, uid, ids, 'unlink', context=context)
715 attachments_to_delete = []
716 for message in self.browse(cr, uid, ids, context=context):
717 for attach in message.attachment_ids:
718 if attach.res_model == self._name and attach.res_id == message.id:
719 attachments_to_delete.append(attach.id)
720 if attachments_to_delete:
721 self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
722 return super(mail_message, self).unlink(cr, uid, ids, context=context)
724 def copy(self, cr, uid, id, default=None, context=None):
725 """ Overridden to avoid duplicating fields that are unique to each email """
728 default.update(message_id=False, headers=False)
729 return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
731 #------------------------------------------------------
733 #------------------------------------------------------
735 # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
736 # will be added to email send for notifications. Currently only WIP.
737 MAIL_TEMPLATE = """<div>
739 ${display_message(message)}
741 % for ctx_msg in context_messages:
742 ${display_message(ctx_msg)}
745 ${display_expandable()}
747 ${display_message(header_message)}
750 <%def name="display_message(message)">
752 Subject: ${message.subject}<br />
753 Body: ${message.body}
757 <%def name="display_expandable()">
758 <div>This is an expandable.</div>
762 def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
764 1. message.parent_id = False: new thread, no quote_context
765 2. get the lasts messages in the thread before message
766 3. get the message header
767 4. add an expandable between them
769 :param dict quote_context: options for quoting
770 :return string: html quote
772 add_expandable = False
774 message = self.browse(cr, uid, id, context=context)
775 if not message.parent_id:
777 context_ids = self.search(cr, uid, [
778 ('parent_id', '=', message.parent_id.id),
779 ('id', '<', message.id),
780 ], limit=limit, context=context)
782 if len(context_ids) >= limit:
783 add_expandable = True
784 context_ids = context_ids[0:-1]
786 context_ids.append(message.parent_id.id)
787 context_messages = self.browse(cr, uid, context_ids, context=context)
788 header_message = context_messages.pop()
793 result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
794 context_messages=context_messages,
795 header_message=header_message,
796 add_expandable=add_expandable,
797 # context kw would clash with mako internals
799 format_exceptions=True)
800 result = result.strip()
803 _logger.exception("failed to render mako template for quoting message")
807 def _notify(self, cr, uid, newid, context=None):
808 """ Add the related record followers to the destination partner_ids if is not a private message.
809 Call mail_notification.notify to manage the email sending
811 message = self.read(cr, uid, newid, ['model', 'res_id', 'author_id', 'subtype_id', 'partner_ids'], context=context)
813 partners_to_notify = set([])
814 # message has no subtype_id: pure log message -> no partners, no one notified
815 if not message.get('subtype_id'):
817 # all partner_ids of the mail.message have to be notified
818 if message.get('partner_ids'):
819 partners_to_notify |= set(message.get('partner_ids'))
820 # all followers of the mail.message document have to be added as partners and notified
821 if message.get('model') and message.get('res_id'):
822 fol_obj = self.pool.get("mail.followers")
823 fol_ids = fol_obj.search(cr, uid, [
824 ('res_model', '=', message.get('model')),
825 ('res_id', '=', message.get('res_id')),
826 ('subtype_ids', 'in', message.get('subtype_id')[0])
828 fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context)
829 partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs)
830 # remove me from notified partners, unless the message is written on my own wall
831 if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]:
832 partners_to_notify |= set([message.get('author_id')[0]])
833 elif message.get('author_id'):
834 partners_to_notify = partners_to_notify - set([message.get('author_id')[0]])
836 if partners_to_notify:
837 self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p_id) for p_id in partners_to_notify]}, context=context)
839 self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
841 #------------------------------------------------------
843 #------------------------------------------------------
845 def check_partners_email(self, cr, uid, partner_ids, context=None):
846 """ Verify that selected partner_ids have an email_address defined.
847 Otherwise throw a warning. """
848 partner_wo_email_lst = []
849 for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
850 if not partner.email:
851 partner_wo_email_lst.append(partner)
852 if not partner_wo_email_lst:
854 warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
855 for partner in partner_wo_email_lst:
856 warning_msg += '\n- %s' % (partner.name)
858 'title': _('Partners email addresses not found'),
859 'message': warning_msg,