[LINT] mail, various: perform a quick code linting

Purpose is to try to have code easier to read and to update by having a
common way of sorting / writing code. Reorder some fields definitions and
dictionaries, update docstrings, ... in mail applications or when invoking
mail thread API.

Additional stuff worth noting here

  * add some tagged in tests helping debugging / choosing tests to execute;
  * in a test about mail generation with server action: correctly check
    body_html field, not body which comes from the mail.message inheritance
    (currently filled due to a side effect but actual field to check is the
    html one);
  * add same check on input for ``_render_template_qweb`` as done on other
    rendering methods (even if rendering on [False] should be supported);
  * move code translation update from specific tests into main test class
    (code update due to new translations, followup of odoo/odoo@f8c2b02abe);

Some test linting done here

  * reorder mail_render tests, remove some duplicated tests;
  * reorder mail_template tests, remove some duplicated tests;

Task-2710804 (Mail: Clean MailThread Posting API)

closes odoo/odoo#106025

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Thibault Delavallée
2022-11-18 14:00:30 +01:00
parent 9f193df0fa
commit 709a65997d
18 changed files with 357 additions and 294 deletions
@@ -41,10 +41,10 @@ class ApplicantSendMail(models.TransientModel):
})
applicant.message_post(
subject=self.subject,
body=self.body,
message_type='comment',
email_from=self.author_id.email,
email_layout_xmlid='mail.mail_notification_light',
message_type='comment',
partner_ids=applicant.partner_id.ids,
subject=self.subject,
)
+14 -5
View File
@@ -13,7 +13,7 @@ from werkzeug import urls
from odoo import _, api, fields, models, tools
from odoo.addons.base.models.ir_qweb import QWebException
from odoo.exceptions import UserError, AccessError
from odoo.tools import is_html_empty, safe_eval
from odoo.tools import is_html_empty
from odoo.tools.rendering_tools import convert_inline_template_to_qweb, parse_inline_template, render_inline_template, template_env_globals
_logger = logging.getLogger(__name__)
@@ -273,6 +273,10 @@ class MailRenderMixin(models.AbstractModel):
add_context=None, options=None):
""" Render a raw QWeb template.
In addition to the generic evaluation context available, some other
variables are added:
* ``object``: record based on which the template is rendered;
:param str template_src: raw QWeb template to render;
:param str model: see ``MailRenderMixin._render_template()``;
:param list res_ids: see ``MailRenderMixin._render_template()``;
@@ -283,8 +287,6 @@ class MailRenderMixin(models.AbstractModel):
:param dict options: options for rendering (not used currently);
:return dict: {res_id: string of rendered template based on record}
:notice: Experimental. Use at your own risks only.
"""
results = dict.fromkeys(res_ids, u"")
if not template_src:
@@ -473,6 +475,7 @@ class MailRenderMixin(models.AbstractModel):
else:
rendered = self._render_template_inline_template(template_src, model, res_ids,
add_context=add_context, options=options)
if post_process:
rendered = self._render_template_postprocess(rendered)
@@ -543,6 +546,7 @@ class MailRenderMixin(models.AbstractModel):
:param string set_lang: force language for rendering. It should be a
valid lang code matching an activate res.lang. Checked only if
``compute_lang`` is False;
:param dict add_context: additional context to give to renderer;
:param dict options: options for rendering;
:param boolean post_process: perform a post processing on rendered result
@@ -570,7 +574,12 @@ class MailRenderMixin(models.AbstractModel):
(res_id, rendered)
for lang, (template, tpl_res_ids) in templates_res_ids.items()
for res_id, rendered in template._render_template(
template[field], template.render_model, tpl_res_ids, engine=engine,
add_context=add_context, options=options, post_process=post_process
template[field],
template.render_model,
tpl_res_ids,
engine=engine,
add_context=add_context,
options=options,
post_process=post_process
).items()
)
+9 -3
View File
@@ -334,10 +334,16 @@ class MailTemplate(models.Model):
# create a mail_mail based on values, without attachments
values = self.generate_email(
res_id,
['subject', 'body_html',
('auto_delete',
'body_html',
'email_cc',
'email_from',
'email_cc', 'email_to', 'partner_to', 'reply_to',
'auto_delete', 'scheduled_date']
'email_to',
'partner_to',
'reply_to',
'scheduled_date',
'subject',
)
)
values['recipient_ids'] = [Command.link(pid) for pid in values.get('partner_ids', list())]
values['attachment_ids'] = [Command.link(aid) for aid in values.get('attachment_ids', list())]
+42 -28
View File
@@ -1977,9 +1977,11 @@ class MailThread(models.AbstractModel):
if not msg_values.get('record_name'):
msg_values['record_name'] = self.display_name
msg_values.update({
# author
'author_id': author_id,
'author_guest_id': author_guest_id,
'email_from': email_from,
# document
'model': self._name,
'res_id': self.id,
# content
@@ -2118,18 +2120,22 @@ class MailThread(models.AbstractModel):
res_id = False
msg_values = {
'parent_id': parent_id,
'model': self._name if self else model,
'res_id': self.id if self else res_id,
'message_type': 'user_notification',
'subject': subject,
'body': body,
# author
'author_id': author_id,
'email_from': email_from,
'partner_ids': partner_ids,
'is_internal': True,
# document
'model': self._name if self else model,
'record_name': False,
'res_id': self.id if self else res_id,
# content
'body': body,
'is_internal': True,
'message_type': 'user_notification',
'parent_id': parent_id,
'subject': subject,
# recipients
'message_id': tools.generate_tracking_message_id('message-notify'),
'partner_ids': partner_ids,
}
msg_values.update(msg_kwargs)
# add default-like values afterwards, to avoid useless queries
@@ -2159,19 +2165,23 @@ class MailThread(models.AbstractModel):
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False)
msg_values = {
'subject': subject,
'body': body,
# author
'author_id': author_id,
'email_from': email_from,
'message_type': message_type,
# document
'model': kwargs.get('model', self._name),
'res_id': self.ids[0] if self.ids else False,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
'res_id': self.ids[0] if self.ids else False,
# content
'body': body,
'is_internal': True,
'message_type': message_type,
'subject': subject,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
# recipients
'email_add_signature': False, # False as no notification -> no need to compute signature
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
}
msg_values.update(kwargs)
@@ -2186,17 +2196,21 @@ class MailThread(models.AbstractModel):
author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False)
base_message_values = {
'subject': subject,
# author
'author_id': author_id,
'email_from': email_from,
'message_type': message_type,
# document
'model': self._name,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
# content
'message_type': message_type,
'is_internal': True,
'subject': subject,
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
# recipients
'email_add_signature': False,
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
}
values_list = [dict(base_message_values,
res_id=record.id,
@@ -2408,9 +2422,9 @@ class MailThread(models.AbstractModel):
notif_create_values = [{
'author_id': message.author_id.id,
'mail_message_id': message.id,
'res_partner_id': pid,
'notification_type': 'inbox',
'notification_status': 'sent',
'notification_type': 'inbox',
'res_partner_id': pid,
} for pid in inbox_pids]
self.env['mail.notification'].sudo().create(notif_create_values)
@@ -2531,12 +2545,12 @@ class MailThread(models.AbstractModel):
})
notif_create_values += [{
'author_id': message.author_id.id,
'mail_message_id': message.id,
'res_partner_id': recipient_id,
'notification_type': 'email',
'mail_mail_id': new_email.id,
'is_read': True, # discard Inbox notification
'mail_mail_id': new_email.id,
'mail_message_id': message.id,
'notification_status': 'ready',
'notification_type': 'email',
'res_partner_id': recipient_id,
} for recipient_id in tocreate_recipient_ids]
emails += new_email
+10 -2
View File
@@ -20,6 +20,7 @@ from odoo.addons.mail.models.mail_message import Message
from odoo.addons.mail.models.mail_notification import MailNotification
from odoo.tests import common, new_test_user
from odoo.tools import formataddr, pycompat
from odoo.tools.translate import code_translations
mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True,
'mail_create_nosubscribe': True,
@@ -1119,9 +1120,16 @@ class MailCommon(common.TransactionCase, MailCase):
if test_record:
cls.env['ir.model']._get(test_record._name).with_context(lang=lang_code).name = 'Spanish description'
# Translate some code strings used in mailing
code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'}
cls.addClassCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES'))
code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'}
cls.addClassCleanup(code_translations.python_translations.pop, ('mail', 'es_ES'))
# Prepare some translated value for template if given
test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}'
test_template.with_context(lang=lang_code).body_html = '<p>SpanishBody for <t t-out="object.name" /></p>'
if test_template:
test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}'
test_template.with_context(lang=lang_code).body_html = '<p>SpanishBody for <t t-out="object.name" /></p>'
# create a custom layout for email notification
if not layout_arch_db:
+147 -129
View File
@@ -8,12 +8,11 @@ from odoo.exceptions import AccessError
from odoo.tests import tagged, users
@tagged('mail_render')
class TestMailRender(common.MailCommon):
class TestMailRenderCommon(common.MailCommon):
@classmethod
def setUpClass(cls):
super(TestMailRender, cls).setUpClass()
super(TestMailRenderCommon, cls).setUpClass()
# activate multi language support
cls.env['res.lang']._activate_lang('fr_FR')
@@ -40,10 +39,7 @@ class TestMailRender(common.MailCommon):
<p>{{ 13 + 13 }}</p>
<h1>This is a test</h1>
""",
"""
<b>Test</b>
{{ '' if True else '<b>Code not executed</b>' }}
""",
"""<b>Test</b>{{ '' if True else '<b>Code not executed</b>' }}""",
]
cls.base_inline_template_bits_fr = [
'<p>Bonjour</p>',
@@ -136,6 +132,10 @@ class TestMailRender(common.MailCommon):
)
cls.user_rendering_restricted.groups_id -= cls.env.ref('mail.group_mail_template_editor')
@tagged('mail_render')
class TestMailRender(TestMailRenderCommon):
@users('employee')
def test_evaluation_context(self):
""" Test evaluation context and various ways of tweaking it. """
@@ -164,6 +164,15 @@ class TestMailRender(common.MailCommon):
)[partner.id]
self.assertEqual(expected, result)
@users('employee')
def test_prepend_preview_inline_template_to_qweb(self):
body = 'body'
preview = 'foo{{"false" if 1 > 2 else "true"}}bar'
result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview)
self.assertEqual(result, '''<div style="display:none;font-size:1px;height:0px;width:0px;opacity:0;">
foo<t t-out="&#34;false&#34; if 1 &gt; 2 else &#34;true&#34;"/>bar
</div>body''')
@users('employee')
def test_render_field(self):
template = self.env['mail.template'].browse(self.test_template.ids)
@@ -176,6 +185,10 @@ class TestMailRender(common.MailCommon):
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_field_lang(self):
""" Test translation in french """
template = self.env['mail.template'].browse(self.test_template.ids)
partner = self.env['res.partner'].browse(self.render_object_fr.ids)
for fname, expected in zip(['subject', 'body_html'], self.base_rendered_fr):
rendered = template._render_field(
@@ -197,23 +210,6 @@ class TestMailRender(common.MailCommon):
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_local_links(self):
local_links_template_bits = [
'<div style="background-image:url(/web/path?a=a&b=b);"/>',
'<div style="background-image:url(\'/web/path?a=a&b=b\');"/>',
'<div style="background-image:url(&#34;/web/path?a=a&b=b&#34;);"/>',
]
base_url = self.env['mail.render.mixin'].get_base_url()
rendered_local_links = [
'<div style="background-image:url(%s/web/path?a=a&b=b);"/>' % base_url,
'<div style="background-image:url(\'%s/web/path?a=a&b=b\');"/>' % base_url,
'<div style="background-image:url(&#34;%s/web/path?a=a&b=b&#34;);"/>' % base_url
]
for source, expected in zip(local_links_template_bits, rendered_local_links):
rendered = self.env['mail.render.mixin']._replace_local_links(source)
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_qweb(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
@@ -239,77 +235,7 @@ class TestMailRender(common.MailCommon):
self.assertEqual(rendered, expected)
@users('employee')
def test_template_rendering_impersonate(self):
""" Test that the use of SUDO do not change the current user. """
partner = self.env['res.partner'].browse(self.render_object.ids)
src = '{{ user.name }} - {{ object.name }}'
expected = '%s - %s' % (self.env.user.name, partner.name)
result = self.env['mail.render.mixin'].sudo()._render_template_inline_template(
src, partner._name, partner.ids
)[partner.id]
self.assertIn(expected, result)
@users('user_rendering_restricted')
def test_template_rendering_function_call(self):
"""Test the case when the template call a custom function.
This function should not be called when the template is not rendered.
"""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
MailRenderMixin = self.env['mail.render.mixin']
def cust_function():
# Can not use "MagicMock" in a Jinja sand-boxed environment
# so create our own function
cust_function.call = True
return 'return value'
cust_function.call = False
src = """<h1>This is a test</h1>
<p>{{ cust_function() }}</p>"""
expected = """<h1>This is a test</h1>
<p>return value</p>"""
context = {'cust_function': cust_function}
result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template(
src, partner._name, partner.ids,
add_context=context
)[partner.id]
self.assertEqual(expected, result)
self.assertTrue(cust_function.call)
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context)
@users('user_rendering_restricted')
def test_template_render_static(self):
"""Test that we render correctly static templates (without placeholders)."""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
MailRenderMixin = self.env['mail.render.mixin']
result = MailRenderMixin._render_template_inline_template(self.base_inline_template_bits[0], model, res_ids)[res_ids[0]]
self.assertEqual(result, self.base_inline_template_bits[0])
@users('user_rendering_restricted')
def test_template_rendering_restricted(self):
"""Test if we correctly detect static template."""
res_ids = self.env['res.partner'].search([], limit=1).ids
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids)
@users('employee')
def test_template_rendering_unrestricted(self):
"""Test if we correctly detect static template."""
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids)[res_ids[0]]
self.assertIn('26', result, 'Template Editor should be able to render inline_template code')
@users('employee')
def test_template_rendering_various(self):
def test_render_template_various(self):
""" Test static rendering """
partner = self.env['res.partner'].browse(self.render_object.ids)
MailRenderMixin = self.env['mail.render.mixin']
@@ -376,6 +302,131 @@ class TestMailRender(common.MailCommon):
)[partner.id]
self.assertEqual(result, expected)
@users('employee')
def test_replace_local_links(self):
local_links_template_bits = [
'<div style="background-image:url(/web/path?a=a&b=b);"/>',
'<div style="background-image:url(\'/web/path?a=a&b=b\');"/>',
'<div style="background-image:url(&#34;/web/path?a=a&b=b&#34;);"/>',
]
base_url = self.env['mail.render.mixin'].get_base_url()
rendered_local_links = [
'<div style="background-image:url(%s/web/path?a=a&b=b);"/>' % base_url,
'<div style="background-image:url(\'%s/web/path?a=a&b=b\');"/>' % base_url,
'<div style="background-image:url(&#34;%s/web/path?a=a&b=b&#34;);"/>' % base_url
]
for source, expected in zip(local_links_template_bits, rendered_local_links):
rendered = self.env['mail.render.mixin']._replace_local_links(source)
self.assertEqual(rendered, expected)
@tagged('mail_render')
class TestMailRenderSecurity(TestMailRenderCommon):
""" Test security of rendering, based on qweb finding + restricted rendering
group usage. """
@users('employee')
def test_render_inline_template_impersonate(self):
""" Test that the use of SUDO do not change the current user. """
partner = self.env['res.partner'].browse(self.render_object.ids)
src = '{{ user.name }} - {{ object.name }}'
expected = '%s - %s' % (self.env.user.name, partner.name)
result = self.env['mail.render.mixin'].sudo()._render_template_inline_template(
src, partner._name, partner.ids
)[partner.id]
self.assertIn(expected, result)
@users('user_rendering_restricted')
def test_render_inline_template_restricted(self):
"""Test if we correctly detect static template."""
res_ids = self.env['res.partner'].search([], limit=1).ids
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
self.env['mail.render.mixin']._render_template_inline_template(
self.base_inline_template_bits[3],
'res.partner',
res_ids
)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_inline_template(
src,
'res.partner',
res_ids
)[res_ids[0]]
self.assertEqual(src, str(result))
@users('user_rendering_restricted')
def test_render_inline_template_restricted_static(self):
"""Test that we render correctly static templates (without placeholders)."""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
MailRenderMixin = self.env['mail.render.mixin']
result = MailRenderMixin._render_template_inline_template(
self.base_inline_template_bits[0],
model,
res_ids
)[res_ids[0]]
self.assertEqual(result, self.base_inline_template_bits[0])
@users('employee')
def test_render_inline_template_unrestricted(self):
""" Test if we correctly detect static template. """
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_inline_template(
self.base_inline_template_bits[3],
'res.partner',
res_ids
)[res_ids[0]]
self.assertIn('26', result, 'Template Editor should be able to render inline_template code')
@users('user_rendering_restricted')
def test_render_template_qweb_restricted(self):
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[
partner.id]
self.assertEqual(src, str(result))
@users('user_rendering_restricted')
def test_security_function_call(self):
"""Test the case when the template call a custom function.
This function should not be called when the template is not rendered.
"""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
MailRenderMixin = self.env['mail.render.mixin']
def cust_function():
# Can not use "MagicMock" in a Jinja sand-boxed environment
# so create our own function
cust_function.call = True
return 'return value'
cust_function.call = False
src = """<h1>This is a test</h1>
<p>{{ cust_function() }}</p>"""
expected = """<h1>This is a test</h1>
<p>return value</p>"""
context = {'cust_function': cust_function}
result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template(
src, partner._name, partner.ids,
add_context=context
)[partner.id]
self.assertEqual(expected, result)
self.assertTrue(cust_function.call)
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context)
@users('user_rendering_restricted')
def test_security_inline_template_restricted(self):
"""Test if we correctly detect condition block (which might contains code)."""
@@ -384,7 +435,7 @@ class TestMailRender(common.MailCommon):
self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)
@users('employee')
def test_is_inline_template_condition_block_unrestricted(self):
def test_security_inline_template_unrestricted(self):
"""Test if we correctly detect condition block (which might contains code)."""
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)[res_ids[0]]
@@ -417,36 +468,3 @@ class TestMailRender(common.MailCommon):
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids)[res_ids[0]]
self.assertNotIn('Code not executed', result, 'The condition block did not work')
@users('user_rendering_restricted')
def test_template_rendering_static_inline_template(self):
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_inline_template(
src, model, res_ids)[partner.id]
self.assertEqual(src, str(result))
@users('user_rendering_restricted')
def test_template_rendering_static_qweb(self):
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[
partner.id]
self.assertEqual(src, str(result))
@users('employee')
def test_prepend_preview_inline_template_to_qweb(self):
body = 'body'
preview = 'foo{{"false" if 1 > 2 else "true"}}bar'
result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview)
self.assertEqual(result, '''<div style="display:none;font-size:1px;height:0px;width:0px;opacity:0;">
foo<t t-out="&#34;false&#34; if 1 &gt; 2 else &#34;true&#34;"/>bar
</div>body''')
+3 -1
View File
@@ -5,10 +5,11 @@ from markupsafe import Markup
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError
from odoo.modules.module import get_module_resource
from odoo.tests import Form, users
from odoo.tests import Form, tagged, users
from odoo.tools import convert_file
@tagged('mail_template')
class TestMailTemplate(MailCommon):
@classmethod
@@ -147,6 +148,7 @@ class TestMailTemplate(MailCommon):
self.assertFalse(server.active)
@tagged('mail_template')
class TestMailTemplateReset(MailCommon):
def _load(self, module, *args):
+45 -23
View File
@@ -387,18 +387,18 @@ class MailComposer(models.TransientModel):
for res_id in res_ids:
# static wizard (mail.message) values
mail_values = {
'subject': self.subject,
'body': self.body or '',
'parent_id': self.parent_id and self.parent_id.id,
'partner_ids': [partner.id for partner in self.partner_ids],
'attachment_ids': [attach.id for attach in self.attachment_ids],
'author_id': self.author_id.id,
'body': self.body or '',
'email_from': self.email_from,
'record_name': self.record_name,
'reply_to_force_new': self.reply_to_force_new,
'mail_server_id': self.mail_server_id.id,
'mail_activity_type_id': self.mail_activity_type_id.id,
'message_type': 'email' if mass_mail_mode else self.message_type,
'parent_id': self.parent_id.id,
'partner_ids': self.partner_ids.ids,
'record_name': self.record_name,
'reply_to_force_new': self.reply_to_force_new,
'subject': self.subject,
}
# mass mailing: rendering override wizard static values
@@ -554,10 +554,12 @@ class MailComposer(models.TransientModel):
template = self.env['mail.template'].browse(template_id)
values = dict(
(field, template[field])
for field in ['subject', 'body_html',
for field in ('body_html',
'email_from',
'mail_server_id',
'reply_to',
'mail_server_id']
'subject',
)
if template[field]
)
if template.attachment_ids:
@@ -567,11 +569,16 @@ class MailComposer(models.TransientModel):
elif template_id:
values = self.generate_email_for_composer(
template_id, [res_id],
['subject', 'body_html',
('attachment_ids',
'body_html',
'email_cc',
'email_from',
'email_cc', 'email_to', 'partner_to', 'reply_to',
'attachment_ids', 'mail_server_id'
]
'email_to',
'mail_server_id',
'partner_to',
'reply_to',
'subject',
)
)[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
@@ -593,19 +600,28 @@ class MailComposer(models.TransientModel):
default_composition_mode=composition_mode,
default_model=model,
default_res_id=res_id
).default_get(['composition_mode', 'model', 'res_id', 'parent_id',
'subject', 'body',
).default_get(['attachment_ids',
'body',
'composition_mode',
'email_from',
'partner_ids', 'reply_to',
'attachment_ids', 'mail_server_id'
'mail_server_id',
'model',
'parent_id',
'partner_ids',
'reply_to',
'res_id',
'subject',
])
values = dict(
(key, default_values[key])
for key in ['subject', 'body',
for key in ('attachment_ids',
'body',
'email_from',
'partner_ids', 'reply_to',
'attachment_ids', 'mail_server_id'
] if key in default_values)
'mail_server_id',
'partner_ids',
'reply_to',
'subject',
) if key in default_values)
if values.get('body_html'):
values['body'] = values.pop('body_html')
@@ -653,10 +669,10 @@ class MailComposer(models.TransientModel):
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],
'subject': subjects[res_id],
}
results[res_id].update(default_recipients.get(res_id, dict()))
@@ -664,7 +680,13 @@ class MailComposer(models.TransientModel):
if self.template_id:
template_values = self.generate_email_for_composer(
self.template_id.id, res_ids,
['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'])
('attachment_ids',
'email_to',
'email_cc',
'mail_server_id',
'partner_to',
)
)
else:
template_values = {}
@@ -692,7 +714,7 @@ class MailComposer(models.TransientModel):
multi_mode = False
res_ids = [res_ids]
returned_fields = fields + ['partner_ids', 'attachments']
returned_fields = list(fields) + ['partner_ids', 'attachments']
values = dict.fromkeys(res_ids, False)
template_values = self.env['mail.template'].with_context(tpl_partners_only=True).browse(template_id).generate_email(res_ids, fields)
+12 -3
View File
@@ -8,8 +8,15 @@ from odoo.exceptions import UserError
class MailTemplatePreview(models.TransientModel):
_name = 'mail.template.preview'
_description = 'Email Template Preview'
_MAIL_TEMPLATE_FIELDS = ['subject', 'body_html', 'email_from', 'email_to',
'email_cc', 'reply_to', 'scheduled_date', 'attachment_ids']
_MAIL_TEMPLATE_FIELDS = ['attachment_ids',
'body_html',
'subject',
'email_cc',
'email_from',
'email_to',
'reply_to',
'scheduled_date',
]
@api.model
def _selection_target_model(self):
@@ -70,7 +77,9 @@ class MailTemplatePreview(models.TransientModel):
else:
copy_depends_values['resource_ref'] = '%s,%s' % (self.resource_ref._name, self.resource_ref.id)
mail_values = mail_template.with_context(template_preview_lang=self.lang).generate_email(
self.resource_ref.id, self._MAIL_TEMPLATE_FIELDS)
self.resource_ref.id,
self._MAIL_TEMPLATE_FIELDS
)
self._set_mail_attributes(values=mail_values)
self.error_msg = False
except UserError as user_error:
+8 -8
View File
@@ -1047,16 +1047,16 @@ class MassMailing(models.Model):
'author_id': author_id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
'body': mailing._prepend_preview(mailing.body_html, mailing.preview),
'subject': mailing.subject,
'model': mailing.mailing_model_real,
'email_from': mailing.email_from,
'record_name': False,
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
'reply_to_force_new': mailing.reply_to_mode == 'new',
'template_id': None,
'email_from': mailing.email_from,
'mail_server_id': mailing.mail_server_id.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
'mass_mailing_id': mailing.id,
'model': mailing.mailing_model_real,
'record_name': False,
'reply_to_force_new': mailing.reply_to_mode == 'new',
'subject': mailing.subject,
'template_id': None,
}
if mailing.reply_to_mode == 'new':
composer_values['reply_to'] = mailing.reply_to
@@ -23,7 +23,6 @@ class MailComposeMessage(models.TransientModel):
""" Override method that generated the mail content by creating the
mailing.trace values in the o2m of mail_mail, when doing pure
email mass mailing. """
now = fields.Datetime.now()
self.ensure_one()
res = super(MailComposeMessage, self).get_mail_values(res_ids)
# use only for allowed models in mass mailing
@@ -32,37 +31,30 @@ class MailComposeMessage(models.TransientModel):
self.model_is_thread:
mass_mailing = self.mass_mailing_id
if not mass_mailing:
mass_mailing = self.env['mailing.mailing'].create({
'campaign_id': self.campaign_id.id,
'name': self.mass_mailing_name,
'subject': self.subject,
'state': 'done',
'reply_to_mode': self.reply_to_mode,
'reply_to': self.reply_to if self.reply_to_mode == 'new' else False,
'sent_date': now,
'body_html': self.body,
'mailing_model_id': self.env['ir.model']._get(self.model).id,
'mailing_domain': self.active_domain,
'attachment_ids': [(6, 0, self.attachment_ids.ids)],
})
mass_mailing = self.env['mailing.mailing'].create(
self._prepare_mailing_values()
)
self.mass_mailing_id = mass_mailing.id
recipients_info = self._process_recipient_values(res)
for res_id in res_ids:
mail_values = res[res_id]
if mail_values.get('body_html'):
body = self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout',
{'body': mail_values['body_html']},
minimal_qcontext=True, raise_if_not_found=False)
body = self.env['ir.qweb']._render(
'mass_mailing.mass_mailing_mail_layout',
{'body': mail_values['body_html']},
minimal_qcontext=True,
raise_if_not_found=False
)
if body:
mail_values['body_html'] = body
trace_vals = {
'model': self.model,
'res_id': res_id,
'mass_mailing_id': mass_mailing.id,
# if mail_to is void, keep falsy values to allow searching / debugging traces
'email': recipients_info[res_id]['mail_to'][0] if recipients_info[res_id]['mail_to'] else '',
'mass_mailing_id': mass_mailing.id,
'model': self.model,
'res_id': res_id,
}
# propagate failed states to trace when still-born
if mail_values.get('state') == 'cancel':
@@ -73,11 +65,11 @@ class MailComposeMessage(models.TransientModel):
trace_vals['failure_type'] = mail_values['failure_type']
mail_values.update({
'mailing_id': mass_mailing.id,
'mailing_trace_ids': [(0, 0, trace_vals)],
'auto_delete': not mass_mailing.keep_archives,
# email-mode: keep original message for routing
'is_notification': mass_mailing.reply_to_mode == 'update',
'auto_delete': not mass_mailing.keep_archives,
'mailing_id': mass_mailing.id,
'mailing_trace_ids': [(0, 0, trace_vals)],
})
return res
@@ -92,3 +84,19 @@ class MailComposeMessage(models.TransientModel):
if self.mass_mailing_id:
opt_out_list += self.mass_mailing_id._get_opt_out_list()
return opt_out_list
def _prepare_mailing_values(self):
now = fields.Datetime.now()
return {
'attachment_ids': [(6, 0, self.attachment_ids.ids)],
'body_html': self.body,
'campaign_id': self.campaign_id.id,
'mailing_model_id': self.env['ir.model']._get(self.model).id,
'mailing_domain': self.active_domain,
'name': self.mass_mailing_name,
'reply_to': self.reply_to if self.reply_to_mode == 'new' else False,
'reply_to_mode': self.reply_to_mode,
'sent_date': now,
'state': 'done',
'subject': self.subject,
}
+1 -1
View File
@@ -84,12 +84,12 @@ class SaleOrderCancel(models.TransientModel):
def action_send_mail_and_cancel(self):
self.ensure_one()
self.order_id.message_post(
subject=self.subject,
body=self.body,
message_type='comment',
email_from=self.email_from,
email_layout_xmlid='mail.mail_notification_light',
partner_ids=self.recipient_ids.ids,
subject=self.subject,
)
return self.action_cancel()
+4 -4
View File
@@ -213,14 +213,14 @@ class SurveyInvite(models.TransientModel):
body = self._render_field('body', answer.ids, post_process=True)[answer.id]
# post the message
mail_values = {
'email_from': self.email_from,
'attachment_ids': [(4, att.id) for att in self.attachment_ids],
'auto_delete': True,
'author_id': self.author_id.id,
'body_html': body,
'email_from': self.email_from,
'model': None,
'res_id': None,
'subject': subject,
'body_html': body,
'attachment_ids': [(4, att.id) for att in self.attachment_ids],
'auto_delete': True,
}
if answer.partner_id:
mail_values['recipient_ids'] = [(4, answer.partner_id.id)]
+1 -1
View File
@@ -41,7 +41,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')])
self.assertEqual(len(mail), 1)
self.assertTrue(mail.auto_delete)
self.assertEqual(mail.body, '<p>Hello TestingPartner</p>')
self.assertEqual(mail.body_html, '<p>Hello TestingPartner</p>')
self.assertFalse(mail.is_notification)
with self.mock_mail_gateway(mail_unlink_sent=True):
mail.send()
+16 -52
View File
@@ -64,9 +64,20 @@ class TestMailTemplateCommon(TestMailCommon, TestRecipients):
cls.test_template.invalidate_recordset(['attachment_ids'])
@tagged('mail_template', 'multi_lang')
@tagged('mail_template')
class TestMailTemplate(TestMailTemplateCommon):
def test_template_add_context_action(self):
self.test_template.create_action()
# check template act_window has been updated
self.assertTrue(bool(self.test_template.ref_ir_act_window))
# check those records
action = self.test_template.ref_ir_act_window
self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name)
self.assertEqual(action.binding_model_id.model, 'mail.test.lang')
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('employee')
def test_template_schedule_email(self):
@@ -93,6 +104,10 @@ class TestMailTemplate(TestMailTemplateCommon):
self.assertFalse(mail.scheduled_date)
self.assertEqual(mail.state, 'outgoing')
@tagged('mail_template', 'multi_lang')
class TestMailTemplateLanguages(TestMailTemplateCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_send_email(self):
mail_id = self.test_template.send_mail(self.test_record.id)
@@ -134,54 +149,3 @@ class TestMailTemplate(TestMailTemplateCommon):
self.assertEqual(mail.body_html,
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish description</body>' % self.test_record.name)
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
def test_template_add_context_action(self):
self.test_template.create_action()
# check template act_window has been updated
self.assertTrue(bool(self.test_template.ref_ir_act_window))
# check those records
action = self.test_template.ref_ir_act_window
self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name)
self.assertEqual(action.binding_model_id.model, 'mail.test.lang')
# def test_template_scheduled_date(self):
# from unittest.mock import patch
# self.email_template_in_2_days = self.email_template.copy()
# with patch('odoo.addons.mail.tests.test_mail_template.datetime', wraps=datetime) as mock_datetime:
# mock_datetime.now.return_value = datetime(2017, 11, 15, 11, 30, 28)
# mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw)
# self.email_template_in_2_days.write({
# 'scheduled_date': "{{ (datetime.datetime.now() + relativedelta(days=2)).strftime('%s') }}" % DEFAULT_SERVER_DATETIME_FORMAT,
# })
# mail_now_id = self.email_template.send_mail(self.test_record.id)
# mail_in_2_days_id = self.email_template_in_2_days.send_mail(self.test_record.id)
# mail_now = self.env['mail.mail'].browse(mail_now_id)
# mail_in_2_days = self.env['mail.mail'].browse(mail_in_2_days_id)
# # mail preparation
# self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_now | mail_in_2_days)
# self.assertEqual(bool(mail_now.scheduled_date), False)
# self.assertEqual(mail_now.state, 'outgoing')
# self.assertEqual(mail_in_2_days.state, 'outgoing')
# scheduled_date = datetime.strptime(mail_in_2_days.scheduled_date, DEFAULT_SERVER_DATETIME_FORMAT)
# date_in_2_days = datetime.now() + timedelta(days = 2)
# self.assertEqual(scheduled_date, date_in_2_days)
# # self.assertEqual(scheduled_date.month, date_in_2_days.month)
# # self.assertEqual(scheduled_date.year, date_in_2_days.year)
# # Launch the scheduler on the first mail, it should be reported in self.mails
# # and the mail_mail is now deleted
# self.env['mail.mail'].process_email_queue()
# self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days)
# # Launch the scheduler on the first mail, it's still in 'outgoing' state
# self.env['mail.mail'].process_email_queue(ids=[mail_in_2_days.id])
# self.assertEqual(mail_in_2_days.state, 'outgoing')
# self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days)
@@ -19,7 +19,6 @@ from odoo.exceptions import AccessError
from odoo.tests import tagged
from odoo.tools import mute_logger, formataddr
from odoo.tests.common import users
from odoo.tools.translate import code_translations
class TestMessagePostCommon(TestMailCommon, TestRecipients):
@@ -1241,12 +1240,6 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_layout_email_lang_context(self):
# hack into code translations:
code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'}
self.addCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES'))
code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'}
self.addCleanup(code_translations.python_translations.pop, ('mail', 'es_ES'))
test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES')
test_records[1].message_subscribe(self.partner_2.ids)
@@ -4,10 +4,11 @@ import lxml.html
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.fields import Command
from odoo.tests.common import users
from odoo.tests.common import users, tagged
from odoo.tools import mute_logger
@tagged('mailing_manage')
class TestMailingTest(TestMassMailCommon):
@users('user_marketing')
+10 -1
View File
@@ -134,7 +134,16 @@ class ChannelUsersRelation(models.Model):
record_email_values = dict()
for template, records in template_to_records.items():
record_email_values.update(template.generate_email(records.ids, ['subject', 'body_html', 'email_from', 'partner_to']))
record_email_values.update(
template.generate_email(
records.ids,
['body_html',
'email_from',
'partner_to',
'subject',
]
)
)
mail_mail_values = []
for record in self: