[IMP] mail.compose.message, email.template: rendering is now done in batch.

Also refactored send_mail in mail.compose.message in order to be able to override
mail values without having to intefere with the send_mail behavior.

bzr revid: tde@openerp.com-20130828140929-xe9hbmbo6jxgs9mh
This commit is contained in:
Thibault Delavallée 2013-08-28 16:09:29 +02:00
parent eaf07ae5c4
commit 9ef46123a6
3 changed files with 272 additions and 178 deletions

View File

@ -67,7 +67,7 @@ class email_template(osv.osv):
_description = 'Email Templates'
_order = 'name'
def render_template(self, cr, uid, template, model, res_id, context=None):
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
"""Render the given template text, replace mako expressions ``${expr}``
with the result of evaluating these expressions with
an evaluation context containing:
@ -79,46 +79,60 @@ class email_template(osv.osv):
:param str template: the template text to render
:param str model: model name of the document record this mail is related to.
:param int res_id: id of the document record this mail is related to.
:param int res_ids: list of ids of document records those mails are related to.
"""
if not template:
return u""
if context is None:
context = {}
try:
template = tools.ustr(template)
record = None
if res_id:
record = self.pool[model].browse(cr, uid, res_id, context=context)
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
variables = {
'object': record,
'user': user,
'ctx': context, # context kw would clash with mako internals
}
result = mako_template_env.from_string(template).render(variables)
if result == u"False":
result = u""
return result
except Exception:
_logger.exception("failed to render mako template value %r", template)
return u""
results = dict.fromkeys(res_ids, u"")
def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
# try to load the template
try:
template = mako_template_env.from_string(tools.ustr(template))
except Exception:
_logger.exception("Failed to load template %r", template)
return results
# prepare template variables
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None]
variables = {
'user': user,
'ctx': context, # context kw would clash with mako internals
}
for record in records:
res_id = record.id if record else None
variables['object'] = record
try:
render_result = template.render(variables)
except Exception:
_logger.exception("Failed to render template %r using values %r" % (template, variables))
render_result = u""
if render_result == u"False":
render_result = u""
results[res_id] = render_result
return results
def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
if context is None:
context = {}
if res_ids is None:
res_ids = [None]
results = dict.fromkeys(res_ids, False)
if not template_id:
return False
return results
template = self.browse(cr, uid, template_id, context)
lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
if lang:
# Use translated template if necessary
ctx = context.copy()
ctx['lang'] = lang
template = self.browse(cr, uid, template.id, ctx)
else:
template = self.browse(cr, uid, int(template_id), context)
return template
langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context)
for res_id, lang in langs.iteritems():
if lang:
# Use translated template if necessary
ctx = context.copy()
ctx['lang'] = lang
template = self.browse(cr, uid, template.id, ctx)
else:
template = self.browse(cr, uid, int(template_id), context)
results[res_id] = template
return results
def onchange_model_id(self, cr, uid, ids, model_id, context=None):
mod_name = False
@ -300,64 +314,75 @@ class email_template(osv.osv):
})
return {'value': result}
def generate_email(self, cr, uid, template_id, res_id, context=None):
"""Generates an email from the template for given (model, res_id) pair.
def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
"""Generates an email from the template for given the given model based on
records given by res_ids.
:param template_id: id of the template to render.
:param res_id: id of the record to use for rendering the template (model
is taken from template definition)
:returns: a dict containing all relevant fields for creating a new
mail.mail entry, with one extra key ``attachments``, in the
format expected by :py:meth:`mail_thread.message_post`.
:param template_id: id of the template to render.
:param res_id: id of the record to use for rendering the template (model
is taken from template definition)
:returns: a dict containing all relevant fields for creating a new
mail.mail entry, with one extra key ``attachments``, in the
format expected by :py:meth:`mail_thread.message_post`.
"""
if context is None:
context = {}
if fields is None:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']
report_xml_pool = self.pool.get('ir.actions.report.xml')
template = self.get_email_template(cr, uid, template_id, res_id, context)
values = {}
for field in ['subject', 'body_html', 'email_from',
'email_to', 'partner_to', 'email_cc', 'reply_to']:
values[field] = self.render_template(cr, uid, getattr(template, field),
template.model, res_id, context=context) \
or False
if template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context)
if values['body_html']:
values['body'] = tools.html_sanitize(values['body_html'])
# templates: res_id -> template; template -> res_ids
templates_to_res_ids = {}
for res_id, template in res_ids_to_templates.iteritems():
templates_to_res_ids.setdefault(template, []).append(res_id)
values.update(mail_server_id=template.mail_server_id.id or False,
auto_delete=template.auto_delete,
model=template.model,
res_id=res_id or False)
results = dict()
for template, template_res_ids in templates_to_res_ids.iteritems():
# generate fields value for all res_ids linked to the current template
for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']:
generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context)
for res_id, field_value in generated_field_values.iteritems():
results.setdefault(res_id, dict())[field] = field_value
# update values for all res_ids
for res_id in template_res_ids:
values = results[res_id]
if template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
if values['body_html']:
values['body'] = tools.html_sanitize(values['body_html'])
values.update(
mail_server_id=template.mail_server_id.id or False,
auto_delete=template.auto_delete,
model=template.model,
res_id=res_id or False,
attachment_ids=[attach.id for attach in template.attachment_ids],
)
attachments = []
# Add report in attachments
if template.report_template:
report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
# Ensure report is rendered using template's language
ctx = context.copy()
if template.lang:
ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
result = base64.b64encode(result)
if not report_name:
report_name = 'report.' + report_service
ext = "." + format
if not report_name.endswith(ext):
report_name += ext
attachments.append((report_name, result))
# Add report in attachments
if template.report_template:
for res_id in template_res_ids:
attachments = []
report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
# Ensure report is rendered using template's language
ctx = context.copy()
if template.lang:
ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, res_id, context) # take 0 ?
result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
result = base64.b64encode(result)
if not report_name:
report_name = 'report.' + report_service
ext = "." + format
if not report_name.endswith(ext):
report_name += ext
attachments.append((report_name, result))
attachment_ids = []
# Add template attachments
for attach in template.attachment_ids:
attachment_ids.append(attach.id)
values['attachments'] = attachments
values['attachments'] = attachments
values['attachment_ids'] = attachment_ids
return values
return results
def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None):
"""Generates a new mail message for the given template and record,
@ -404,4 +429,14 @@ class email_template(osv.osv):
mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context)
return msg_id
# Compatibility method
def render_template(self, cr, uid, template, model, res_id, context=None):
return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id]
def generate_email(self, cr, uid, template_id, res_id, context=None):
return self.generate_email_batch(cr, uid, template_id, [res_id], context)[res_id]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -62,6 +62,7 @@ class mail_compose_message(osv.TransientModel):
for wizard in self.browse(cr, uid, ids, context=context):
if wizard.template_id:
wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html
wizard_context['mail_auto_delete'] = wizard.template_id.auto_delete # mass mailing: use template auto_delete value -> note, for emails mass mailing only
if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
continue
new_attachment_ids = []
@ -81,7 +82,7 @@ class mail_compose_message(osv.TransientModel):
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:
values = self.generate_email_for_composer(cr, uid, template_id, res_id, context=context)
values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
values['attachment_ids'] = values.pop('attachment_ids', [])
@ -147,45 +148,55 @@ class mail_compose_message(osv.TransientModel):
partner_ids.append(int(partner_id))
return partner_ids
def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
# filter template values
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
values['body'] = values.pop('body_html', '')
values = dict.fromkeys(res_ids, False)
# transform email_to, email_cc into partner_ids
ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
partner_ids = self._get_or_create_partners_from_values(cr, uid, values, context=ctx)
# legacy template behavior: void values do not erase existing values and the
# related key is removed from the values dict
if partner_ids:
values['partner_ids'] = list(partner_ids)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context)
for res_id in res_ids:
res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field))
res_id_values['body'] = res_id_values.pop('body_html', '')
# transform email_to, email_cc into partner_ids
ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx)
# legacy template behavior: void values do not erase existing values and the
# related key is removed from the values dict
if partner_ids:
res_id_values['partner_ids'] = list(partner_ids)
values[res_id] = res_id_values
return values
def render_message(self, cr, uid, wizard, res_id, context=None):
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override to handle templates. """
# generate the composer email
# generate template-based values
if wizard.template_id:
values = self.generate_email_for_composer(cr, uid, wizard.template_id.id, res_id, context=context)
template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context)
else:
values = {}
# remove attachments as they should not be rendered
values.pop('attachment_ids', None)
# get values to return
email_dict = super(mail_compose_message, self).render_message(cr, uid, wizard, res_id, context)
# those values are not managed; they are readonly
email_dict.pop('email_to', None)
email_dict.pop('email_cc', None)
email_dict.pop('partner_to', None)
# update template values by wizard values
values.update(email_dict)
return values
template_values = dict.fromkeys(res_ids, dict())
# generate composer values
composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
def render_template(self, cr, uid, template, model, res_id, context=None):
return self.pool.get('email.template').render_template(cr, uid, template, model, res_id, context=context)
for res_id in res_ids:
# remove attachments from template values as they should not be rendered
template_values[res_id].pop('attachment_ids', None)
# remove some keys from composer that are readonly
composer_values[res_id].pop('email_to', None)
composer_values[res_id].pop('email_cc', None)
composer_values[res_id].pop('partner_to', None)
# update template values by composer values
template_values[res_id].update(composer_values[res_id])
return template_values
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context)
# Compatibility methods
def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
return self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context)[res_id]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -233,7 +233,11 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
ir_attachment_obj = self.pool.get('ir.attachment')
# 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)
@ -252,43 +256,11 @@ class mail_compose_message(osv.TransientModel):
else:
res_ids = [wizard.res_id]
for res_id in res_ids:
# mail.message values, according to the wizard options
post_values = {
'subject': wizard.subject,
'body': wizard.body,
'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],
}
# mass mailing: render and override default values
if mass_mail_mode and wizard.model:
email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
post_values['partner_ids'] += email_dict.pop('partner_ids', [])
post_values['attachments'] = email_dict.pop('attachments', [])
attachment_ids = []
for attach_id in post_values.pop('attachment_ids'):
new_attach_id = ir_attachment_obj.copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
attachment_ids.append(new_attach_id)
post_values['attachment_ids'] = attachment_ids
# email_from: mass mailing only can specify another email_from
if email_dict.get('email_from'):
post_values['email_from'] = email_dict.pop('email_from')
# replies redirection: mass mailing only
if not wizard.same_thread:
post_values['reply_to'] = email_dict.pop('reply_to')
else:
email_dict.pop('reply_to')
post_values.update(email_dict)
# 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)
# post the message
all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
for res_id, mail_values in all_mail_values.iteritems():
if mass_mail_mode and not wizard.post:
post_values['body_html'] = post_values.get('body', '')
post_values['recipient_ids'] = [(4, id) for id in post_values.pop('partner_ids', [])]
self.pool.get('mail.mail').create(cr, uid, post_values, context=context)
self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
else:
subtype = 'mail.mt_comment'
if is_log: # log a note: subtype is False
@ -297,46 +269,122 @@ class mail_compose_message(osv.TransientModel):
if not wizard.notify:
subtype = False
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, **post_values)
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)
return {'type': 'ir.actions.act_window_close'}
def render_message(self, cr, uid, wizard, res_id, context=None):
""" Generate an email from the template for given (wizard.model, res_id)
pair. This method is meant to be inherited by email_template that
will produce a more complete dictionary. """
return {
'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
'email_from': self.render_template(cr, uid, wizard.email_from, wizard.model, res_id, context),
'reply_to': self.render_template(cr, uid, wizard.reply_to, wizard.model, res_id, context),
}
def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
"""Generate the values that will be used by send_mail to create mail_messages
or mail_mails. """
results = dict.fromkeys(res_ids, False)
mass_mail_mode = wizard.composition_mode == 'mass_mail'
def render_template(self, cr, uid, template, model, res_id, context=None):
# render all template-based value at once
if mass_mail_mode and wizard.model:
rendered_values = self.render_message_batch(cr, uid, wizard, res_ids, context=context)
for res_id in res_ids:
# static wizard (mail.message) values
mail_values = {
'subject': wizard.subject,
'body': wizard.body,
'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],
}
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
email_dict = rendered_values[res_id]
mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
mail_values['attachments'] = email_dict.pop('attachments', [])
attachment_ids = []
for attach_id in mail_values.pop('attachment_ids'):
new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
attachment_ids.append(new_attach_id)
mail_values['attachment_ids'] = attachment_ids
# email_from: mass mailing only can specify another email_from
if email_dict.get('email_from'):
mail_values['email_from'] = email_dict.pop('email_from')
# replies redirection: mass mailing only
if not wizard.same_thread:
mail_values['reply_to'] = email_dict.pop('reply_to')
else:
email_dict.pop('reply_to')
mail_values.update(email_dict)
# mass mailing without post: mail_mail values
if mass_mail_mode and not wizard.post:
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
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
will produce a more complete dictionary, using Jinja2 templates.
Each template is generated for all res_ids, allowing to parse the template
once, and render it multiple times. This is useful for mass mailing where
template rendering represent a significant part of the process.
:param browse wizard: current mail.compose.message browse record
:param list res_ids: list of record ids
:return dict results: for each res_id, the generated template values for
subject, body, email_from and reply_to
"""
subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context)
bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context)
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context)
results = dict.fromkeys(res_ids, False)
for res_id in res_ids:
results[res_id] = {
'subject': subjects[res_id],
'body': bodies[res_id],
'email_from': emails_from[res_id],
'reply_to': replies_to[res_id],
}
return results
def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
""" Render the given template text, replace mako-like expressions ``${expr}``
with the result of evaluating these expressions with an evaluation context
containing:
with the result of evaluating these expressions with an evaluation context
containing:
* ``user``: browse_record of the current user
* ``object``: browse_record of the document record this mail is
related to
* ``context``: the context passed to the mail composition wizard
* ``user``: browse_record of the current user
* ``object``: browse_record of the document record this mail is
related to
* ``context``: the context passed to the mail composition wizard
:param str template: the template text to render
:param str model: model name of the document record this mail is related to.
:param int res_id: id of the document record this mail is related to.
:param str template: the template text to render
:param str model: model name of the document record this mail is related to
:param list res_ids: list of record ids
"""
if context is None:
context = {}
results = dict.fromkeys(res_ids, False)
def merge(match):
exp = str(match.group()[2:-1]).strip()
result = eval(exp, {
'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object': self.pool[model].browse(cr, uid, res_id, context=context),
'context': dict(context), # copy context to prevent side-effects of eval
for res_id in res_ids:
def merge(match):
exp = str(match.group()[2:-1]).strip()
result = eval(exp, {
'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object': self.pool[model].browse(cr, uid, res_id, context=context),
'context': dict(context), # copy context to prevent side-effects of eval
})
return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template)
return result and tools.ustr(result) or ''
results[res_id] = template and EXPRESSION_PATTERN.sub(merge, template)
return results
# Compatibility methods
def render_template(self, cr, uid, template, model, res_id, context=None):
return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
def render_message(self, cr, uid, wizard, res_id, context=None):
return self.render_message_batch(cr, uid, wizard, [res_id], context)[res_id]