[MERGE] Perform auto-join and mailboxes+needaction speed udpate. Addons-side branch, holding an update of functional fields to use _auto_join and refactoring of starred messages implementation.

mail_thread
- updated the message_unread function field, now returning a domain on mail_message notifications. This domain is quite complex and redundant, but mandatory in 7.0 considering the limitations of _auto_join
- message_ids is now using auto_join
- removed message_comment_ids field, not necessary and that lead to confusing data on kanban views
mail_message:
- notifications_ids is now using auto_join
- refactored the todo, now delegated to the notifications
- the favorite_user_ids field is therefore removed, and various methods updated
- updated _search_to_read and _search_starred to return a domain on notifications
- updated the domains of the mailboxes accordingly

bzr revid: tde@openerp.com-20121210154452-ngy6lw1jg97ib5gn
This commit is contained in:
Thibault Delavallée
2012-12-10 16:44:52 +01:00
8 changed files with 132 additions and 115 deletions
+3 -2
View File
@@ -666,7 +666,6 @@ Andrew</field>
<field name="type">comment</field>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="author_id" ref="base.res_partner_4"/>
<field name="favorite_user_ids" eval="[(6, 0, [ref('base.user_root')])]"/>
</record>
<record id="msg_case18_2" model="mail.message">
<field name="model">crm.lead</field>
@@ -676,7 +675,6 @@ Andrew</field>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="parent_id" ref="msg_case18_1"/>
<field name="author_id" ref="base.partner_demo"/>
<field name="favorite_user_ids" eval="[(6, 0, [ref('base.user_root')])]"/>
</record>
<record id="msg_case1_1" model="mail.message">
<field name="subject">Inquiry</field>
@@ -694,6 +692,9 @@ Andrew</field>
<field name="body">Want to know features and benefits to use the new software.</field>
<field name="type">comment</field>
</record>
<function model="mail.message" name="set_message_starred"
eval="[ ref('msg_case18_1'), ref('msg_case18_2')], True, {}"
/>
</data>
</openerp>
+5 -28
View File
@@ -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.
+93 -48
View File
@@ -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'):
+7 -20
View File
@@ -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'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids))
res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (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)
+12 -5
View File
@@ -11,9 +11,8 @@
}</field>
<field name="params" eval="&quot;{
'domain': [
('notification_ids.partner_id.user_ids', 'in', [uid]),
('to_read', '=', True),
('favorite_user_ids', 'not in', [uid])
('starred', '=', False),
],
'view_mailbox': True,
'view_inbox': True,
@@ -40,7 +39,9 @@
'search_default_message_unread': True
}</field>
<field name="params" eval="&quot;{
'domain': [('partner_ids.user_ids', 'in', [uid])],
'domain': [
('partner_ids.user_ids', 'in', [uid])
],
'view_mailbox': True,
'read_action': 'read',
'show_compose_message': False }&quot;"/>
@@ -63,7 +64,9 @@
'search_default_message_unread': True
}</field>
<field name="params" eval="&quot;{
'domain': [('favorite_user_ids', 'in', [uid])],
'domain': [
('starred', '=', True),
],
'view_mailbox': True,
'read_action': 'read',
'compose_as_todo': True }&quot;"/>
@@ -85,7 +88,11 @@
'default_res_id': uid
}</field>
<field name="params" eval="&quot;{
'domain': ['|', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('author_id.user_ids', 'in', [uid])],
'domain': [
'|',
('notification_ids.partner_id.user_ids', 'in', [uid]),
('author_id.user_ids', 'in', [uid]),
],
'view_mailbox': True,
'show_compose_message': False }&quot;"/>
<field name="help" type="html">
+3 -3
View File
@@ -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) {
+8 -7
View File
@@ -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')
+1 -2
View File
@@ -57,9 +57,8 @@
}</field>
<field name="params" eval="&quot;{
'domain': [
('notification_ids.partner_id.user_ids', 'in', [uid]),
('to_read', '=', True),
('favorite_user_ids', 'not in', [uid])
('starred', '=', False),
],
'show_compose_message': False,
'show_link_partner': False,