[MERGE] Merged prototyping branch about improving performances

in messaging, especially for mass mailing and mail mail creation. Some things will
probably be discarded or improved further, but this work will serve as a basis
for the whole mass mailing refactorign about speed.

bzr revid: tde@openerp.com-20140314165621-stpmdbq92fbigc3u
This commit is contained in:
Thibault Delavallée 2014-03-14 17:56:21 +01:00
commit fd85a00311
7 changed files with 143 additions and 128 deletions

View File

@ -55,14 +55,23 @@ class mail_compose_message(osv.TransientModel):
)
return res
def get_recipients_data(self, cr, uid, values, context=None):
if values['composition_mode'] != 'mass_mail':
return super(mail_compose_message, self).get_recipients_data(cr, uid, values, context=context)
model, res_id, template_id = values['model'], values['res_id'], values.get('template_id')
active_ids = context.get('active_ids', list())
if not active_ids or not template_id:
return False
template = self.pool['email.template'].browse(cr, uid, template_id, context=context)
partner_to = self.render_template_batch(cr, uid, template.partner_to, model, active_ids[:3], context=context)
partner_ids = [int(data) for key, data in partner_to.iteritems() if data]
rec_names = [rec_name[1] for rec_name in self.pool['res.partner'].name_get(cr, SUPERUSER_ID, partner_ids, context=context)]
recipients = ', '.join(rec_names)
recipients += ' and %d more.' % (len(active_ids) - 3) if len(active_ids) > 3 else '.'
return recipients
_columns = {
'template_id': fields.many2one('email.template', 'Use template', select=True),
'partner_to': fields.char('To (Partner IDs)',
help="Comma-separated list of recipient partners ids (placeholders may be used here)"),
'email_to': fields.char('To (Emails)',
help="Comma-separated recipient addresses (placeholders may be used here)",),
'email_cc': fields.char('Cc (Emails)',
help="Carbon copy recipients (placeholders may be used here)"),
}
def send_mail(self, cr, uid, ids, context=None):
@ -92,7 +101,7 @@ class mail_compose_message(osv.TransientModel):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
fields = ['subject', 'body_html', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:

View File

@ -7,22 +7,6 @@
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='subject']" position="after">
<label string="Template Recipients" for="partner_to"
groups="base.group_no_one"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<div groups="base.group_no_one" name="template_recipients"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}">
<group class="oe_grey">
<!-- <label string="Partners" for="partner_to"/> -->
<field name="partner_to" readonly="1"/>
<!-- <label string="Email To" for="email_to"/> -->
<field name="email_to" readonly="1"/>
<!-- <label string="Email CC" for="email_cc"/> -->
<field name="email_cc" readonly="1"/>
</group>
</div>
</xpath>
<xpath expr="//footer" position="inside">
<group class="oe_right oe_form" col="1">
<div>Use template

View File

@ -36,6 +36,7 @@
<div>
<group string="Status">
<field name="auto_delete"/>
<field name="notification"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>

View File

@ -76,6 +76,7 @@ class mail_message(osv.Model):
_message_read_more_limit = 1024
def default_get(self, cr, uid, fields, context=None):
# print '\tmail_message: default_get on', fields
# 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)
@ -86,17 +87,6 @@ class mail_message(osv.Model):
return name
return name[:self._message_record_name_length] + '...'
def _get_record_name(self, cr, uid, ids, name, arg, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
# TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
continue
result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
return result
def _get_to_read(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)
@ -172,9 +162,7 @@ class mail_message(osv.Model):
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
'record_name': fields.function(_get_record_name, type='char',
store=True, string='Message Record Name',
help="Name get of the related document."),
'record_name': fields.char('Message Record Name', help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id',
string='Notifications', auto_join=True,
help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
@ -783,6 +771,13 @@ class mail_message(osv.Model):
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
(self._description, operation))
def _get_record_name(self, cr, uid, values, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
return False
return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
def _get_reply_to(self, cr, uid, values, context=None):
""" Return a specific reply_to: alias of the document through message_get_reply_to
or take the email_from
@ -841,8 +836,12 @@ class mail_message(osv.Model):
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
if 'record_name' not in values and 'default_record_name' not in context:
values['record_name'] = self._get_record_name(cr, uid, values, context=context)
newid = super(mail_message, self).create(cr, uid, values, context)
if not values.get('subtype_id'):
return newid
self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))

View File

@ -507,7 +507,7 @@ openerp.mail = function (session) {
}
$.when(recipient_done).done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
// 'default_composition_mode': default_composition_mode,
'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),

View File

@ -68,28 +68,25 @@ class mail_compose_message(osv.TransientModel):
if context is None:
context = {}
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
# get some important values from context
composition_mode = context.get('default_composition_mode', context.get('mail.compose.message.mode'))
model = context.get('default_model', context.get('active_model'))
res_id = context.get('default_res_id', context.get('active_id'))
message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
active_ids = context.get('active_ids')
# v6.1 compatibility mode
result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode'))
result['model'] = result.get('model', context.get('active_model'))
result['res_id'] = result.get('res_id', context.get('active_id'))
result['parent_id'] = result.get('parent_id', context.get('message_id'))
# default values according to composition mode - NOTE: reply is deprecated, fall back on comment
if result['composition_mode'] == 'reply':
result['composition_mode'] = 'comment'
vals = {}
if 'active_domain' in context: # not context.get() because we want to keep global [] domains
result['use_active_domain'] = True
result['active_domain'] = '%s' % context.get('active_domain')
elif not result.get('active_domain'):
result['active_domain'] = ''
# get default values according to the composition mode
if composition_mode == 'reply':
vals = self.get_message_data(cr, uid, message_id, context=context)
elif composition_mode == 'comment' and model and res_id:
vals = self.get_record_data(cr, uid, model, res_id, context=context)
elif composition_mode == 'mass_mail' and model and active_ids:
vals = {'model': model, 'res_id': res_id}
else:
vals = {'model': model, 'res_id': res_id}
if composition_mode:
vals['composition_mode'] = composition_mode
vals['use_active_domain'] = True
vals['active_domain'] = '%s' % context.get('active_domain')
if result.get('parent_id'):
vals.update(self.get_message_data(cr, uid, result.get('parent_id'), context=context))
if result['composition_mode'] == 'comment' and result['model'] and result['res_id']:
vals.update(self.get_record_data(cr, uid, result['model'], result['res_id'], context=context))
result['recipients_data'] = self.get_recipients_data(cr, uid, result, context=context)
for field in vals:
if field in fields:
@ -102,13 +99,15 @@ class mail_compose_message(osv.TransientModel):
# but when creating the mail.message to create the mail.compose.message
# access rights issues may rise
# We therefore directly change the model and res_id
if result.get('model') == 'res.users' and result.get('res_id') == uid:
if result['model'] == 'res.users' and result['res_id'] == uid:
result['model'] = 'res.partner'
result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id
return result
def _get_composition_mode_selection(self, cr, uid, context=None):
return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')]
return [('comment', 'Post on a document'),
('mass_mail', 'Email Mass Mailing'),
('mass_post', 'Post on Multiple Documents')]
_columns = {
'composition_mode': fields.selection(
@ -116,15 +115,15 @@ class mail_compose_message(osv.TransientModel):
string='Composition mode'),
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional contacts'),
'wizard_id', 'partner_id', 'Additional Contacts'),
'recipients_data': fields.text(string='Recipients Data',
help='Helper field used in mass mailing to display a sample of recipients'),
'use_active_domain': fields.boolean('Use active domain'),
'active_domain': fields.char('Active domain', readonly=True),
'post': fields.boolean('Post a copy in the document',
help='Post a copy of the message on the document communication history.'),
'notify': fields.boolean('Notify followers',
help='Notify followers of the document'),
help='Notify followers of the document (mass post only)'),
'same_thread': fields.boolean('Replies in the document',
help='Replies to the messages will go into the selected document.'),
help='Replies to the messages will go into the selected document (mass mail only)'),
'attachment_ids': fields.many2many('ir.attachment',
'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments'),
@ -136,8 +135,7 @@ class mail_compose_message(osv.TransientModel):
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
'post': False,
'notify': False,
'notify': True,
'same_thread': True,
}
@ -169,6 +167,21 @@ class mail_compose_message(osv.TransientModel):
not want that feature in the wizard. """
return
def get_recipients_data(self, cr, uid, values, context=None):
""" Returns a string explaining the targetted recipients, to ease the use
of the wizard. """
composition_mode, model, res_id = values['composition_mode'], values['model'], values['res_id']
if composition_mode == 'comment' and model and res_id:
doc_name = self.pool[model].name_get(cr, uid, [res_id], context=context)
return doc_name and 'Followers of %s' % doc_name[0][1] or False
elif composition_mode == 'mass_post' and model:
active_ids = context.get('active_ids', list())
if not active_ids:
return False
name_gets = [rec_name[1] for rec_name in self.pool[model].name_get(cr, uid, active_ids[:3], context=context)]
return 'Followers of selected documents (' + ', '.join(name_gets) + len(active_ids) > 3 and ', ...' or '' + ')'
return False
def get_record_data(self, cr, uid, model, res_id, context=None):
""" Returns a defaults-like dict with initial values for the composition
wizard when sending an email related to the document record
@ -179,17 +192,10 @@ class mail_compose_message(osv.TransientModel):
:param int res_id: id of the document record this mail is related to
"""
doc_name_get = self.pool[model].name_get(cr, uid, [res_id], context=context)
record_name = False
if doc_name_get:
record_name = doc_name_get[0][1]
values = {
'model': model,
'res_id': res_id,
'record_name': record_name,
return {
'record_name': doc_name_get and doc_name_get[0][1] or False,
'subject': doc_name_get and 'Re: %s' % doc_name_get[0][1] or False,
}
if record_name:
values['subject'] = 'Re: %s' % record_name
return values
def get_message_data(self, cr, uid, message_id, context=None):
""" Returns a defaults-like dict with initial values for the composition
@ -208,23 +214,20 @@ class mail_compose_message(osv.TransientModel):
# create subject
re_prefix = _('Re:')
reply_subject = tools.ustr(message_data.subject or message_data.record_name or '')
if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject:
if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
reply_subject = "%s %s" % (re_prefix, reply_subject)
# get partner_ids from original message
partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else []
partner_ids += context.get('default_partner_ids', [])
if context.get('is_private',False) and message_data.author_id : #check message is private then add author also in partner list.
if context.get('is_private') and message_data.author_id: # check message is private then add author also in partner list.
partner_ids += [message_data.author_id.id]
# update the result
result = {
return {
'record_name': message_data.record_name,
'model': message_data.model,
'res_id': message_data.res_id,
'parent_id': message_data.id,
'subject': reply_subject,
'partner_ids': partner_ids,
}
return result
#------------------------------------------------------
# Wizard validation and send
@ -235,54 +238,46 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
import datetime
print '--> beginning sending email', datetime.datetime.now()
# clean the context (hint: mass mailing sets some default values that
# could be wrongly interpreted by mail_mail)
context.pop('default_email_to', None)
context.pop('default_partner_ids', None)
active_ids = context.get('active_ids')
is_log = context.get('mail_compose_log', False)
for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail'
mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')
active_model_pool = self.pool[wizard.model if wizard.model else 'mail.thread']
if not hasattr(active_model_pool, 'message_post'):
context['thread_model'] = wizard.model
active_model_pool = self.pool['mail.thread']
# wizard works in batch mode: [res_id] or active_ids or active_domain
if mass_mail_mode and wizard.use_active_domain and wizard.model:
if mass_mode and wizard.use_active_domain and wizard.model:
res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context)
elif mass_mail_mode and wizard.model and active_ids:
res_ids = active_ids
elif mass_mode and wizard.model and context.get('active_ids'):
res_ids = context['active_ids']
else:
res_ids = [wizard.res_id]
print '----> before computing values', datetime.datetime.now()
all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
print '----> after computing values', datetime.datetime.now()
for res_id, mail_values in all_mail_values.iteritems():
if mass_mail_mode and not wizard.post:
m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments(
cr, uid, mail_values.pop('attachments', []),
mail_values.pop('attachment_ids', []),
'mail.message', 0,
context=context)
mail_values['attachment_ids'] = m2m_attachment_ids
if not mail_values.get('reply_to'):
mail_values['reply_to'] = mail_values['email_from']
self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
if wizard.composition_mode == 'mass_mail':
self.pool['mail.mail'].create(cr, uid, mail_values, context=context)
else:
subtype = 'mail.mt_comment'
if is_log: # log a note: subtype is False
if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False
subtype = False
elif mass_mail_mode: # mass mail: is a log pushed to recipients unless specified, author not added
if not wizard.notify:
subtype = False
if wizard.composition_mode == 'mass_post':
context = dict(context,
mail_notify_force_send=False, # do not send emails directly but use the queue instead
mail_create_nosubscribe=True) # add context key to avoid subscribing the author
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
print '--> finished sending email', datetime.datetime.now()
return {'type': 'ir.actions.act_window_close'}
def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
@ -303,9 +298,12 @@ class mail_compose_message(osv.TransientModel):
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachment_ids': [attach.id for attach in wizard.attachment_ids],
'author_id': wizard.author_id.id,
'email_from': wizard.email_from,
}
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
# rendered values using template
email_dict = rendered_values[res_id]
mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
# process attachments: should not be encoded before being processed by message_post / mail_mail create
@ -323,20 +321,38 @@ class mail_compose_message(osv.TransientModel):
if email_dict.get('email_from'):
mail_values['email_from'] = email_dict.pop('email_from')
# replies redirection: mass mailing only
if wizard.same_thread and wizard.post:
if wizard.same_thread:
email_dict.pop('reply_to', None)
else:
mail_values['reply_to'] = email_dict.pop('reply_to', None)
mail_values.update(email_dict)
# mass mailing without post: mail_mail values
if mass_mail_mode and not wizard.post:
# value tweaking in mass mailing
mail_values['record_name'] = False # avoid browsing the record for an email
if wizard.same_thread: # same thread: keep a copy of the message in the chatter to enable the reply redirection
mail_values.update(notification=True, model=wizard.model, res_id=res_id)
m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments(
cr, uid, mail_values.pop('attachments', []),
mail_values.pop('attachment_ids', []),
'mail.message', 0,
context=context)
mail_values['attachment_ids'] = m2m_attachment_ids
if not mail_values.get('reply_to'):
mail_values['reply_to'] = mail_values['email_from']
# mail_mail values
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')
mail_values['body_html'] = mail_values.get('body', '')
mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
results[res_id] = mail_values
return results
#------------------------------------------------------
# Template rendering
#------------------------------------------------------
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
"""Generate template-based values of wizard, for the document records given
by res_ids. This method is meant to be inherited by email_template that

View File

@ -28,29 +28,35 @@
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="subject" placeholder="Subject..." required="True"/>
<!-- classic message composer -->
<label for="partner_ids" string="Recipients"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}"/>
<div groups="base.group_user"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}">
<span attrs="{'invisible':[('model', '=', False)]}">
Followers of
<field name="record_name" readonly="1" class="oe_inline oe_compose_recipients"/>
and
<!-- recipients -->
<label for="partner_ids" string="Recipients" groups="base.group_user"/>
<div groups="base.group_user">
<span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
<strong>Email mass mailing</strong> on
<span attrs="{'invisible': [('use_active_domain', '=', True)]}">the selected records</span>
<span attrs="{'invisible': [('use_active_domain', '=', False)]}">the current search filter</span>.
<br />
<span>The following contacts will be mailed: </span>
</span>
<field name="recipients_data" class="oe_inline oe_compose_recipients" readonly="1"/>
<span attrs="{'invisible':['|', ('composition_mode', '!=', 'comment'), ('recipients_data', '=', False)]}">and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
</div>
<!-- mass post / mass mailing -->
<field name="post"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<!-- mass post -->
<field name="notify"
attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/>
<field name="same_thread"
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_mail'), ('post', '=', False)]}"/>
<field name="reply_to" placeholder="Email address te redirect replies..."
attrs="{'invisible':['|', '&amp;', ('same_thread', '=', True), ('post', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':['&amp;', '|', ('post', '=', False), ('same_thread', '=', False), ('composition_mode', '=', 'mass_mail')]}"/>
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_post')]}"/>
<!-- mass mailing -->
<label for="same_thread"/>
<div>
<field name="same_thread"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
(a copy of the message will be added in the Chatter of each document)
</div>
<field name="reply_to" placeholder="Email address to redirect replies..."
attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':[('same_thread', '!=', True), ('composition_mode', '=', 'mass_mail')]}"/>
</group>
<field name="body"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>