[IMP]if subtype not selected should not b show in wall
[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 from email.header import decode_header
24 from osv import osv, fields
25 import tools
26
27 _logger = logging.getLogger(__name__)
28
29 """ Some tools for parsing / creating email fields """
30 def decode(text):
31     """Returns unicode() string conversion of the the given encoded smtp header text"""
32     if text:
33         text = decode_header(text.replace('\r', ''))
34         return ''.join([tools.ustr(x[0], x[1]) for x in text])
35
36 class mail_message(osv.Model):
37     """Model holding messages: system notification (replacing res.log
38     notifications), comments (for OpenChatter feature). This model also
39     provides facilities to parse new email messages. Type of messages are
40     differentiated using the 'type' column. """
41
42     _name = 'mail.message'
43     _description = 'Message'
44     _inherit = ['ir.needaction_mixin']
45     _order = 'id desc'
46
47     _message_read_limit = 10
48     _message_record_name_length = 18
49
50     def _shorten_name(self, name):
51         if len(name) <= (self._message_record_name_length+3):
52             return name
53         return name[:self._message_record_name_length] + '...'
54
55     def _get_record_name(self, cr, uid, ids, name, arg, context=None):
56         """ Return the related document name, using get_name. """
57         result = dict.fromkeys(ids, '')
58         for message in self.browse(cr, uid, ids, context=context):
59             if not message.model or not message.res_id:
60                 continue
61             result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
62         return result
63
64     def _get_unread(self, cr, uid, ids, name, arg, context=None):
65         """ Compute if the message is unread by the current user. """
66         res = dict((id, {'unread': False}) for id in ids)
67         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
68         notif_obj = self.pool.get('mail.notification')
69         notif_ids = notif_obj.search(cr, uid, [
70             ('partner_id', 'in', [partner_id]),
71             ('message_id', 'in', ids),
72             ('read', '=', False)
73         ], context=context)
74         for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
75             res[notif.message_id.id]['unread'] = True
76         return res
77
78     def _search_unread(self, cr, uid, obj, name, domain, context=None):
79         """ Search for messages unread by the current user. """
80         read_value = not domain[0][2]
81         read_cond = '' if read_value else '!= true'
82         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
83         cr.execute("""  SELECT mail_message.id \
84                         FROM mail_message \
85                         JOIN mail_notification ON ( \
86                             mail_notification.message_id = mail_message.id ) \
87                         WHERE mail_notification.partner_id = %%s AND \
88                             mail_notification.read %s \
89                     """ % read_cond, (partner_id,) )
90         res = cr.fetchall()
91         message_ids = [r[0] for r in res]
92         return [('id', 'in', message_ids)]
93
94
95     def name_get(self, cr, uid, ids, context=None):
96         # name_get may receive int id instead of an id list
97         if isinstance(ids, (int, long)):
98             ids = [ids]
99         res = []
100         for message in self.browse(cr, uid, ids, context=context):
101             name = '%s: %s' % (message.subject or '', message.body or '')
102             res.append((message.id, self._shorten_name(name.lstrip(' :'))))
103         return res
104
105     _columns = {
106         # should we keep a distinction between email and comment ?
107         'type': fields.selection([
108                         ('email', 'email'),
109                         ('comment', 'Comment'),
110                         ('notification', 'System notification'),
111                         ], 'Type',
112             help="Message type: email for email message, notification for system "\
113                  "message, comment for other messages such as user replies"),
114         'author_id': fields.many2one('res.partner', 'Author', required=True),
115         'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
116         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
117             'message_id', 'attachment_id', 'Attachments'),
118         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
119         'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
120         'model': fields.char('Related Document Model', size=128, select=1),
121         'res_id': fields.integer('Related Document ID', select=1),
122         'record_name': fields.function(_get_record_name, type='string',
123             string='Message Record Name',
124             help="Name get of the related document."),
125         'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
126         'subject': fields.char('Subject'),
127         'date': fields.datetime('Date'),
128         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
129         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
130         'unread': fields.function(_get_unread, fnct_search=_search_unread,
131             type='boolean', string='Unread',
132             help='Functional field to search for unread messages linked to uid'),
133         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
134     }
135
136     def _needaction_domain_get(self, cr, uid, context=None):
137         if self._needaction:
138             return [('unread', '=', True)]
139         return []
140
141     def _get_default_author(self, cr, uid, context=None):
142         return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
143
144     _defaults = {
145         'type': 'email',
146         'date': lambda *a: fields.datetime.now(),
147         'author_id': _get_default_author
148     }
149
150
151     #------------------------------------------------------
152     # Message loading for web interface
153     #------------------------------------------------------
154
155     def _message_dict_get(self, cr, uid, msg, context=None):
156         """ Return a dict representation of the message browse record. """
157         attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
158         author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
159         author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
160         partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
161         return {
162             'id': msg.id,
163             'type': msg.type,
164             'attachment_ids': attachment_ids,
165             'body': msg.body,
166             'model': msg.model,
167             'res_id': msg.res_id,
168             'record_name': msg.record_name,
169             'subject': msg.subject,
170             'date': msg.date,
171             'author_id': author_id,
172             'author_user_id': author_user_id,
173             'partner_ids': partner_ids,
174             'child_ids': [],
175         }
176
177     def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
178         """ Given a tree with several roots of following structure :
179             [
180                 {'id': 1, 'child_ids':[
181                     {'id': 11, 'child_ids': [...] },
182                 ] },
183                 {...}
184             ]
185             Flatten it to have a maximum number of level, with 0 being
186             completely flat.
187             Perform the flattening at leafs if above the maximum depth, then get
188             back in the tree.
189         """
190         def _flatten(msg_dict):
191             """ from    {'id': x, 'child_ids': [{child1}, {child2}]}
192                 get     [{'id': x, 'child_ids': []}, {child1}, {child2}]
193             """
194             child_ids = msg_dict.pop('child_ids', [])
195             msg_dict['child_ids'] = []
196             return [msg_dict] + child_ids
197         # Depth-first flattening
198         for message in messages:
199             message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level+1, level, context=context)
200         # Flatten if above maximum depth
201         if current_level < level:
202             return messages
203         new_list = []
204         for x in range(0, len(messages)):
205             flatenned = _flatten(messages[x])
206             for flat in flatenned:
207                 new_list.append(flat)
208         messages = new_list
209         return messages
210
211     def _debug_print_tree(self, tree, prefix=''):
212         for elem in tree:
213             print '%s%s (%s childs: %s)' % (prefix, elem['id'], len(elem['child_ids']), [xelem['id'] for xelem in elem['child_ids']])
214             if elem['child_ids']:
215                 self._debug_print_tree(elem['child_ids'], prefix+'--')
216
217     def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
218         """ 
219             If IDS are provided, fetch these records, otherwise use the domain to
220             fetch the matching records. After having fetched the records provided
221             by IDS, it will fetch children (according to thread_level).
222             
223             Return [
224             
225             ]
226         """
227         limit = limit or self._message_read_limit
228         context = context or {}
229         if ids is False:
230             ids = self.search(cr, uid, domain, context=context, limit=limit)
231
232         # FP Todo: flatten to max X level of mail_thread
233         messages = self.browse(cr, uid, ids, context=context)
234
235         result = []
236         tree = {} # key: ID, value: record
237         for msg in messages:
238             if len(result)<(limit-1):
239                 record = self._message_dict_get(cr, uid, msg, context=context)
240                 if thread_level and msg.parent_id:
241                     while msg.parent_id:
242                         if msg.parent_id.id in tree:
243                             record_parent = tree[msg.parent_id.id]
244                         else:
245                             record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
246                             if msg.parent_id.parent_id:
247                                 tree[msg.parent_id.id] = record_parent
248                         if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
249                             record_parent['child_ids'].append(record)
250                         record = record_parent
251                         msg = msg.parent_id
252                 if msg.id not in tree:
253                     result.append(record)
254                     tree[msg.id] = record
255             else:
256                 result.append({
257                     'type': 'expandable',
258                     'domain': [('id','<=', msg.id)]+domain,
259                     'context': context,
260                     'thread_level': thread_level  # should be improve accodting to level of records
261                 })
262                 break
263
264         # Flatten the result
265 #        if thread_level > 0:
266 #            result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
267
268         return result
269
270
271     #------------------------------------------------------
272     # Email api
273     #------------------------------------------------------
274
275     def init(self, cr):
276         cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
277         if not cr.fetchone():
278             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
279
280     def check(self, cr, uid, ids, mode, context=None):
281         """
282         You can read/write a message if:
283           - you received it (a notification exists) or
284           - you can read the related document (res_model, res_id)
285         If a message is not attached to a document, normal access rights on
286         the mail.message object apply.
287         """
288         if not ids:
289             return
290         if isinstance(ids, (int, long)):
291             ids = [ids]
292
293         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
294
295         # check messages for which you have a notification
296         not_obj = self.pool.get('mail.notification')
297         not_ids = not_obj.search(cr, uid, [
298             ('partner_id', '=', partner_id),
299             ('message_id', 'in', ids),
300         ], context=context)
301         for notification in not_obj.browse(cr, uid, not_ids, context=context):
302             if notification.message_id.id in ids:
303                 pass
304                 # FP Note: we should put this again !!!
305                 #ids.remove(notification.message_id.id)
306
307         # check messages according to related documents
308         res_ids = {}
309         cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
310         for rmod, rid in cr.fetchall():
311             if not (rmod and rid):
312                 continue
313             res_ids.setdefault(rmod,set()).add(rid)
314
315         ima_obj = self.pool.get('ir.model.access')
316         for model, mids in res_ids.items():
317             mids = self.pool.get(model).exists(cr, uid, mids)
318             ima_obj.check(cr, uid, model, mode)
319             self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
320
321     def create(self, cr, uid, values, context=None):
322         if not values.get('message_id') and values.get('res_id') and values.get('model'):
323             values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s'% values)
324         newid = super(mail_message, self).create(cr, uid, values, context)
325         self.check(cr, uid, [newid], mode='create', context=context)
326         self.notify(cr, uid, newid, context=context)
327         return newid
328
329     def notify(self, cr, uid, newid, context=None):
330         """ Add the related record followers to the destination partner_ids.
331             Call mail_notification.notify to manage the email sending
332         """
333         followers_obj = self.pool.get("mail.followers")
334         message = self.browse(cr, uid, newid, context=context)
335         partners_to_notify = set([])
336         # add all partner_ids of the message
337         if message.partner_ids:
338             partners_to_notify |= set(partner.id for partner in message.partner_ids)
339         # add all followers and set add them in partner_ids
340         if message.model and message.res_id:
341             record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context)
342             extra_notified = set(partner.id for partner in record.message_follower_ids)
343             missing_notified = extra_notified - partners_to_notify
344             missing_follow_ids = []
345             if message.subtype_id:
346                 for p_id in missing_notified:
347                     follow_ids = followers_obj.search(cr, uid, [('partner_id','=',p_id),('subtype_ids','in',[message.subtype_id.id]),('res_model','=',message.model),('res_id','=',message.res_id)])
348                     if follow_ids and len(follow_ids):
349                         missing_follow_ids.append(p_id)
350             message.write({'partner_ids': [(4, p_id) for p_id in missing_follow_ids]})
351             partners_to_notify |= extra_notified
352         self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
353
354     def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
355         self.check(cr, uid, ids, 'read', context=context)
356         return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
357
358     def copy(self, cr, uid, id, default=None, context=None):
359         """Overridden to avoid duplicating fields that are unique to each email"""
360         if default is None:
361             default = {}
362         self.check(cr, uid, [id], 'read', context=context)
363         default.update(message_id=False, headers=False)
364         return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
365
366     def write(self, cr, uid, ids, vals, context=None):
367         result = super(mail_message, self).write(cr, uid, ids, vals, context)
368         self.check(cr, uid, ids, 'write', context=context)
369         return result
370
371     def unlink(self, cr, uid, ids, context=None):
372         self.check(cr, uid, ids, 'unlink', context=context)
373         return super(mail_message, self).unlink(cr, uid, ids, context)
374
375
376 class mail_notification(osv.Model):
377     """ mail_notification is a relational table modeling messages pushed to partners.
378     """
379     _inherit = 'mail.notification'
380     _columns = {
381         'message_id': fields.many2one('mail.message', string='Message',
382                         ondelete='cascade', required=True, select=1),
383     }
384
385     def set_message_read(self, cr, uid, msg_id, context=None):
386         partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
387         notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
388         return self.write(cr, uid, notif_ids, {'read': True}, context=context)
389