[REF] mail: refactored tracked feature. It is now based on a dict defined at model...
[odoo/odoo.git] / addons / mail / tests / test_mail_features.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (c) 2012-TODAY OpenERP S.A. <http://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 from openerp import tools
23
24 from openerp.addons.mail.tests.test_mail_base import TestMailBase
25 from openerp.tools.mail import html_sanitize, append_content_to_html
26
27 MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
28 To: {to}
29 Received: by mail1.openerp.com (Postfix, from userid 10002)
30     id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
31 From: Sylvie Lelitre <sylvie.lelitre@agrolait.com>
32 Subject: {subject}
33 MIME-Version: 1.0
34 Content-Type: multipart/alternative;
35     boundary="----=_Part_4200734_24778174.1344608186754"
36 Date: Fri, 10 Aug 2012 14:16:26 +0000
37 Message-ID: <1198923581.41972151344608186760.JavaMail@agrolait.com>
38 {extra}
39 ------=_Part_4200734_24778174.1344608186754
40 Content-Type: text/plain; charset=utf-8
41 Content-Transfer-Encoding: quoted-printable
42
43 Please call me as soon as possible this afternoon!
44
45 --
46 Sylvie
47 ------=_Part_4200734_24778174.1344608186754
48 Content-Type: text/html; charset=utf-8
49 Content-Transfer-Encoding: quoted-printable
50
51 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
52 <html>
53  <head>=20
54   <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" />
55  </head>=20
56  <body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20
57
58   <p>Please call me as soon as possible this afternoon!</p>
59
60   <p>--<br/>
61      Sylvie
62   <p>
63  </body>
64 </html>
65 ------=_Part_4200734_24778174.1344608186754--
66 """
67
68 MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com>
69 To: {to}
70 Received: by mail1.openerp.com (Postfix, from userid 10002)
71     id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
72 From: Sylvie Lelitre <sylvie.lelitre@agrolait.com>
73 Subject: {subject}
74 MIME-Version: 1.0
75 Content-Type: text/plain
76 Date: Fri, 10 Aug 2012 14:16:26 +0000
77 Message-ID: {msg_id}
78 {extra}
79
80 Please call me as soon as possible this afternoon!
81
82 --
83 Sylvie
84 """
85
86
87 class test_mail(TestMailBase):
88
89     def _mock_send_get_mail_body(self, *args, **kwargs):
90         # def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None)
91         body = append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner', plaintext=False)
92         return body
93
94     def setUp(self):
95         super(test_mail, self).setUp()
96
97         # Mock send_get_mail_body to test its functionality without other addons override
98         self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body
99         self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
100
101     def tearDown(self):
102         # Remove mocks
103         self.registry('mail.mail').send_get_mail_body = self._send_get_mail_body
104         super(test_mail, self).tearDown()
105
106     def test_00_message_process(self):
107         """ Testing incoming emails processing. """
108         cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
109
110         # groups@.. will cause the creation of new mail groups
111         self.mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
112         self.mail_alias.create(cr, uid, {'alias_name': 'groups', 'alias_model_id': self.mail_group_model_id})
113
114         # Incoming mail creates a new mail_group "frogs"
115         self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', 'frogs')]), [])
116         mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='')
117         self.mail_thread.message_process(cr, uid, None, mail_frogs)
118         frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'frogs')])
119         self.assertTrue(len(frog_groups) == 1)
120
121         # Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
122         frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
123         group_messages = frog_group.message_ids
124         self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
125         mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='')
126         self.mail_thread.message_process(cr, uid, None, mail_frog_news)
127         frog_group.refresh()
128         self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
129
130         # Even with a wrong destination, a reply should end up in the correct thread
131         mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
132                                           extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
133         self.mail_thread.message_process(cr, uid, None, mail_reply)
134         frog_group.refresh()
135         self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
136
137         # No model passed and no matching alias must raise
138         mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
139         self.assertRaises(Exception,
140                           self.mail_thread.message_process,
141                           cr, uid, None, mail_spam)
142
143         # plain text content should be wrapped and stored as html
144         test_msg_id = '<deadcafe.1337@smtp.agrolait.com>'
145         mail_text = MAIL_TEMPLATE_PLAINTEXT.format(to='groups@example.com', subject='frogs', extra='', msg_id=test_msg_id)
146         self.mail_thread.message_process(cr, uid, None, mail_text)
147         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
148         self.assertEqual(new_mail.body, '\n<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>\n',
149                          'plaintext mail incorrectly parsed')
150
151         # Do: post a new message, with a known partner
152         test_msg_id = '<deadcafe.1337-2@smtp.agrolait.com>'
153         TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', user_raoul.email)
154         mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='extra news', extra='', msg_id=test_msg_id)
155         self.mail_thread.message_process(cr, uid, None, mail_new)
156         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
157         # Test: author_id set, not email_from
158         self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
159         self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found')
160
161         # Do: post a new message, with a unknown partner
162         test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>'
163         TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', '_abcd_')
164         mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='super news', extra='', msg_id=test_msg_id)
165         self.mail_thread.message_process(cr, uid, None, mail_new)
166         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
167         # Test: author_id set, not email_from
168         self.assertFalse(new_mail.author_id, 'message process shnould not have found a partner for _abcd_ email address')
169         self.assertIn('_abcd_', new_mail.email_from, 'message process should set en email_from when not finding a partner_id')
170
171     def test_05_thread_parent_resolution(self):
172         """Verify parent/child relationships are correctly established when processing incoming mails"""
173         cr, uid = self.cr, self.uid
174         group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
175         msg1 = group_pigs.message_post(body='My Body', subject='1')
176         msg2 = group_pigs.message_post(body='My Body', subject='2')
177         msg1, msg2 = self.mail_message.browse(cr, uid, [msg1, msg2])
178         self.assertTrue(msg1.message_id, "New message should have a proper message_id")
179
180         # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
181         # 1. In-Reply-To header
182         reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
183                                          extra='In-Reply-To: %s' % msg1.message_id)
184         self.mail_group.message_process(cr, uid, None, reply_msg)
185
186         # 2. References header
187         reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: Re: 1',
188                                          extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
189         self.mail_group.message_process(cr, uid, None, reply_msg2)
190
191         # 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, not to mail
192         reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com',
193                                           extra='', subject='Re: [%s] 1' % self.group_pigs_id)
194         self.mail_group.message_process(cr, uid, 'mail.group', reply_msg3)
195
196         group_pigs.refresh()
197         msg1.refresh()
198         self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
199         self.assertEqual(2, len(msg1.child_ids), 'msg1 should have 2 children now')
200
201     def test_10_followers_function_field(self):
202         """ Tests designed for the many2many function field 'follower_ids'.
203             We will test to perform writes using the many2many commands 0, 3, 4,
204             5 and 6. """
205         cr, uid, user_admin, partner_bert_id, group_pigs = self.cr, self.uid, self.user_admin, self.partner_bert_id, self.group_pigs
206
207         # Data: create 'disturbing' values in mail.followers: same res_id, other res_model; same res_model, other res_id
208         group_dummy_id = self.mail_group.create(cr, uid,
209             {'name': 'Dummy group'})
210         self.mail_followers.create(cr, uid,
211             {'res_model': 'mail.thread', 'res_id': self.group_pigs_id, 'partner_id': partner_bert_id})
212         self.mail_followers.create(cr, uid,
213             {'res_model': 'mail.group', 'res_id': group_dummy_id, 'partner_id': partner_bert_id})
214
215         # Pigs just created: should be only Admin as follower
216         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
217         self.assertEqual(follower_ids, set([user_admin.partner_id.id]), 'Admin should be the only Pigs fan')
218
219         # Subscribe Bert through a '4' command
220         group_pigs.write({'message_follower_ids': [(4, partner_bert_id)]})
221         group_pigs.refresh()
222         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
223         self.assertEqual(follower_ids, set([partner_bert_id, user_admin.partner_id.id]), 'Bert and Admin should be the only Pigs fans')
224
225         # Unsubscribe Bert through a '3' command
226         group_pigs.write({'message_follower_ids': [(3, partner_bert_id)]})
227         group_pigs.refresh()
228         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
229         self.assertEqual(follower_ids, set([user_admin.partner_id.id]), 'Admin should be the only Pigs fan')
230
231         # Set followers through a '6' command
232         group_pigs.write({'message_follower_ids': [(6, 0, [partner_bert_id])]})
233         group_pigs.refresh()
234         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
235         self.assertEqual(follower_ids, set([partner_bert_id]), 'Bert should be the only Pigs fan')
236
237         # Add a follower created on the fly through a '0' command
238         group_pigs.write({'message_follower_ids': [(0, 0, {'name': 'Patrick Fiori'})]})
239         partner_patrick_id = self.res_partner.search(cr, uid, [('name', '=', 'Patrick Fiori')])[0]
240         group_pigs.refresh()
241         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
242         self.assertEqual(follower_ids, set([partner_bert_id, partner_patrick_id]), 'Bert and Patrick should be the only Pigs fans')
243
244         # Finally, unlink through a '5' command
245         group_pigs.write({'message_follower_ids': [(5, 0)]})
246         group_pigs.refresh()
247         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
248         self.assertFalse(follower_ids, 'Pigs group should not have fans anymore')
249
250         # Test dummy data has not been altered
251         fol_obj_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.thread'), ('res_id', '=', self.group_pigs_id)])
252         follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)])
253         self.assertEqual(follower_ids, set([partner_bert_id]), 'Bert should be the follower of dummy mail.thread data')
254         fol_obj_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', group_dummy_id)])
255         follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)])
256         self.assertEqual(follower_ids, set([partner_bert_id, user_admin.partner_id.id]), 'Bert and Admin should be the followers of dummy mail.group data')
257
258     def test_11_message_followers_and_subtypes(self):
259         """ Tests designed for the subscriber API as well as message subtypes """
260         cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
261         # Data: message subtypes
262         self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.group'})
263         self.mail_message_subtype.create(cr, uid, {'name': 'mt_other_def', 'default': True, 'res_model': 'crm.lead'})
264         self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_def', 'default': True, 'res_model': False})
265         mt_mg_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.group'})
266         mt_all_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_nodef', 'default': False, 'res_model': False})
267         default_group_subtypes = self.mail_message_subtype.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', 'mail.group'), ('res_model', '=', False)])
268
269         # ----------------------------------------
270         # CASE1: test subscriptions with subtypes
271         # ----------------------------------------
272
273         # Do: Subscribe Raoul three times (niak niak) through message_subscribe_users
274         group_pigs.message_subscribe_users([user_raoul.id, user_raoul.id])
275         group_pigs.message_subscribe_users([user_raoul.id])
276         group_pigs.refresh()
277         # Test: 2 followers (Admin and Raoul)
278         follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
279         self.assertEqual(set(follower_ids), set([user_raoul.partner_id.id, user_admin.partner_id.id]), 'Admin and Raoul should be the only 2 Pigs fans')
280         # Test: Raoul follows default subtypes
281         fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_raoul.partner_id.id)])
282         fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
283         fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
284         self.assertEqual(set(fol_subtype_ids), set(default_group_subtypes), 'subscription subtypes are incorrect')
285
286         # Do: Unsubscribe Raoul twice through message_unsubscribe_users
287         group_pigs.message_unsubscribe_users([user_raoul.id, user_raoul.id])
288         group_pigs.refresh()
289         # Test: 1 follower (Admin)
290         follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
291         self.assertEqual(follower_ids, [user_admin.partner_id.id], 'Admin must be the only Pigs fan')
292
293         # Do: subscribe Admin with subtype_ids
294         group_pigs.message_subscribe_users([uid], [mt_mg_nodef, mt_all_nodef])
295         fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_admin.partner_id.id)])
296         fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
297         fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
298         self.assertEqual(set(fol_subtype_ids), set([mt_mg_nodef, mt_all_nodef]), 'subscription subtypes are incorrect')
299
300         # ----------------------------------------
301         # CASE2: test mail_thread fields
302         # ----------------------------------------
303
304         subtype_data = group_pigs._get_subscription_data(None, None)[group_pigs.id]['message_subtype_data']
305         self.assertEqual(set(subtype_data.keys()), set(['Discussions', 'mt_mg_def', 'mt_all_def', 'mt_mg_nodef', 'mt_all_nodef']), 'mail.group available subtypes incorrect')
306         self.assertFalse(subtype_data['Discussions']['followed'], 'Admin should not follow Discussions in pigs')
307         self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
308         self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
309
310     def test_20_message_quote_context(self):
311         """ Tests designed for message_post. """
312         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
313
314         msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'})
315         msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id})
316         msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
317         msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
318         msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id})
319
320         result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3)
321         self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.')
322         self.assertIn('Second answer', result, 'Answer should be in quote.')
323         self.assertIn('Third answer', result, 'Answer should be in quote.')
324         self.assertIn('expandable', result, 'Expandable should be present.')
325         self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
326         self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
327
328     def test_21_message_post(self):
329         """ Tests designed for message_post. """
330         cr, uid, user_raoul, group_pigs = self.cr, self.uid, self.user_raoul, self.group_pigs
331         self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
332         # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
333         p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
334         # 2 - Carine Poilvache, with email, should never receive emails
335         p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
336         # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
337         p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
338
339         # Subscribe Raoul, #1, #2
340         group_pigs.message_subscribe([self.partner_raoul_id, p_b_id, p_c_id])
341
342         # Mail data
343         _subject = 'Pigs'
344         _mail_subject = '%s posted on %s' % (user_raoul.name, group_pigs.name)
345         _body1 = 'Pigs rules'
346         _mail_body1 = 'Pigs rules\n<div><p>Raoul</p></div>\n'
347         _mail_bodyalt1 = 'Pigs rules\nRaoul\n'
348         _body2 = '<html>Pigs rules</html>'
349         _mail_body2 = html_sanitize('<html>Pigs rules\n<div><p>Raoul</p></div>\n</html>')
350         _mail_bodyalt2 = 'Pigs rules\nRaoul'
351         _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
352
353         # ----------------------------------------
354         # CASE1: post comment, body and subject specified
355         # ----------------------------------------
356
357         # 1. Post a new comment on Pigs
358         self._init_mock_build_email()
359         msg1_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body1, subject=_subject, type='comment', subtype='mt_comment')
360         message1 = self.mail_message.browse(cr, uid, msg1_id)
361         sent_emails = self._build_email_kwargs_list
362         # Test: mail.mail notifications have been deleted
363         self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg1_id)]), 'mail.mail notifications should have been auto-deleted!')
364         # Test: mail_message: subject is _subject, body is _body1 (no formatting done)
365         self.assertEqual(message1.subject, _subject, 'mail.message subject incorrect')
366         self.assertEqual(message1.body, _body1, 'mail.message body incorrect')
367         # Test: sent_email: email send by server: correct subject, body, body_alternative
368         self.assertEqual(len(sent_emails), 2, 'sent_email number of sent emails incorrect')
369         for sent_email in sent_emails:
370             self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
371             self.assertTrue(sent_email['body'] in [_mail_body1 + '\nBert Tartopoils\n', _mail_body1 + '\nAdministrator\n'],
372                 'sent_email body incorrect')
373             # the html2plaintext uses etree or beautiful soup, so the result may be slighly different
374             # depending if you have installed beautiful soup.
375             self.assertTrue(sent_email['body_alternative'] in [_mail_bodyalt1 + '\nBert Tartopoils\n', _mail_bodyalt1 + '\nAdministrator\n'],
376                 'sent_email body_alternative is incorrect')
377         # Test: mail_message: notified_partner_ids = group followers
378         message_pids = set([partner.id for partner in message1.notified_partner_ids])
379         test_pids = set([self.partner_admin_id, p_b_id, p_c_id])
380         self.assertEqual(test_pids, message_pids, 'mail.message notified partners incorrect')
381         # Test: notification linked to this message = group followers = notified_partner_ids
382         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', msg1_id)])
383         notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
384         self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
385         # Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
386         for sent_email in sent_emails:
387             self.assertTrue(set(sent_email['email_to']).issubset(set(['a@a', 'b@b'])), 'sent_email email_to is incorrect')
388
389         # ----------------------------------------
390         # CASE2: post an email with attachments, parent_id, partner_ids, parent notification
391         # ----------------------------------------
392
393         # 1. Post a new email comment on Pigs
394         self._init_mock_build_email()
395         msg2_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
396             partner_ids=[(6, 0, [p_d_id])], parent_id=msg1_id, attachments=_attachments)
397         message2 = self.mail_message.browse(cr, uid, msg2_id)
398         sent_emails = self._build_email_kwargs_list
399         self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg2_id)]), 'mail.mail notifications should have been auto-deleted!')
400         # Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
401         self.assertEqual(message2.subject, False, 'mail.message subject incorrect')
402         self.assertEqual(message2.body, html_sanitize(_body2), 'mail.message body incorrect')
403         self.assertEqual(message2.parent_id.id, msg1_id, 'mail.message parent_id incorrect')
404         # Test: sent_email: email send by server: correct automatic subject, body, body_alternative
405         self.assertEqual(len(sent_emails), 3, 'sent_email number of sent emails incorrect')
406         for sent_email in sent_emails:
407             self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
408             self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect')
409             self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
410         # Test: mail_message: notified_partner_ids = group followers
411         message_pids = set([partner.id for partner in message2.notified_partner_ids])
412         test_pids = set([self.partner_admin_id, p_b_id, p_c_id, p_d_id])
413         self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
414         # Test: notifications linked to this message = group followers = notified_partner_ids
415         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', msg2_id)])
416         notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
417         self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
418         # Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
419         for sent_email in sent_emails:
420             self.assertTrue(set(sent_email['email_to']).issubset(set(['a@a', 'b@b', 'c@c'])), 'sent_email email_to incorrect')
421         # Test: attachments
422         for attach in message2.attachment_ids:
423             self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
424             self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
425             self.assertIn((attach.name, attach.datas.decode('base64')), _attachments,
426                 'mail.message attachment name / data incorrect')
427         # Test: download attachments
428         for attach in message2.attachment_ids:
429             dl_attach = self.mail_message.download_attachment(cr, user_raoul.id, id_message=message2.id, attachment_id=attach.id)
430             self.assertIn((dl_attach['filename'], dl_attach['base64'].decode('base64')), _attachments, 'mail.message download_attachment is incorrect')
431
432         # 2. Dédé has been notified -> should also have been notified of the parent message
433         message1.refresh()
434         message_pids = set([partner.id for partner in message1.notified_partner_ids])
435         test_pids = set([self.partner_admin_id, p_b_id, p_c_id, p_d_id])
436         self.assertEqual(test_pids, message_pids, 'mail.message parent notification not created')
437
438         # 3. Reply to the last message, check that its parent will be the first message
439         msg3_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body='Test', parent_id=msg2_id)
440         message = self.mail_message.browse(cr, uid, msg3_id)
441         self.assertEqual(message.parent_id.id, msg1_id, 'message_post did not flatten the thread structure')
442
443     def test_25_message_compose_wizard(self):
444         """ Tests designed for the mail.compose.message wizard. """
445         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
446         mail_compose = self.registry('mail.compose.message')
447         self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
448         group_bird_id = self.mail_group.create(cr, uid, {'name': 'Bird', 'description': 'Bird resistance'})
449         group_bird = self.mail_group.browse(cr, uid, group_bird_id)
450
451         # Mail data
452         _subject = 'Pigs'
453         _body = 'Pigs <b>rule</b>'
454         _reply_subject = 'Re: Pigs'
455         _attachments = [
456             {'name': 'First', 'datas_fname': 'first.txt', 'datas': 'My first attachment'.encode('base64')},
457             {'name': 'Second', 'datas_fname': 'second.txt', 'datas': 'My second attachment'.encode('base64')}
458             ]
459         _attachments_test = [('first.txt', 'My first attachment'), ('second.txt', 'My second attachment')]
460
461         # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
462         p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
463         # 2 - Carine Poilvache, with email, should never receive emails
464         p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
465         # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
466         p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
467
468         # Subscribe #1
469         group_pigs.message_subscribe([p_b_id])
470
471         # ----------------------------------------
472         # CASE1: comment on group_pigs
473         # ----------------------------------------
474
475         # 1. Comment group_pigs with body_text and subject
476         compose_id = mail_compose.create(cr, uid,
477             {'subject': _subject, 'body': _body, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
478             {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id,
479              'default_content_subtype': 'plaintext'})
480         compose = mail_compose.browse(cr, uid, compose_id)
481         # Test: mail.compose.message: composition_mode, model, res_id
482         self.assertEqual(compose.composition_mode,  'comment', 'mail.compose.message incorrect composition_mode')
483         self.assertEqual(compose.model,  'mail.group', 'mail.compose.message incorrect model')
484         self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
485
486         # 2. Post the comment, get created message
487         mail_compose.send_mail(cr, uid, [compose_id])
488         group_pigs.refresh()
489         message = group_pigs.message_ids[0]
490         # Test: mail.message: subject, body inside pre
491         self.assertEqual(message.subject,  _subject, 'mail.message incorrect subject')
492         self.assertEqual(message.body, _body, 'mail.message incorrect body')
493         # Test: mail.message: notified_partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
494         msg_pids = [partner.id for partner in message.notified_partner_ids]
495         test_pids = [p_b_id, p_c_id, p_d_id]
496         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
497         self.assertEqual(len(notif_ids), 3, 'mail.message: too much notifications created')
498         self.assertEqual(set(msg_pids), set(test_pids), 'mail.message notified_partner_ids incorrect')
499
500         # ----------------------------------------
501         # CASE2: reply to last comment with attachments
502         # ----------------------------------------
503
504         # 1. Update last comment subject, reply with attachments
505         message.write({'subject': _subject})
506         compose_id = mail_compose.create(cr, uid,
507             {'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]},
508             {'default_composition_mode': 'reply', 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id})
509         compose = mail_compose.browse(cr, uid, compose_id)
510         # Test: model, res_id, parent_id
511         self.assertEqual(compose.model,  'mail.group', 'mail.compose.message incorrect model')
512         self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
513         self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
514         # Test: mail.message: subject as Re:.., body in html, parent_id
515         self.assertEqual(compose.subject, _reply_subject, 'mail.message incorrect subject')
516         # self.assertIn('Administrator wrote:<blockquote><pre>Pigs rules</pre></blockquote>', compose.body, 'mail.message body is incorrect')
517         self.assertEqual(compose.parent_id and compose.parent_id.id, message.id, 'mail.message parent_id incorrect')
518         # Test: mail.message: attachments
519         for attach in compose.attachment_ids:
520             self.assertIn((attach.datas_fname, attach.datas.decode('base64')), _attachments_test, 'mail.message attachment name / data incorrect')
521
522         # ----------------------------------------
523         # CASE3: mass_mail on Pigs and Bird
524         # ----------------------------------------
525
526         # 1. mass_mail on pigs and bird
527         compose_id = mail_compose.create(cr, uid,
528             {'subject': _subject, 'body': '${object.description}'},
529             {'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': False,
530                 'active_ids': [self.group_pigs_id, group_bird_id]})
531         compose = mail_compose.browse(cr, uid, compose_id)
532
533         # 2. Post the comment, get created message for each group
534         mail_compose.send_mail(cr, uid, [compose_id],
535             context={'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id]})
536         group_pigs.refresh()
537         group_bird.refresh()
538         message1 = group_pigs.message_ids[0]
539         message2 = group_bird.message_ids[0]
540         # Test: Pigs and Bird did receive their message
541         test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
542         self.assertIn(message1.id, test_msg_ids, 'Pigs did not receive its mass mailing message')
543         self.assertIn(message2.id, test_msg_ids, 'Bird did not receive its mass mailing message')
544         # Test: mail.message: subject, body
545         self.assertEqual(message1.subject, _subject, 'mail.message subject incorrect')
546         self.assertEqual(message1.body, group_pigs.description, 'mail.message body incorrect')
547         self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
548         self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
549
550     def test_30_needaction(self):
551         """ Tests for mail.message needaction. """
552         cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
553         group_pigs_demo = self.mail_group.browse(cr, self.user_raoul_id, self.group_pigs_id)
554         na_admin_base = self.mail_message._needaction_count(cr, uid, domain=[])
555         na_demo_base = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
556
557         # Test: number of unread notification = needaction on mail.message
558         notif_ids = self.mail_notification.search(cr, uid, [
559             ('partner_id', '=', user_admin.partner_id.id),
560             ('read', '=', False)
561             ])
562         na_count = self.mail_message._needaction_count(cr, uid, domain=[])
563         self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
564
565         # Do: post 2 message on group_pigs as admin, 3 messages as demo user
566         for dummy in range(2):
567             group_pigs.message_post(body='My Body', subtype='mt_comment')
568         for dummy in range(3):
569             group_pigs_demo.message_post(body='My Demo Body', subtype='mt_comment')
570
571         # Test: admin has 3 new notifications (from demo), and 3 new needaction
572         notif_ids = self.mail_notification.search(cr, uid, [
573             ('partner_id', '=', user_admin.partner_id.id),
574             ('read', '=', False)
575             ])
576         self.assertEqual(len(notif_ids), na_admin_base + 3, 'Admin should have 3 new unread notifications')
577         na_admin = self.mail_message._needaction_count(cr, uid, domain=[])
578         na_admin_group = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
579         self.assertEqual(na_admin, na_admin_base + 3, 'Admin should have 3 new needaction')
580         self.assertEqual(na_admin_group, 3, 'Admin should have 3 needaction related to Pigs')
581         # Test: demo has 0 new notifications (not a follower, not receiving its own messages), and 0 new needaction
582         notif_ids = self.mail_notification.search(cr, uid, [
583             ('partner_id', '=', user_raoul.partner_id.id),
584             ('read', '=', False)
585             ])
586         self.assertEqual(len(notif_ids), na_demo_base + 0, 'Demo should have 0 new unread notifications')
587         na_demo = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
588         na_demo_group = self.mail_message._needaction_count(cr, user_raoul.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
589         self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction')
590         self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs')
591
592     def test_40_track_field(self):
593         """ Testing auto tracking of fields. """
594         def _strip_string_spaces(body):
595             return body.replace(' ', '').replace('\n', '')
596
597         cr, uid = self.cr, self.uid
598
599         # Data: subscribe Raoul to Pigs, because he will change the public attribute and may loose access to the record
600         self.mail_group.message_subscribe_users(cr, uid, [self.group_pigs_id], [self.user_raoul_id])
601
602         # Data: res.users.group, to test group_public_id automatic logging
603         group_system_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_system')
604         group_system_id = group_system_ref and group_system_ref[1] or False
605
606         # Data: custom subtypes
607         mt_private_id = self.mail_message_subtype.create(cr, uid, {'name': 'private', 'description': 'Private public'})
608         self.ir_model_data.create(cr, uid, {'name': 'mt_private', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_private_id})
609         mt_name_supername_id = self.mail_message_subtype.create(cr, uid, {'name': 'name_supername', 'description': 'Supername name'})
610         self.ir_model_data.create(cr, uid, {'name': 'mt_name_supername', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_name_supername_id})
611         mt_group_public_id = self.mail_message_subtype.create(cr, uid, {'name': 'group_public', 'description': 'Group changed'})
612         self.ir_model_data.create(cr, uid, {'name': 'mt_group_public', 'model': 'mail.group', 'module': 'mail', 'res_id': mt_group_public_id})
613
614         # Data: alter mail_group model for testing purposes (test on classic, selection and many2one fields)
615         self.mail_group._track = {
616             'public': {
617                 'mail.mt_private': lambda self, cr, uid, obj, ctx=None: obj.public == 'private',
618             },
619             'name': {
620                 'mail.mt_name_supername': lambda self, cr, uid, obj, ctx=None: obj.name == 'supername',
621             },
622             'group_public_id': {
623                 'mail.mt_group_public': lambda self, cr, uid, obj, ctx=None: True,
624             },
625         }
626         public_col = self.mail_group._columns.get('public')
627         name_col = self.mail_group._columns.get('name')
628         group_public_col = self.mail_group._columns.get('group_public_id')
629         public_col._track_visibility = 1
630         name_col._track_visibility = 2
631         group_public_col._track_visibility = 1
632
633         # Test: change name -> always tracked, not related to a subtype
634         self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public'})
635         self.group_pigs.refresh()
636         self.assertEqual(len(self.group_pigs.message_ids), 1, 'tracked: a message should have been produced')
637         # Test: first produced message: no subtype, name change tracked
638         last_msg = self.group_pigs.message_ids[-1]
639         self.assertFalse(last_msg.subtype_id, 'tracked: message should not have been linked to a subtype')
640         self.assertIn('SelectedGroupOnly-&gt;Public', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
641         self.assertIn('Pigs', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
642
643         # Test: change name as supername, public as private -> 2 subtypes
644         self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'name': 'supername', 'public': 'private'})
645         self.group_pigs.refresh()
646         self.assertEqual(len(self.group_pigs.message_ids), 3, 'tracked: two messages should have been produced')
647         # Test: first produced message: mt_name_supername
648         last_msg = self.group_pigs.message_ids[-2]
649         self.assertEqual(last_msg.subtype_id.id, mt_private_id, 'tracked: message should be linked to mt_private subtype')
650         self.assertIn('Private public', last_msg.body, 'tracked: message body does not hold the subtype description')
651         self.assertIn('Pigs-&gt;supername', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
652         # Test: second produced message: mt_name_supername
653         last_msg = self.group_pigs.message_ids[-3]
654         self.assertEqual(last_msg.subtype_id.id, mt_name_supername_id, 'tracked: message should be linked to mt_name_supername subtype')
655         self.assertIn('Supername name', last_msg.body, 'tracked: message body does not hold the subtype description')
656         self.assertIn('Public-&gt;Private', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect')
657         self.assertIn('Pigs-&gt;supername', _strip_string_spaces(last_msg.body), 'tracked feature: message body does not hold always tracked field')
658
659         # Test: change public as public, group_public_id -> 1 subtype, name always tracked
660         self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public', 'group_public_id': group_system_id})
661         self.group_pigs.refresh()
662         self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: one message should have been produced')
663         # Test: first produced message: mt_group_public_id, with name always tracked, public tracked on change
664         last_msg = self.group_pigs.message_ids[-4]
665         self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should not be linked to any subtype')
666         self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description')
667         self.assertIn('Private-&gt;Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field')
668         self.assertIn('HumanResources/Employee-&gt;Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field')
669
670         # Test: change not tracked field, no tracking message
671         self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'description': 'Dummy'})
672         self.group_pigs.refresh()
673         self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: No message should have been produced')
674
675         # Data: removed changes
676         public_col._track_visibility = False
677         name_col._track_visibility = False
678         group_public_col._track_visibility = False
679         self.mail_group._track = {}