From ed3518d858392cbdd7fd4780c9985fc27ac0c77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Sep 2012 12:17:04 +0200 Subject: [PATCH] [CLEAN] Cleaned subtype model, implementation and tests. Added a message_subtype_data field, holding data about subtypes. This will allow to avoid doing too much calls from the front-end. bzr revid: tde@openerp.com-20120920101704-cri6zq7sqi7y660b --- addons/mail/mail_followers.py | 6 +- addons/mail/mail_message.py | 27 +++--- addons/mail/mail_thread.py | 99 +++++++++++++++------- addons/mail/tests/test_mail.py | 89 ++++++++++--------- addons/mail/wizard/mail_compose_message.py | 2 +- 5 files changed, 135 insertions(+), 88 deletions(-) diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 6b1f2d302bc..e77e4a3d4da 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -45,10 +45,8 @@ class mail_followers(osv.Model): help='Id of the followed resource'), 'partner_id': fields.many2one('res.partner', string='Related Partner', ondelete='cascade', required=True, select=1), - 'subtype_ids': fields.many2many('mail.message.subtype', - 'mail_message_subtyp_rel', - 'subscription_id', 'subtype_id', 'Subtype', - help = "linking some subscription to several subtype for projet/task"), + 'subtype_ids': fields.many2many('mail.message.subtype', string='Subtype', + help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."), } diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 65ee7f41d77..793345ffb32 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -354,27 +354,24 @@ class mail_message(osv.Model): """ Add the related record followers to the destination partner_ids. Call mail_notification.notify to manage the email sending """ - followers_obj = self.pool.get("mail.followers") message = self.browse(cr, uid, newid, context=context) partners_to_notify = set([]) - # add all partner_ids of the message + # message has no subtype_id: pure log message -> no partners, no one notified + if not message.subtype_id: + message.write({'partner_ids': [5]}) + return True + # all partner_ids of the mail.message have to be notified if message.partner_ids: partners_to_notify |= set(partner.id for partner in message.partner_ids) - # add all followers and set add them in partner_ids + # all followers of the mail.message document have to be added as partners and notified if message.model and message.res_id: - record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context) - extra_notified = set(partner.id for partner in record.message_follower_ids) + fol_obj = self.pool.get("mail.followers") + fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context) + fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context) + extra_notified = set(fol.partner_id.id for fol in fol_objs) missing_notified = extra_notified - partners_to_notify - missing_follow_ids = [] - if message.subtype_id: - for p_id in missing_notified: - 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)], context=context) - if follow_ids and len(follow_ids): - missing_follow_ids.append(p_id) - subtype_record = self.pool.get('mail.message.subtype').browse(cr, uid, message.subtype_id.id,context=context) - if not subtype_record.res_model: - missing_follow_ids.append(p_id) - message.write({'partner_ids': [(4, p_id) for p_id in missing_follow_ids]}) + if missing_notified: + message.write({'partner_ids': [(4, p_id) for p_id in missing_notified]}) partners_to_notify |= extra_notified self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index a3cd727aeb6..39a8e011f3c 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -95,6 +95,7 @@ class many2many_reference(fields.many2many): else: return super(many2many_reference, self).set(cr, model, id, name, values, user, context) + class mail_thread(osv.AbstractModel): ''' mail_thread model is meant to be inherited by any model that needs to act as a discussion topic on which messages can be attached. Public @@ -117,8 +118,10 @@ class mail_thread(osv.AbstractModel): _description = 'Email Thread' def _get_message_data(self, cr, uid, ids, name, args, context=None): + """ Computes: + - message_unread: has uid unread message for the document + - message_summary: html snippet summarizing the Chatter for kanban views """ res = dict((id, dict(message_unread=False, message_summary='')) for id in ids) - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) notif_obj = self.pool.get('mail.notification') notif_ids = notif_obj.search(cr, uid, [ @@ -133,7 +136,35 @@ class mail_thread(osv.AbstractModel): for thread in self.browse(cr, uid, ids, context=context): cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or '' res[thread.id]['message_summary'] = "9 %d + %d" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids)) - res[thread.id]['message_is_follower'] = user.partner_id.id in [follower.id for follower in thread.message_follower_ids] + return res + + def _get_subscription_data(self, cr, uid, ids, name, args, context=None): + """ Computes: + - message_is_follower: is uid in the document followers + - message_subtype_data: data about document subtypes: which are + available, which are followed if any """ + res = dict((id, dict(message_subtype_data='')) for id in ids) + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] + + # find current model subtypes, add them to a dictionary + subtype_obj = self.pool.get('mail.message.subtype') + subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) + subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context)) + + # find the document followers, update the data + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, uid, [ + ('partner_id', '=', user_pid), + ('res_id', 'in', ids), + ('res_model', '=', self._name), + ], context=context) + for fol in fol_obj.browse(cr, uid, fol_ids, context=context): + thread_subtype_dict = subtype_dict.copy() + res[fol.res_id]['message_is_follower'] = True + for subtype in fol.subtype_ids: + thread_subtype_dict[subtype.name]['followed'] = True + res[fol.res_id]['message_subtype_data'] = '%s' % thread_subtype_dict + return res def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): @@ -150,8 +181,13 @@ class mail_thread(osv.AbstractModel): return [('id', 'in', res.keys())] _columns = { - 'message_is_follower': fields.function(_get_message_data, - type='boolean', string='Is a Follower', multi='_get_message_data'), + 'message_is_follower': fields.function(_get_subscription_data, + type='boolean', string='Is a Follower', multi='_get_subscription_data,'), + 'message_subtype_data': fields.function(_get_subscription_data, + type='text', string='Subscription data', multi="_get_subscription_data", + help="Holds data about the subtypes. The content of this field "\ + "is a structure holding the current model subtypes, and the "\ + "current document followed subtypes."), 'message_follower_ids': many2many_reference('res.partner', 'mail_followers', 'res_id', 'partner_id', reference_column='res_model', string='Followers'), @@ -563,7 +599,7 @@ class mail_thread(osv.AbstractModel): self.message_post(cr, uid, [id], message, context=context) def message_post(self, cr, uid, thread_id, body='', subject=False, type='notification', - subtype_xml_id=None, parent_id=False, attachments=None, context=None, **kwargs): + subtype=None, parent_id=False, attachments=None, context=None, **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. Extra keyword arguments will be used as default column values for the new mail.message record. @@ -579,8 +615,8 @@ class mail_thread(osv.AbstractModel): """ context = context or {} attachments = attachments or [] - assert (not thread_id) or isinstance(thread_id, (int,long)) or \ - (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id" + assert (not thread_id) or isinstance(thread_id, (int, long)) or \ + (isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id" if isinstance(thread_id, (list, tuple)): thread_id = thread_id and thread_id[0] @@ -598,15 +634,13 @@ class mail_thread(osv.AbstractModel): } attachment_ids.append((0, 0, data_attach)) + if subtype: + ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', subtype) + subtype_id = ref and ref[1] or False + else: + subtype_id = False + values = kwargs - subtype_obj = self.pool.get('mail.message.subtype') - if subtype_xml_id: - ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', subtype_xml_id) - subtype_browse = subtype_obj.browse(cr, uid, ref[1],context=context) - if self._name == subtype_browse.res_model: - values['subtype_id']=subtype_browse.id - if not subtype_browse.res_model: - values['subtype_id']=subtype_browse.id values.update({ 'model': context.get('thread_model', self._name) if thread_id else False, 'res_id': thread_id or False, @@ -615,34 +649,44 @@ class mail_thread(osv.AbstractModel): 'type': type, 'parent_id': parent_id, 'attachment_ids': attachment_ids, + 'subtype_id': subtype_id, }) - for x in ('from', 'to', 'cc'): values.pop(x, None) # Avoid warnings + # Avoid warnings about non-existing fields + for x in ('from', 'to', 'cc'): + values.pop(x, None) return self.pool.get('mail.message').create(cr, uid, values, context=context) #------------------------------------------------------ # Followers API #------------------------------------------------------ - def message_subscribe_users(self, cr, uid, ids, user_ids=None, context=None): + def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None): """ Wrapper on message_subscribe, using users. If user_ids is not provided, subscribe uid instead. """ - if not user_ids: user_ids = [uid] + if not user_ids: + user_ids = [uid] partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] - return self.message_subscribe(cr, uid, ids, partner_ids, context=context) + return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context) - def message_subscribe(self, cr, uid, ids, partner_ids,subtype_ids=None, context=None): + def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None): """ Add partners to the records followers. """ + self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) + if not subtype_ids: subtype_obj = self.pool.get('mail.message.subtype') - subtype_ids = subtype_obj.search(cr, uid, [('default', '=', 'true'),('res_model', '=', self._name)],context=context) - if subtype_ids: - self.message_subscribe_udpate_subtypes(cr, uid, ids, partner_ids, subtype_ids, context=context) - return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) + subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) + + fol_obj = self.pool.get('mail.followers') + fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context) + fol_obj.write(cr, uid, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context) + + return True def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None): """ Wrapper on message_subscribe, using users. If user_ids is not provided, unsubscribe uid instead. """ - if not user_ids: user_ids = [uid] + if not user_ids: + user_ids = [uid] partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)] return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context) @@ -678,9 +722,4 @@ class mail_thread(osv.AbstractModel): ''', (ids, self._name, partner_id)) return True - def message_subscribe_udpate_subtypes(self, cr, uid, ids, user_id, subtype_ids,context=None): - followers_obj = self.pool.get('mail.followers') - followers_ids = followers_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context) - return followers_obj.write(cr, uid, followers_ids, {'subtype_ids': [(6, 0 , subtype_ids)]}, context = context) - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index 1ecdc4b6e5f..28dd3e6fe4f 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -253,30 +253,65 @@ class test_mail(TestMailMockups): follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)]) 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') - def test_11_message_followers(self): - """ Tests designed for the subscriber API. """ + def test_11_message_followers_and_subtypes(self): + """ Tests designed for the subscriber API as well as message subtypes """ cr, uid = self.cr, self.uid user_admin = self.res_users.browse(cr, uid, uid) group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) - - # Create user Raoul + # Data: user Raoul user_raoul_id = self.res_users.create(cr, uid, {'name': 'Raoul Grosbedon', 'login': 'raoul'}) user_raoul = self.res_users.browse(cr, uid, user_raoul_id) + # Data: message subtypes + self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.group'}) + self.mail_message_subtype.create(cr, uid, {'name': 'mt_other_def', 'default': True, 'res_model': 'crm.lead'}) + self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_def', 'default': True, 'res_model': False}) + mt_mg_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.group'}) + mt_all_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_nodef', 'default': False, 'res_model': False}) + default_group_subtypes = self.mail_message_subtype.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', 'mail.group'), ('res_model', '=', False)]) - # Subscribe Raoul three times (niak niak) through message_subscribe_users + # ---------------------------------------- + # CASE1: test subscriptions with subtypes + # ---------------------------------------- + + # Do: Subscribe Raoul three times (niak niak) through message_subscribe_users group_pigs.message_subscribe_users([user_raoul_id, user_raoul_id]) group_pigs.message_subscribe_users([user_raoul_id]) group_pigs.refresh() + # Test: 2 followers (Admin and Raoul) follower_ids = [follower.id for follower in group_pigs.message_follower_ids] self.assertEqual(len(follower_ids), 2, 'There should be 2 Pigs fans') 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') + # Test: Raoul follows default subtypes + 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)]) + fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0] + fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids]) + self.assertEqual(set(fol_subtype_ids), set(default_group_subtypes), 'subscription subtypes are incorrect') - # Unsubscribe Raoul twice through message_unsubscribe_users + # Do: Unsubscribe Raoul twice through message_unsubscribe_users group_pigs.message_unsubscribe_users([user_raoul_id, user_raoul_id]) group_pigs.refresh() + # Test: 1 follower (Admin) follower_ids = [follower.id for follower in group_pigs.message_follower_ids] self.assertEqual(follower_ids, [user_admin.partner_id.id], 'Admin must be the only Pigs fan') + # Do: subscribe Admin with subtype_ids + group_pigs.message_subscribe_users([uid], [mt_mg_nodef, mt_all_nodef]) + 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)]) + fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0] + fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids]) + self.assertEqual(set(fol_subtype_ids), set([mt_mg_nodef, mt_all_nodef]), 'subscription subtypes are incorrect') + + # ---------------------------------------- + # CASE2: test mail_thread fields + # ---------------------------------------- + + group_pigs.refresh() + subtype_data = eval(group_pigs.message_subtype_data) + self.assertEqual(set(subtype_data.keys()), set(['comment', 'mt_mg_def', 'mt_all_def', 'mt_mg_nodef', 'mt_all_nodef']), 'mail.group available subtypes incorrect') + self.assertFalse(subtype_data['comment']['followed'], 'Admin should not follow comments in pigs') + self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs') + self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs') + def test_20_message_post(self): """ Tests designed for message_post. """ cr, uid = self.cr, self.uid @@ -307,9 +342,12 @@ class test_mail(TestMailMockups): _mail_bodyalt2 = 'Pigs rules\nAdmin' _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')] + # ---------------------------------------- # CASE1: post comment, body and subject specified + # ---------------------------------------- + self._init_mock_build_email() - msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment') + msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment', subtype='mt_comment') message = self.mail_message.browse(cr, uid, msg_id) sent_emails = self._build_email_kwargs_list # Test: notifications have been deleted @@ -334,10 +372,13 @@ class test_mail(TestMailMockups): for sent_email in sent_emails: self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect') + # ---------------------------------------- # CASE2: post an email with attachments, parent_id, partner_ids + # ---------------------------------------- + # TESTS: automatic subject, signature in body_html, attachments propagation self._init_mock_build_email() - msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email', + msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment', partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments) message = self.mail_message.browse(cr, uid, msg_id2) sent_emails = self._build_email_kwargs_list @@ -608,7 +649,7 @@ class test_mail(TestMailMockups): # Post 4 message on group_pigs for dummy in range(4): - group_pigs.message_post(body='My Body') + group_pigs.message_post(body='My Body', subtype='mt_comment') # Check there are 4 new needaction on mail.message notif_ids = self.mail_notification.search(cr, uid, [ @@ -652,6 +693,7 @@ class test_mail(TestMailMockups): def test_60_vote(self): """ Test designed for the vote/unvote feature. """ cr, uid = self.cr, self.uid + user_admin = self.res_users.browse(cr, uid, uid) group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) msg1 = group_pigs.message_post(body='My Body', subject='1') msg1 = self.mail_message.browse(cr, uid, msg1) @@ -677,32 +719,3 @@ class test_mail(TestMailMockups): msg1.refresh() # Test: msg1 has Bert as voter self.assertEqual(set(msg1.vote_user_ids), set([user_bert]), 'after unvoting for Admin, Bert is not the voter') - - def test_70_message_subtype(self): - """ Tests designed for message_subtype. """ - cr, uid = self.cr, self.uid - self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'}) - user_admin = self.res_users.browse(cr, uid, uid) - group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) - - # 0 - Admin - p_a_id = user_admin.partner_id.id - # Subscribe #1, - - group_pigs.message_subscribe_users([uid]) - - # Mail data - _subject = 'Pigs' - _mail_subject = '%s posted on %s' % (user_admin.name, group_pigs.name) - _body1 = 'Pigs rules' - _mail_body1 = 'Pigs rules\n
Admin
\n' - _mail_bodyalt1 = 'Pigs rules\nAdmin' - _body2 = 'Pigs rules' - _mail_body2 = 'Pigs rules\n
Admin
\n' - _mail_bodyalt2 = 'Pigs rules\nAdmin\n' - filter_subtype_id = self.mail_message_subtype.search(cr, uid, [('default','=',True)]) - # Post comment with body and subject, comment preference - msg_id = self.mail_group.message_post(cr, uid, [self.group_pigs_id], body=_body1, subject=_subject, type='comment',subtype_xml_id='mt_comment') - notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', msg_id)]) - self.assertTrue(len(notif_ids) >= 1,"subtype is email and show notification on wall") - diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 760d16dbef2..32d361a3126 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -245,7 +245,7 @@ class mail_compose_message(osv.TransientModel): post_values['attachments'] += new_attachments post_values.update(email_dict) # post the message - active_model_pool.message_post(cr, uid, [res_id], type='comment', context=context, **post_values) + active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values) # post process: update attachments, because id is not necessarily known when adding attachments in Chatter self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], {'res_id': wizard.id}, context=context)