[IMP] test_mail: add composer tests for email layout support and translation

Purpose is to add some tests about email layout support in both comment and
email mode, as well as language support in those two modes. They are added
in a test targeting a complete template usage, testing globally the results
(outgoing notifications or emails, buttons, language, recipients, ...).
Posting in multi-languages environment with notification layout is also
tested. Finally a test about reply-to is moved into its own subtest to
avoid polluting core template tests. Other low level details should already
sufficiently tested. Normally.

Main observations

  * translations are quite broken in mass mode (either batch comment either
    mass mailing). Indeed translations are fetched based on <mail.compose.
    message> body and subject fields, which are probably not translated
    as they are linked to a transient model. Real translations are stored
    at <mail.template> level when users translate their templates;
  * layout translation is partly supported in comment mode, due to a small
    context-based hack. However access buttons are not translated and some
    use case (like using a 'res_domain') are not supported;
  * mass_mail mode does not support layouting at all;
  * when posting manually ('message_post') current users' language determines
    the language of notification email layout e.g. a user using Odoo in French
    will send a french layout to all followers, whatever their language and
    whatever the language of the post (which is not known as user input);

Most of those use cases will be fixed soon.

Also update some query counters while passing by.

Task-3046371 (Mail: Better Language Support in Composer)

Part-of: odoo/odoo#114511
This commit is contained in:
Thibault Delavallée
2023-03-07 18:03:01 +01:00
parent 3147d09b90
commit 248e0e495b
3 changed files with 526 additions and 174 deletions
+14 -3
View File
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.tools import email_normalize
from odoo.tools.translate import _
class MailTestSimple(models.Model):
@@ -164,10 +163,22 @@ class MailTestTicket(models.Model):
def _notify_get_recipients_groups(self, msg_vals=None):
""" Activate more groups to test query counters notably (and be backward
compatible for tests). """
groups = super(MailTestTicket, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
groups = super()._notify_get_recipients_groups(msg_vals=msg_vals)
for group_name, _group_method, group_data in groups:
if group_name == 'portal':
group_data['active'] = True
elif group_name == 'customer':
group_data['active'] = True
group_data['has_button_access'] = True
group_data['actions'] = [{
'url': self._notify_get_action_link(
'controller',
controller='/test_mail/do_stuff',
**local_msg_vals
),
'title': _('NotificationButtonTitle')
}]
return groups
+283 -106
View File
@@ -89,6 +89,7 @@ class TestMailComposer(TestMailCommon, TestRecipients):
'partner_to': '{{ object.customer_id.id if object.customer_id else "" }}',
'email_to': '{{ (object.email_from if not object.customer_id else "") }}',
'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}',
'lang': '{{ object.customer_id.lang }}',
'mail_server_id': cls.mail_server_domain.id,
'model_id': cls.env['ir.model']._get('mail.test.ticket').id,
'reply_to': '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}',
@@ -129,6 +130,14 @@ class TestMailComposer(TestMailCommon, TestRecipients):
@tagged('mail_composer')
class TestComposerForm(TestMailComposer):
def test_assert_initial_data(self):
""" Ensure class initial data to ease understanding """
self.assertTrue(self.template.auto_delete)
self.assertEqual(len(self.test_records), 2)
self.assertEqual(self.test_records.user_id, self.user_employee_2)
self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2)
@users('employee')
def test_mail_composer_comment(self):
composer_form = Form(self.env['mail.compose.message'].with_context(
@@ -1153,11 +1162,26 @@ class TestComposerInternals(TestMailComposer):
self.assertEqual(template.body_html, '<p>Template Body</p>', 'email_template incorrect body_html')
@tagged('mail_composer')
@tagged('mail_composer', 'multi_lang')
class TestComposerResultsComment(TestMailComposer, CronMixinCase):
""" Test global output of composer used in comment mode. Test notably
notification and emails generated during this process. """
def test_assert_initial_data(self):
""" Ensure class initial data to ease understanding """
self.assertTrue(self.template.auto_delete)
self.assertEqual(len(self.test_records), 2)
self.assertEqual(self.test_records.user_id, self.user_employee_2)
self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2)
self.assertEqual(self.test_records[0].customer_id.lang, 'en_US')
self.assertEqual(self.test_records[1].customer_id.lang, 'en_US')
self.assertEqual(len(self.test_partners), 2)
self.assertEqual(self.user_employee.lang, 'en_US')
self.assertEqual(self.user_employee_2.lang, 'en_US')
@users('employee')
def test_mail_composer_default_subject(self):
""" Make sure the default subject is applied in the composer. """
@@ -1322,6 +1346,10 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
* scheduled_date: creates mail.message.schedule (no email sent), then
scheduling send notifications with notification parameters kept
* otherwise: global behavior
Test with and without notification layout specified.
Test with and without languages.
"""
attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id)
email_to_1 = 'test.to.1@test.example.com'
@@ -1339,17 +1367,37 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])])
self.assertEqual(len(attachs), 2)
for batch_mode, scheduled_date, email_layout_xmlid in product(
for batch_mode, scheduled_date, email_layout_xmlid, use_lang in product(
(False, True, 'domain'),
(False, '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}'),
(False, 'mail.test_layout'),
(False, True),
):
with self.subTest(batch_mode=batch_mode, scheduled_date=scheduled_date):
with self.subTest(batch_mode=batch_mode,
scheduled_date=scheduled_date,
email_layout_xmlid=email_layout_xmlid,
use_lang=use_lang):
# update test configuration
batch = bool(batch_mode)
self.template.write({
'scheduled_date': scheduled_date,
'email_layout_xmlid': email_layout_xmlid,
})
if use_lang:
if batch:
langs = ('es_ES', 'en_US')
self.test_partners[0].lang = langs[0]
self.test_partners[1].lang = langs[1]
else:
langs = ('es_ES',)
self.partner_1.lang = langs[0]
if not use_lang:
if batch:
langs = (False, False)
self.test_partners.lang = False
else:
langs = (False,)
self.partner_1.lang = False
test_records = self.test_records if batch else self.test_record
# ensure initial data
@@ -1383,7 +1431,7 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
else:
author = self.partner_employee_2
self.assertEqual(composer.author_id, author,
'Author cannot be synchronized with a raw email_from')
'Author is synchronized with rendered email_from')
self.assertEqual(composer.email_from, self.partner_employee_2.email_formatted)
self.assertFalse(composer.reply_to_force_new, 'Mail: thread-enabled models should use auto thread by default')
@@ -1426,7 +1474,11 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
self.assertEqual(len(new_partners), 3)
self.assertEqual(
set(new_partners.mapped('email')),
set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com'])
{'test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com'},
)
self.assertEqual(
set(new_partners.mapped('lang')),
{'en_US'},
)
# if scheduled_date is set: simulate cron for sending notifications
@@ -1453,15 +1505,31 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
self.env['mail.mail'].sudo().process_email_queue()
# template is sent only to partners (email_to are transformed)
for test_record in test_records:
for test_record, exp_lang in zip(test_records, langs):
message = test_record.message_ids[0]
# check created mail.mail and outgoing emails. In comment
# 2 mails are generated (due to group-based layouting):
# - one for recipient that is a user
# - one for recipients that are customers
# Then each recipient receives its own outging email. See
# 'assertMailMail' for more details.
# TDE FIXME: lang not supported in batch mode, translations
# are fetched on composer-side, not template-side
if exp_lang == 'es_ES' and not batch:
exp_body = f'SpanishBody for {test_record.name}'
exp_subject = f'SpanishSubject for {test_record.name}'
else:
exp_body = f'TemplateBody {test_record.name}'
exp_subject = f'TemplateSubject {test_record.name}'
self.assertMailMail(self.partner_employee_2, 'sent',
mail_message=message,
author=author, # author is different in batch and monorecord mode (raw or rendered email_from)
email_values={
'body_content': f'TemplateBody {test_record.name}',
'body_content': exp_body,
'email_from': test_record.user_id.email_formatted, # set by template
'subject': f'TemplateSubject {test_record.name}',
'subject': exp_subject,
'attachments_info': [
{'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'},
{'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'},
@@ -1477,9 +1545,9 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
mail_message=message,
author=author, # author is different in batch and monorecord mode (raw or rendered email_from)
email_values={
'body_content': f'TemplateBody {test_record.name}',
'body_content': exp_body,
'email_from': test_record.user_id.email_formatted, # set by template
'subject': f'TemplateSubject {test_record.name}',
'subject': exp_subject,
'attachments_info': [
{'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'},
{'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'},
@@ -1492,6 +1560,36 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
},
)
# Low-level checks on outgoing email for the recipient to
# check layouting and language. Note that standard layout
# is not tested against translations, only the custom one
# to ease translations checks.
email = self._find_sent_email(test_record.user_id.email_formatted, [test_record.customer_id.email_formatted])
self.assertTrue(bool(email), 'Email not found, check recipients')
# TDE FIXME: as it currently depends on a context-based hack
# translation is not supported when scheduling notifications
# or when a domain is given. Moreover access buttons are not
# translated
exp_layout_content_en = 'English Layout for Ticket-like model'
exp_layout_content_es = 'Spanish Layout para Spanish Model Description'
exp_button_en = 'View Ticket-like model'
# exp_button_es = 'SpanishView Spanish Model Description'
if email_layout_xmlid:
if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date:
self.assertIn(exp_layout_content_es, email['body'])
self.assertIn(exp_button_en, email['body'],
'TODO: buttons should be translated')
else:
self.assertIn(exp_layout_content_en, email['body'])
self.assertIn(exp_button_en, email['body'])
else:
# check default layouting applies
if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date:
self.assertIn('html lang="es_ES"', email['body'])
else:
self.assertIn('html lang="en_US"', email['body'])
# message is posted and notified admin
self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment'))
self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}])
@@ -1560,7 +1658,7 @@ class TestComposerResultsCommentStatus(TestMailComposer):
'model_id': cls.env['ir.model']._get_id(cls.test_records._name),
})
def test_initial_data(self):
def test_assert_initial_data(self):
""" Ensure class initial data to ease understanding """
self.assertFalse(self.template.auto_delete)
@@ -1603,7 +1701,7 @@ class TestComposerResultsCommentStatus(TestMailComposer):
self.assertEqual(len(self._mails), 2, 'Should have sent 2 emails, skipping the exclusion list')
@tagged('mail_composer')
@tagged('mail_composer', 'multi_lang')
class TestComposerResultsMass(TestMailComposer):
@classmethod
@@ -1718,7 +1816,10 @@ class TestComposerResultsMass(TestMailComposer):
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_mail_composer_wtpl_complete(self):
""" Test a composer in mass mode with a quite complete template, containing
notably email-based recipients and attachments. """
notably email-based recipients and attachments.
Translations and email layout supported are also tested.
"""
# as we use the email queue, don't have failing tests due to other outgoing emails
self.env['mail.mail'].sudo().search([]).unlink()
@@ -1742,109 +1843,155 @@ class TestComposerResultsMass(TestMailComposer):
self.assertEqual(self.test_records.user_id, self.user_employee_2)
self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2)
# launch composer in mass mode
composer_form = Form(self.env['mail.compose.message'].with_context(
self._get_web_context(self.test_records, add_web=True,
default_template_id=self.template.id)
))
composer = composer_form.save()
# ensure some parameters used afterwards
author = self.env.user.partner_id
self.assertEqual(composer.author_id, author,
'Author cannot be synchronized with a raw email_from')
self.assertEqual(composer.email_from, self.template.email_from)
for use_domain, scheduled_date, email_layout_xmlid, use_lang in product(
(False, True),
(False, '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}'),
(False, 'mail.test_layout'),
(False, True),
):
with self.subTest(use_domain=use_domain,
scheduled_date=scheduled_date,
email_layout_xmlid=email_layout_xmlid,
use_lang=use_lang):
# update test configuration
self.template.write({
'scheduled_date': scheduled_date,
})
if use_lang:
langs = ('es_ES', 'en_US')
self.test_partners[0].lang = langs[0]
self.test_partners[1].lang = langs[1]
else:
langs = (False, False)
self.test_partners.lang = False
with self.mock_mail_gateway(mail_unlink_sent=False), \
freeze_time(self.reference_now):
composer._action_send_mail()
ctx = {
'default_model': self.test_records._name,
'default_composition_mode': 'mass_mail',
'default_template_id': self.template.id,
}
if use_domain:
ctx['default_res_domain'] = [('id', 'in', self.test_records.ids)]
else:
ctx['default_res_ids'] = self.test_records.ids
if email_layout_xmlid:
ctx['default_email_layout_xmlid'] = email_layout_xmlid
new_partners = self.env['res.partner'].search([
('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1])
])
self.assertEqual(len(new_partners), 3)
# launch composer in mass mode
composer_form = Form(self.env['mail.compose.message'].with_context(ctx))
composer = composer_form.save()
# global outgoing: emails not sent due to scheduled_date
self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
self.assertEqual(len(self._mails), 0, 'Should not send emails, scheduled in the future')
self.assertEqual(self._new_mails.mapped('scheduled_date'),
[self.reference_now + timedelta(days=2)] * 2)
# ensure some parameters used afterwards
author = self.env.user.partner_id
self.assertEqual(composer.author_id, author,
'Author is not synchronized, as template email_from does not match existing partner')
self.assertEqual(composer.email_from, self.template.email_from)
# simulate cron queue at right time for sending
with freeze_time(self.reference_now + timedelta(days=2)):
self.env['mail.mail'].sudo().process_email_queue()
with self.mock_mail_gateway(mail_unlink_sent=False), \
freeze_time(self.reference_now):
composer._action_send_mail()
self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record')
# partners created from raw emails
new_partners = self.env['res.partner'].search([
('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1])
])
self.assertEqual(len(new_partners), 3)
self.assertEqual(new_partners.mapped('lang'), ['en_US'] * 3,
'New partners lang is always the default DB one, whatever the context')
# hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails
_mails_records = [
[mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']]
for record in self.test_records
]
# check global outgoing
self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record')
if not scheduled_date:
# emails sent directly
self.assertEqual(len(self._mails), 10, 'Should have sent emails')
self.assertEqual(self._new_mails.mapped('scheduled_date'),
[False] * 2)
else:
# emails not sent due to scheduled_date
self.assertEqual(len(self._mails), 0, 'Should not send emails, scheduled in the future')
self.assertEqual(self._new_mails.mapped('scheduled_date'),
[self.reference_now + timedelta(days=2)] * 2)
for record, _mails in zip(self.test_records, _mails_records):
# message copy is kept
message = record.message_ids[0]
# simulate cron queue at right time for sending
with freeze_time(self.reference_now + timedelta(days=2)):
self.env['mail.mail'].sudo().process_email_queue()
# template is sent only to partners (email_to are transformed)
self._mails = _mails
self.assertMailMail(record.customer_id + new_partners + self.partner_admin,
'sent',
mail_message=message,
author=author,
email_values={
'attachments_info': [
{'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'},
{'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'},
{'name': 'TestReport for %s.html' % record.name, 'type': 'text/plain'},
],
'body_content': 'TemplateBody %s' % record.name,
'email_from': self.partner_employee_2.email_formatted,
'subject': 'TemplateSubject %s' % record.name,
},
fields_values={
'email_from': self.partner_employee_2.email_formatted,
'mail_server_id': self.mail_server_domain,
'reply_to': formataddr((
f'{self.env.user.company_id.name} {record.name}',
f'{self.alias_catchall}@{self.alias_domain}'
)),
'subject': 'TemplateSubject %s' % record.name,
},
)
# everything should be sent now
self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record')
# test without catchall filling reply-to
composer_form = Form(self.env['mail.compose.message'].with_context(
self._get_web_context(self.test_records, add_web=True,
default_template_id=self.template.id)
))
composer = composer_form.save()
with self.mock_mail_gateway(mail_unlink_sent=True):
# remove alias so that _notify_get_reply_to will return the default value instead of alias
self.env['ir.config_parameter'].sudo().set_param("mail.catchall.domain", None)
composer.action_send_mail()
# check email content
for record, exp_lang in zip(self.test_records, langs):
# message copy is kept
message = record.message_ids[0]
# hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails
_mails_records = [
[mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']]
for record in self.test_records
]
# Translations are currently not really supported as they
# are fetched composer side. As it is a wizard people use
# and discard they are not translated. Only templates are
# translated as they are master data.
if False and exp_lang == 'es_ES': # remove False when improving support
exp_body = f'SpanishBody for {record.name}'
exp_subject = f'SpanishSubject for {record.name}'
else:
exp_body = f'TemplateBody {record.name}'
exp_subject = f'TemplateSubject {record.name}'
for record, _mails in zip(self.test_records, _mails_records):
# template is sent only to partners (email_to are transformed)
self._mails = _mails
self.assertMailMail(record.customer_id + new_partners + self.partner_admin,
'sent',
mail_message=record.message_ids[0],
author=author,
email_values={
'email_from': self.partner_employee_2.email_formatted,
'reply_to': self.partner_employee_2.email_formatted,
},
fields_values={
'email_from': self.partner_employee_2.email_formatted,
'reply_to': self.partner_employee_2.email_formatted,
},
)
# template is sent only to partners (email_to are transformed)
self.assertMailMail(record.customer_id + new_partners + self.partner_admin,
'sent',
mail_message=message,
author=author,
email_values={
'attachments_info': [
{'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'},
{'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'},
{'name': 'TestReport for %s.html' % record.name, 'type': 'text/plain'},
],
'body_content': exp_body,
'email_from': self.partner_employee_2.email_formatted,
'subject': exp_subject,
},
fields_values={
'email_from': self.partner_employee_2.email_formatted,
'mail_server_id': self.mail_server_domain,
'reply_to': formataddr((
f'{self.env.user.company_id.name} {record.name}',
f'{self.alias_catchall}@{self.alias_domain}'
)),
'subject': exp_subject,
},
)
# Low-level checks on outgoing email for the recipient to
# check layouting and language. Note that standard layout
# is not tested against translations, only the custom one
# to ease translations checks.
email = self._find_sent_email(self.partner_employee_2.email_formatted, [record.customer_id.email_formatted])
self.assertTrue(bool(email), 'Email not found, check recipients')
# Currently layouting in mailing mode is not supported.
# Hence no translations.
self.assertEqual(
email['body'],
f'<p>TemplateBody {record.name}</p>'
)
# exp_layout_content_en = 'English Layout for Ticket-like model'
# exp_layout_content_es = 'Spanish Layout para Spanish Model Description'
# exp_button_en = 'View Ticket-like model'
# exp_button_es = 'zefView Ticket-like model'
# if email_layout_xmlid:
# if exp_lang == 'es_ES':
# self.assertIn(exp_layout_content_es, email['body'])
# self.assertIn(exp_button_en, email['body'])
# else:
# self.assertIn(exp_layout_content_en, email['body'])
# # self.assertIn(exp_button_es, email['body'])
# self.assertIn(exp_button_en, email['body'])
# else:
# # check default layouting applies
# if exp_lang == 'es_ES':
# self.assertIn('html lang="es_ES"', email['body'])
# else:
# self.assertIn('html lang="en_US"', email['body'])
@users('employee')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
@@ -2020,6 +2167,36 @@ class TestComposerResultsMass(TestMailComposer):
self.assertSentEmail(self.partner_employee_2.email_formatted,
self.test_records[0].customer_id)
@users('employee')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_mail_composer_wtpl_reply_to(self):
# test without catchall filling reply-to
composer_form = Form(self.env['mail.compose.message'].with_context(
self._get_web_context(self.test_records, add_web=True,
default_template_id=self.template.id)
))
composer = composer_form.save()
with self.mock_mail_gateway(mail_unlink_sent=False):
# remove alias so that _notify_get_reply_to will return the default value instead of alias
self.env['ir.config_parameter'].sudo().set_param("mail.catchall.domain", None)
composer.action_send_mail()
for record in self.test_records:
# template is sent only to partners (email_to are transformed)
self.assertMailMail(record.customer_id,
'sent',
mail_message=record.message_ids[0],
author=self.partner_employee,
email_values={
'email_from': self.partner_employee_2.email_formatted,
'reply_to': self.partner_employee_2.email_formatted,
},
fields_values={
'email_from': self.partner_employee_2.email_formatted,
'reply_to': self.partner_employee_2.email_formatted,
},
)
@users('employee')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_mail_composer_wtpl_reply_to_force_new(self):
@@ -2111,7 +2288,7 @@ class TestComposerResultsMassStatus(TestMailComposer):
'model_id': cls.env['ir.model']._get_id(cls.test_records._name),
})
def test_initial_data(self):
def test_assert_initial_data(self):
""" Ensure class initial data to ease understanding """
self.assertTrue(self.template.auto_delete)
+229 -65
View File
@@ -5,6 +5,7 @@ import base64
from datetime import datetime, timedelta
from freezegun import freeze_time
from itertools import product
from markupsafe import escape
from unittest.mock import patch
@@ -201,12 +202,14 @@ class TestMailNotifyAPI(TestMessagePostCommon):
template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, main_company.id)
@users('employee')
def test_notify_recipients_internals(self):
base_record = self.test_record.with_env(self.env)
pdata = self._generate_notify_recipients(self.partner_1 | self.partner_employee)
msg_vals = {
'body': 'Message body',
'model': self.test_record._name,
'res_id': self.test_record.id,
'model': base_record._name,
'res_id': base_record.id,
'subject': 'Message subject',
}
link_vals = {
@@ -216,49 +219,58 @@ class TestMailNotifyAPI(TestMessagePostCommon):
'auth_login': 'auth_login_val',
}
notify_msg_vals = dict(msg_vals, **link_vals)
classify_res = self.env[self.test_record._name]._notify_get_recipients_classify(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
# test notifying the class (void recordset)
classify_res = self.env[base_record._name]._notify_get_recipients_classify(
pdata, 'My Custom Model Name', msg_vals=notify_msg_vals
)
# find back information for each recipients
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
# partner: no access button
self.assertFalse(partner_info['has_button_access'])
# employee: access button and link
self.assertTrue(emp_info['has_button_access'])
for param, value in link_vals.items():
self.assertIn('%s=%s' % (param, value), emp_info['button_access']['url'])
self.assertIn('model=%s' % self.test_record._name, emp_info['button_access']['url'])
self.assertIn('res_id=%s' % self.test_record.id, emp_info['button_access']['url'])
self.assertIn('model=%s' % base_record._name, emp_info['button_access']['url'])
self.assertIn('res_id=%s' % base_record.id, emp_info['button_access']['url'])
self.assertNotIn('body', emp_info['button_access']['url'])
self.assertNotIn('subject', emp_info['button_access']['url'])
# test when notifying on non-records (e.g. MailThread._message_notify())
for model, res_id in ((self.test_record._name, False),
(self.test_record._name, 0), # browse(0) does not return a valid recordset
(False, self.test_record.id),
for model, res_id in ((base_record._name, False),
(base_record._name, 0), # browse(0) does not return a valid recordset
('mail.thread', False),
('mail.thread', base_record.id)):
with self.subTest(model=model, res_id=res_id):
notify_msg_vals.update({
'model': model,
'res_id': res_id,
})
classify_res = self.env[model].browse(res_id)._notify_get_recipients_classify(
pdata, 'Test', msg_vals=notify_msg_vals)
# find back information for partner
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
# check there is no access button
self.assertFalse(partner_info['has_button_access'])
self.assertFalse(emp_info['has_button_access'])
# test when notifying based a valid record, but asking for a falsy record in msg_vals
for model, res_id in ((base_record._name, False),
(base_record._name, 0), # browse(0) does not return a valid recordset
(False, base_record.id),
(False, False),
('mail.thread', False),
('mail.thread', self.test_record.id)):
msg_vals.update({
'model': model,
'res_id': res_id,
})
# note that msg_vals wins over record on which method is called
notify_msg_vals = dict(msg_vals, **link_vals)
classify_res = self.test_record._notify_get_recipients_classify(
pdata, 'Test', msg_vals=notify_msg_vals)
# find back information for partner
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
# check there is no access button
self.assertFalse(partner_info['has_button_access'])
self.assertFalse(emp_info['has_button_access'])
# test on falsy records (False model cannot be browsed, skipped)
if model:
record_falsy = self.env[model].browse(res_id)
classify_res = record_falsy._notify_get_recipients_classify(
('mail.thread', base_record.id)):
with self.subTest(model=model, res_id=res_id):
# note that msg_vals wins over record on which method is called
notify_msg_vals.update({
'model': model,
'res_id': res_id,
})
classify_res = base_record._notify_get_recipients_classify(
pdata, 'Test', msg_vals=notify_msg_vals)
# find back information for partner
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
@@ -436,7 +448,7 @@ class TestMessageNotify(TestMessagePostCommon):
)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
def test_notify_parameters(self):
""" Test usage of parameters in notify, both for unwanted side effects
and magic parameters. """
@@ -591,7 +603,8 @@ class TestMessageLog(TestMessagePostCommon):
@tagged('mail_post')
class TestMessagePost(TestMessagePostCommon, CronMixinCase):
def test_initial_values(self):
def test_assert_initial_values(self):
""" Be sure of what we are testing """
self.assertFalse(self.test_record.message_ids)
self.assertFalse(self.test_record.message_follower_ids)
self.assertFalse(self.test_record.message_partner_ids)
@@ -662,7 +675,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
partner_ids=self.partner_portal.ids,
)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
@users('employee')
def test_message_post_author(self):
""" Test author recognition """
@@ -765,6 +778,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase):
)
@users('employee')
@mute_logger('odoo.models.unlink')
def test_message_post_inactive_follower(self):
""" Test posting with inactive followers does not notify them (e.g. odoobot) """
test_record = self.env['mail.test.simple'].browse(self.test_record.ids)
@@ -1480,46 +1494,103 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
cls.partner_2.write({'lang': 'es_ES'})
def test_assert_initial_values(self):
""" Be sure of what we are testing """
self.assertEqual(self.partner_1.lang, 'en_US')
self.assertEqual(self.partner_2.lang, 'es_ES')
self.assertEqual(self.test_records[0].lang, 'es_ES')
self.assertEqual(self.test_records[0].customer_id.lang, False)
self.assertEqual(self.test_records[1].lang, False)
self.assertEqual(self.test_records[1].customer_id.lang, 'es_ES')
self.assertFalse(self.test_records[0].message_follower_ids)
self.assertFalse(self.test_records[1].message_follower_ids)
self.assertEqual(self.user_employee.lang, 'en_US')
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_composer_lang_template_comment(self):
""" When posting in comment mode, content is rendered using the lang
field of template. Notification layout lang is partly coming from
template, partly from environment (to be improved). """
test_record = self.test_records[0].with_user(self.env.user)
test_template = self.test_template.with_user(self.env.user)
with self.mock_mail_gateway():
test_record.message_post_with_source(
test_template,
email_layout_xmlid='mail.test_layout',
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
for partner in self.env['res.partner'] + self.partner_1 + self.partner_2:
with self.subTest(partner=partner):
test_record.write({
'customer_id': partner.id,
})
with self.mock_mail_gateway():
test_record.message_post_with_source(
test_template,
email_layout_xmlid='mail.test_layout',
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
# expected languages: content depend on template (lang field) aka
# customer.lang or record.lang (see template); notif lang is
# partner lang or default DB lang
exp_content_lang = partner.lang if partner.lang else 'es_ES'
exp_notif_lang = partner.lang if partner.lang else 'en_US'
customer_email = self._find_sent_mail_wemail(record0_customer.email_formatted)
self.assertTrue(customer_email)
body = customer_email['body']
# check content
self.assertIn(f'SpanishBody for {test_record.name}', body,
'Body based on template should be translated')
# check subject
self.assertEqual(f'SpanishSubject for {test_record.name}', customer_email['subject'],
'Subject based on template should be translated')
# check notification layout content
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated')
# check notification layout strings
self.assertIn('View Lang Chatter Model', body,
'Fixme: "View document" should be translated')
# self.assertIn('SpanishView Spanish Model Description', body,
# '"View document" should be translated')
# self.assertNotIn(f'View {test_record._description}', body,
# '"View document" should be translated')
# self.assertIn('SpanishButtonTitle', body,
# 'Groups-based action names should be translated')
self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated')
if partner:
customer = partner
else:
customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
self.assertTrue(customer, 'Template usage should have created a contact based on record email')
self.assertEqual(customer.lang, exp_notif_lang)
customer_email = self._find_sent_mail_wemail(customer.email_formatted)
self.assertTrue(customer_email)
body = customer_email['body']
# check content: depends on object.lang / object.customer_id.lang
if exp_content_lang == 'en_US':
self.assertIn(f'EnglishBody for {test_record.name}', body,
'Body based on template should be translated')
else:
self.assertIn(f'SpanishBody for {test_record.name}', body,
'Body based on template should be translated')
# check subject
if exp_content_lang == 'en_US':
self.assertEqual(f'EnglishSubject for {test_record.name}', customer_email['subject'],
'Subject based on template should be translated')
else:
self.assertEqual(f'SpanishSubject for {test_record.name}', customer_email['subject'],
'Subject based on template should be translated')
# check notification layout content: currently partly translated
# based only on template definition
if exp_content_lang == 'en_US':
self.assertNotIn('Spanish Layout para', body, 'Layout translation failed')
self.assertIn('English Layout for Lang Chatter Model', body,
'Layout / model translation failed')
self.assertNotIn('Spanish Model Description', body, 'Model translation failed')
# check notification layout strings
self.assertNotIn('SpanishView Spanish Model Description', body,
'"View document" translation failed')
self.assertIn(f'View {test_record._description}', body,
'"View document" translation failed')
self.assertNotIn('SpanishButtonTitle', body,
'Groups-based action names translation failed')
self.assertIn('NotificationButtonTitle', body,
'Groups-based action names translation failed')
else:
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated')
# check notification layout strings
self.assertIn('View Lang Chatter Model', body,
'Fixme: "View document" should be translated')
# self.assertIn('SpanishView Spanish Model Description', body,
# '"View document" should be translated')
# self.assertNotIn(f'View {test_record._description}', body,
# '"View document" should be translated')
# self.assertIn('SpanishButtonTitle', body,
# 'Groups-based action names should be translated')
self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated')
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -1589,6 +1660,8 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_layout_email_lang_template(self):
""" Test language support when posting in batch using a template.
Content and layout are is translated based on template definition. """
test_records = self.test_records.with_user(self.env.user)
test_template = self.test_template.with_user(self.env.user)
@@ -1606,6 +1679,10 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
for record, customer in zip(test_records, record0_customer + self.partner_2):
customer_email = self._find_sent_mail_wemail(customer.email_formatted)
self.assertTrue(customer_email)
# body and layouting are translated partly based on template. Bits
# of layout are not translated due to lang not being correctly
# propagate everywhere we need it
body = customer_email['body']
# check content
self.assertIn(f'SpanishBody for {record.name}', body,
@@ -1627,3 +1704,90 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
# 'Groups-based action names should be translated')
self.assertIn('NotificationButtonTitle', body,
'Fixme: groups-based action names should be translated')
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_post_multi_lang_recipients(self):
""" Test posting on a document in a multilang environment. Currently
current user's lang determines completely language used for notification
layout notably, when no template is involved.
Lang layout for this test (to better check various configuration and
check which lang wins the final output, if any)
* current users: various between en and es;
* partner1: es
* partner2: en
"""
test_records = self.test_records.with_env(self.env)
test_records.message_subscribe(partner_ids=(self.partner_1 + self.partner_2).ids)
for employee_lang, email_layout_xmlid in product(
('en_US', 'es_ES'),
(False, 'mail.test_layout'),
):
with self.subTest(employee_lang=employee_lang, email_layout_xmlid=email_layout_xmlid):
self.user_employee.write({
'lang': employee_lang,
})
for record in test_records:
with self.mock_mail_gateway(mail_unlink_sent=False), \
self.mock_mail_app():
record.message_post(
body='<p>Hi there</p>',
email_layout_xmlid=email_layout_xmlid,
message_type='comment',
subject='TeDeum',
subtype_xmlid='mail.mt_comment',
)
message = record.message_ids[0]
self.assertEqual(
message.notified_partner_ids, self.partner_1 + self.partner_2
)
# check created mail.mail and outgoing emails. One email
# is generated for 'partner_1' and 'partner_2' (same email
# as same configuration (aka all customers)).
_mail = self.assertMailMail(
self.partner_1 + self.partner_2, 'sent',
mail_message=message,
author=self.partner_employee,
email_values={
'body_content': '<p>Hi there</p>',
'email_from': self.partner_employee.email_formatted,
'subject': 'TeDeum',
},
)
# Low-level checks on outgoing email for the recipient to
# check layouting and language. Note that standard layout
# is not tested against translations, only the custom one
# to ease translations checks.
for partner in self.partner_1 + self.partner_2:
email = self._find_sent_email(
self.partner_employee.email_formatted,
[partner.email_formatted]
)
self.assertTrue(bool(email), 'Email not found, check recipients')
exp_layout_content_en = 'English Layout for Lang Chatter Model'
exp_layout_content_es = 'Spanish Layout para Spanish Model Description'
exp_button_en = 'View Lang Chatter Model'
exp_button_es = 'SpanishView Spanish Model Description'
exp_action_en = 'NotificationButtonTitle'
exp_action_es = 'SpanishButtonTitle'
if email_layout_xmlid:
if employee_lang == 'es_ES':
self.assertIn(exp_layout_content_es, email['body'])
self.assertIn(exp_button_es, email['body'])
self.assertIn(exp_action_es, email['body'])
else:
self.assertIn(exp_layout_content_en, email['body'])
self.assertIn(exp_button_en, email['body'])
self.assertIn(exp_action_en, email['body'])
else:
# check default layouting applies
if employee_lang == 'es_ES':
self.assertIn('html lang="es_ES"', email['body'])
else:
self.assertIn('html lang="en_US"', email['body'])