diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index 6d392097d84..746936e28cd 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -666,7 +666,6 @@ Andrew comment - crm.lead @@ -676,7 +675,6 @@ Andrew - Inquiry @@ -694,6 +692,9 @@ Andrew Want to know features and benefits to use the new software. comment + diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 8d3876ec91f..9793e036351 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -19,7 +19,6 @@ # ############################################################################## -from openerp import SUPERUSER_ID from osv import osv from osv import fields import tools @@ -63,18 +62,21 @@ class mail_notification(osv.Model): 'partner_id': fields.many2one('res.partner', string='Contact', ondelete='cascade', required=True, select=1), 'read': fields.boolean('Read', select=1), + 'starred': fields.boolean('Starred', select=1, + help='Starred message that goes into the todo mailbox'), 'message_id': fields.many2one('mail.message', string='Message', ondelete='cascade', required=True, select=1), } _defaults = { 'read': False, + 'starred': False, } def init(self, cr): - cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_message_id',)) + cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_starred_message_id',)) if not cr.fetchone(): - cr.execute('CREATE INDEX mail_notification_partner_id_read_message_id ON mail_notification (partner_id, read, message_id)') + cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)') def create(self, cr, uid, vals, context=None): """ Override of create to check that we can not create a notification @@ -83,31 +85,6 @@ class mail_notification(osv.Model): return super(mail_notification, self).create(cr, uid, vals, context=context) return False - def set_message_read(self, cr, uid, msg_ids, read=None, context=None): - """ Set messages as (un)read. Technically, the notifications related - to uid are set to (un)read. If for some msg_ids there are missing - notifications (i.e. due to load more or thread parent fetching), - they are created. - - :param bool read: (un)read notification - """ - user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] - notif_ids = self.search(cr, uid, [ - ('partner_id', '=', user_pid), - ('message_id', 'in', msg_ids) - ], context=context) - - # all message have notifications: already set them as (un)read - if len(notif_ids) == len(msg_ids): - return self.write(cr, uid, notif_ids, {'read': read}, context=context) - - # some messages do not have notifications: find which one, create notification, update read status - notified_msg_ids = [notification.message_id.id for notification in self.browse(cr, uid, notif_ids, context=context)] - to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids)) - for msg_id in to_create_msg_ids: - self.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context) - return self.write(cr, uid, notif_ids, {'read': read}, context=context) - def get_partners_to_notify(self, cr, uid, message, context=None): """ Return the list of partners to notify, based on their preferences. diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index aec106f766b..a5080e48373 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -55,7 +55,7 @@ class mail_message(osv.Model): _message_read_limit = 30 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from', - 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids'] + 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name'] _message_record_name_length = 18 _message_read_more_limit = 1024 @@ -63,7 +63,7 @@ class mail_message(osv.Model): # protection for `default_type` values leaking from menu action context (e.g. for invoices) if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection: context = dict(context, default_type=None) - return super(mail_message, self).default_get(cr, uid, fields, context=context) + return super(mail_message, self).default_get(cr, uid, fields, context=context) def _shorten_name(self, name): if len(name) <= (self._message_record_name_length + 3): @@ -98,15 +98,26 @@ class mail_message(osv.Model): def _search_to_read(self, cr, uid, obj, name, domain, context=None): """ Search for messages to read by the current user. Condition is inversed because we search unread message on a read column. """ - if domain[0][2]: - read_cond = "(read = False OR read IS NULL)" - else: - read_cond = "read = True" + return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.read', '=', not domain[0][2])] + + def _get_starred(self, cr, uid, ids, name, arg, context=None): + """ Compute if the message is unread by the current user. """ + res = dict((id, False) for id in ids) partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] - cr.execute("SELECT message_id FROM mail_notification "\ - "WHERE partner_id = %%s AND %s" % read_cond, - (partner_id,)) - return [('id', 'in', [r[0] for r in cr.fetchall()])] + notif_obj = self.pool.get('mail.notification') + notif_ids = notif_obj.search(cr, uid, [ + ('partner_id', 'in', [partner_id]), + ('message_id', 'in', ids), + ('starred', '=', True), + ], context=context) + for notif in notif_obj.browse(cr, uid, notif_ids, context=context): + res[notif.message_id.id] = True + return res + + def _search_starred(self, cr, uid, obj, name, domain, context=None): + """ Search for messages to read by the current user. Condition is + inversed because we search unread message on a read column. """ + return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])] def name_get(self, cr, uid, ids, context=None): # name_get may receive int id instead of an id list @@ -146,7 +157,7 @@ class mail_message(osv.Model): store=True, string='Message Record Name', help="Name get of the related document."), 'notification_ids': fields.one2many('mail.notification', 'message_id', - string='Notifications', + string='Notifications', auto_join=True, help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'), 'subject': fields.char('Subject'), 'date': fields.datetime('Date'), @@ -154,21 +165,19 @@ class mail_message(osv.Model): 'body': fields.html('Contents', help='Automatically sanitized HTML contents'), 'to_read': fields.function(_get_to_read, fnct_search=_search_to_read, type='boolean', string='To read', - help='Functional field to search for messages the current user has to read'), + help='Current user has an unread notification linked to this message'), + 'starred': fields.function(_get_starred, fnct_search=_search_starred, + type='boolean', string='Starred', + help='Current user has a starred notification linked to this message'), 'subtype_id': fields.many2one('mail.message.subtype', 'Subtype', ondelete='set null', select=1,), 'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', help='Users that voted for this message'), - 'favorite_user_ids': fields.many2many('res.users', 'mail_favorite', - 'message_id', 'user_id', string='Favorite', - help='Users that set this message in their favorites'), } def _needaction_domain_get(self, cr, uid, context=None): - if self._needaction: - return [('to_read', '=', True)] - return [] + return [('to_read', '=', True)] def _get_default_author(self, cr, uid, context=None): return self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] @@ -196,27 +205,62 @@ class mail_message(osv.Model): return new_has_voted or False #------------------------------------------------------ - # Favorite + # Notification API #------------------------------------------------------ - def favorite_toggle(self, cr, uid, ids, context=None): - ''' Toggles favorite. Performed using read to avoid access rights issues. - Done as SUPERUSER_ID because uid may star a message he cannot modify. ''' - for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context): - new_is_favorite = not (uid in message.get('favorite_user_ids')) - if new_is_favorite: - self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context) - # when setting a favorite, set the related notification as unread, or create an unread notification if not existing - notification_obj = self.pool.get('mail.notification') - pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] - notif_id = notification_obj.search(cr, SUPERUSER_ID, [('message_id', '=', message.get('id')), ('partner_id', '=', pid)], context=context) - if notif_id: - notification_obj.write(cr, SUPERUSER_ID, notif_id, {'read': False}, context=context) - else: - notification_obj.create(cr, SUPERUSER_ID, {'message_id': message.get('id'), 'partner_id': pid, 'read': False}, context=context) - else: - self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context) - return new_is_favorite or False + def set_message_read(self, cr, uid, msg_ids, read, context=None): + """ Set messages as (un)read. Technically, the notifications related + to uid are set to (un)read. If for some msg_ids there are missing + notifications (i.e. due to load more or thread parent fetching), + they are created. + + :param bool read: set notification as (un)read + """ + notification_obj = self.pool.get('mail.notification') + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] + notif_ids = notification_obj.search(cr, uid, [ + ('partner_id', '=', user_pid), + ('message_id', 'in', msg_ids) + ], context=context) + + # all message have notifications: already set them as (un)read + if len(notif_ids) == len(msg_ids): + return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context) + + # some messages do not have notifications: find which one, create notification, update read status + notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)] + to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids)) + for msg_id in to_create_msg_ids: + notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context) + return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context) + + def set_message_starred(self, cr, uid, msg_ids, starred, context=None): + """ Set messages as (un)starred. Technically, the notifications related + to uid are set to (un)starred. If for some msg_ids there are missing + notifications (i.e. due to load more or thread parent fetching), + they are created. + + :param bool starred: set notification as (un)starred + """ + notification_obj = self.pool.get('mail.notification') + user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] + notif_ids = notification_obj.search(cr, uid, [ + ('partner_id', '=', user_pid), + ('message_id', 'in', msg_ids) + ], context=context) + + # all message have notifications: already set them as (un)starred + if len(notif_ids) == len(msg_ids): + notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context) + return starred + + # some messages do not have notifications: find which one, create notification, update starred status + notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)] + to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids)) + for msg_id in to_create_msg_ids: + notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context) + notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context) + return starred #------------------------------------------------------ # Message loading for web interface @@ -291,7 +335,6 @@ class mail_message(osv.Model): # votes and favorites: res.users ids, no prefetching should be done vote_nb = len(message.vote_user_ids) has_voted = uid in [user.id for user in message.vote_user_ids] - is_favorite = uid in [user.id for user in message.favorite_user_ids] return {'id': message.id, 'type': message.type, @@ -309,7 +352,7 @@ class mail_message(osv.Model): 'partner_ids': [], 'vote_nb': vote_nb, 'has_voted': has_voted, - 'is_favorite': is_favorite, + 'is_favorite': message.starred, 'attachment_ids': [], } @@ -482,15 +525,6 @@ class mail_message(osv.Model): thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context) return message_list - # TDE Note: do we need this ? - # def user_free_attachment(self, cr, uid, context=None): - # attachment = self.pool.get('ir.attachment') - # attachment_list = [] - # attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('create_uid', '=', uid)]) - # if len(attachment_ids): - # attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)] - # return attachment_list - #------------------------------------------------------ # mail_message internals #------------------------------------------------------ @@ -648,12 +682,23 @@ class mail_message(osv.Model): (self._description, operation)) def create(self, cr, uid, values, context=None): + if context is None: + context = {} + default_starred = context.pop('default_starred', False) if not values.get('message_id') and values.get('res_id') and values.get('model'): values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values) elif not values.get('message_id'): values['message_id'] = tools.generate_tracking_message_id('private') newid = super(mail_message, self).create(cr, uid, values, context) self._notify(cr, SUPERUSER_ID, newid, context=context) + # TDE FIXME: handle default_starred. Why not setting an inv on starred ? + # Because starred will call set_message_starred, that looks for notifications. + # When creating a new mail_message, it will create a notification to a message + # that does not exist, leading to an error (key not existing). Also this + # this means unread notifications will be created, yet we can not assure + # this is what we want. + if default_starred: + self.set_message_starred(cr, uid, [newid], True, context=context) return newid def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 30954be4ed8..e706ad66a33 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -88,7 +88,7 @@ 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_summary'] = "9 %d + %d" % (cls, len(thread.message_ids), len(thread.message_follower_ids)) return res @@ -121,18 +121,8 @@ class mail_thread(osv.AbstractModel): return res - def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): - partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] - res = {} - notif_obj = self.pool.get('mail.notification') - notif_ids = notif_obj.search(cr, uid, [ - ('partner_id', '=', partner_id), - ('message_id.model', '=', self._name), - ('read', '=', False) - ], context=context) - for notif in notif_obj.browse(cr, uid, notif_ids, context=context): - res[notif.message_id.res_id] = True - return [('id', 'in', res.keys())] + def _search_message_unread(self, cr, uid, obj=None, name=None, domain=None, context=None): + return [('message_ids.to_read', '=', True)] def _get_followers(self, cr, uid, ids, name, arg, context=None): fol_obj = self.pool.get('mail.followers') @@ -201,16 +191,14 @@ class mail_thread(osv.AbstractModel): 'message_follower_ids': fields.function(_get_followers, fnct_inv=_set_followers, fnct_search=_search_followers, type='many2many', obj='res.partner', string='Followers', multi='_get_followers'), - 'message_comment_ids': fields.one2many('mail.message', 'res_id', - domain=lambda self: [('model', '=', self._name), ('type', 'in', ('comment', 'email'))], - string='Comments and emails', - help="Comments and emails"), 'message_ids': fields.one2many('mail.message', 'res_id', domain=lambda self: [('model', '=', self._name)], + auto_join=True, string='Messages', help="Messages and communication history"), - 'message_unread': fields.function(_get_message_data, fnct_search=_search_unread, - type='boolean', string='Unread Messages', multi="_get_message_data", + 'message_unread': fields.function(_get_message_data, + fnct_search=_search_message_unread, multi="_get_message_data", + type='boolean', string='Unread Messages', help="If checked new messages require your attention."), 'message_summary': fields.function(_get_message_data, method=True, type='text', string='Summary', multi="_get_message_data", @@ -245,7 +233,6 @@ class mail_thread(osv.AbstractModel): def copy(self, cr, uid, id, default=None, context=None): default = default or {} default['message_ids'] = [] - default['message_comment_ids'] = [] default['message_follower_ids'] = [] return super(mail_thread, self).copy(cr, uid, id, default=default, context=context) diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml index 4106eb2e024..d5c111573ea 100644 --- a/addons/mail/mail_thread_view.xml +++ b/addons/mail/mail_thread_view.xml @@ -11,9 +11,8 @@ } @@ -63,7 +64,9 @@ 'search_default_message_unread': True } @@ -85,7 +88,11 @@ 'default_res_id': uid } diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 82534d713a3..7181367cf85 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -847,7 +847,7 @@ openerp.mail = function (session) { } var message_ids = _.map(messages, function (val) { return val.id; }); - this.ds_notification.call('set_message_read', [message_ids, read_value, this.context]) + this.ds_message.call('set_message_read', [message_ids, read_value, this.context]) .then(function () { // apply modification _.each(messages, function (msg) { @@ -896,7 +896,7 @@ openerp.mail = function (session) { var self=this; var button = self.$('.oe_star:first'); - this.ds_message.call('favorite_toggle', [[self.id]]) + this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite]) .then(function (star) { self.is_favorite=star; if (self.is_favorite) { @@ -991,7 +991,7 @@ openerp.mail = function (session) { // add message composition form view if (!this.compose_message) { this.compose_message = new mail.ThreadComposeMessage(this, this, { - 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_favorite_user_ids': [this.session.uid] }) : this.context, + 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context, 'options': this.options, }); if (!this.thread_level || this.thread_level > this.options.display_indented_thread) { diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index bc6df813ef7..c18dea46452 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -795,7 +795,7 @@ class test_mail(test_mail_mockup.TestMailMockups): # Test: msg has Bert as voter self.assertEqual(set(msg.vote_user_ids), set([user_raoul]), 'mail_message vote: after unvoting, Bert should be in the voter') - def test_70_message_favorite(self): + def test_70_message_star(self): """ Tests for favorites. """ cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs # Data: post a message on Pigs @@ -803,17 +803,18 @@ class test_mail(test_mail_mockup.TestMailMockups): msg = self.mail_message.browse(cr, uid, msg_id) # Do: Admin stars msg - self.mail_message.favorite_toggle(cr, uid, [msg.id]) + self.mail_message.set_message_starred(cr, uid, [msg.id], True) msg.refresh() # Test: msg starred by Admin - self.assertEqual(set(msg.favorite_user_ids), set([user_admin]), 'mail_message favorite: after starring, Admin should be in favorite_user_ids') + self.assertTrue(msg.starred, 'mail_message starred failed') # Do: Bert stars msg - self.mail_message.favorite_toggle(cr, user_raoul.id, [msg.id]) + self.mail_message.set_message_starred(cr, user_raoul.id, [msg.id], True) msg.refresh() # Test: msg starred by Admin and Raoul - self.assertEqual(set(msg.favorite_user_ids), set([user_admin, user_raoul]), 'mail_message favorite: after starring, Admin and Raoul should be in favorite_user_ids') + # self.assertTrue(msg.starred, set([user_admin, user_raoul]), 'mail_message favorite: after starring, Admin and Raoul should be in favorite_user_ids') # Do: Admin unvote for msg - self.mail_message.favorite_toggle(cr, uid, [msg.id]) + self.mail_message.set_message_starred(cr, uid, [msg.id], False) msg.refresh() # Test: msg starred by Raoul - self.assertEqual(set(msg.favorite_user_ids), set([user_raoul]), 'mail_message favorite: after unstarring, Raoul should be in favorite_user_ids') + self.assertFalse(msg.starred, 'mail_message starred failed') + # self.assertEqual(set(msg.favorite_user_ids), set([user_raoul]), 'mail_message favorite: after unstarring, Raoul should be in favorite_user_ids') diff --git a/addons/portal/portal_view.xml b/addons/portal/portal_view.xml index b7765e63ffd..4aad43716b6 100644 --- a/addons/portal/portal_view.xml +++ b/addons/portal/portal_view.xml @@ -57,9 +57,8 @@ }