[MERGE] mail, sms, test_mail(_full): prepare SMS refactoring

Purpose of this merge is to prepare upcoming SMS refactoring by
already pushing some tools, cleaning and tests in master.

Contained in this merge

* improvement of phonenumbers library tools and usage in SMS;
* addition of SMS test classes with tools and mocks;
* addition of SMS tests;

See sub commits for more details. Related to task 1922163.

closes odoo/odoo#34516

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Robot Odoo
2019-07-02 14:07:56 +02:00
committed by GitHub
31 changed files with 550 additions and 134 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<act_window id="sms_message_send_action_mutli"
name="Send SMS to attendees"
binding_model="calendar.event"
res_model="sms.send_sms"
res_model="sms.composer"
view_mode="form"
target="new"/>
+1 -1
View File
@@ -133,7 +133,7 @@ Please, take appropriate measures in order to carry out this work absence.
Do not hesitate to contact your manager or the human resource department.""")
return {
"type": "ir.actions.act_window",
"res_model": "sms.send_sms",
"res_model": "sms.composer",
"view_mode": 'form',
"context": {
'active_id': self.id,
+1
View File
@@ -11,6 +11,7 @@
'data': [
'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
'wizard/mail_resend_cancel_views.xml',
'wizard/mail_resend_message_views.xml',
'views/mail_message_subtype_views.xml',
'views/mail_tracking_views.xml',
+1
View File
@@ -3,6 +3,7 @@
from . import invite
from . import mail_compose_message
from . import mail_resend_cancel
from . import mail_resend_message
from . import email_template_preview
from . import base_module_uninstall
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class MailCancelResend(models.TransientModel):
_name = 'mail.resend.cancel'
_description = 'Dismiss notification for resend by model'
model = fields.Char(string='Model')
help_message = fields.Char(string='Help message', compute='_compute_help_message')
@api.multi
@api.depends('model')
def _compute_help_message(self):
for wizard in self:
wizard.help_message = _("Are you sure you want to discard %s mail delivery failures. You won't be able to re-send these mails later!") % (wizard._context.get('unread_counter'))
@api.multi
def cancel_resend_action(self):
author_id = self.env.user.partner_id.id
for wizard in self:
self._cr.execute("""
SELECT notif.id, mes.id
FROM mail_message_res_partner_needaction_rel notif
JOIN mail_message mes
ON notif.mail_message_id = mes.id
WHERE notif.email_status IN ('bounce', 'exception')
AND mes.model = %s
AND mes.author_id = %s
""", (wizard.model, author_id))
res = self._cr.fetchall()
notif_ids = [row[0] for row in res]
messages_ids = list(set([row[1] for row in res]))
if notif_ids:
self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'})
self.env["mail.message"].browse(messages_ids)._notify_failure_update()
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="mail_resend_cancel_view_form" model="ir.ui.view">
<field name="name">mail.resend.cancel.view.form</field>
<field name="model">mail.resend.cancel</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Cancel notification in failure">
<field name="model" invisible='1'/>
<field name="help_message"/>
<p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message.</p>
<img src="/mail/static/img/red_envelope.png" alt="Envelope Example"/>
<footer>
<button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" />
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="mail_resend_cancel_action" model="ir.actions.act_window">
<field name="name">Discard mail delivery failures</field>
<field name="res_model">mail.resend.cancel</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data></odoo>
+2 -37
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from ast import literal_eval
from odoo import api, fields, models
from odoo.exceptions import UserError
class MailResendMessage(models.TransientModel):
_name = 'mail.resend.message'
_description = 'Email resend wizard'
@@ -100,38 +100,3 @@ class PartnerResend(models.TransientModel):
resend = fields.Boolean(string="Send Again", default=True)
resend_wizard_id = fields.Many2one('mail.resend.message', string="Resend wizard")
message = fields.Char(string="Help message")
class MailCancelResend(models.TransientModel):
_name = 'mail.resend.cancel'
_description = 'Dismiss notification for resend by model'
model = fields.Char(string='Model')
help_message = fields.Char(string='Help message', compute='_compute_help_message')
@api.multi
@api.depends('model')
def _compute_help_message(self):
for wizard in self:
wizard.help_message = _("Are you sure you want to discard %s mail delivery failures. You won't be able to re-send these mails later!") % (wizard._context.get('unread_counter'))
@api.multi
def cancel_resend_action(self):
author_id = self.env.user.partner_id.id
for wizard in self:
self._cr.execute("""
SELECT notif.id, mes.id
FROM mail_message_res_partner_needaction_rel notif
JOIN mail_message mes
ON notif.mail_message_id = mes.id
WHERE notif.email_status IN ('bounce', 'exception')
AND mes.model = %s
AND mes.author_id = %s
""", (wizard.model, author_id))
res = self._cr.fetchall()
notif_ids = [row[0] for row in res]
messages_ids = list(set([row[1] for row in res]))
if notif_ids:
self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'})
self.env["mail.message"].browse(messages_ids)._notify_failure_update()
return {'type': 'ir.actions.act_window_close'}
@@ -2,7 +2,7 @@
<odoo>
<data>
<record id="mail_resend_message_view_form" model="ir.ui.view">
<field name="name">mail.resend.message.form</field>
<field name="name">mail.resend.message.view.form</field>
<field name="model">mail.resend.message</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
@@ -31,23 +31,6 @@
</form>
</field>
</record>
<record id="mail_resend_cancel_view_form" model="ir.ui.view">
<field name="name">mail.resend.cancel.form</field>
<field name="model">mail.resend.cancel</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Cancel notification in failure">
<field name="model" invisible='1'/>
<field name="help_message"/>
<p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message.</p>
<img src="/mail/static/img/red_envelope.png" alt="Envelope Example"/>
<footer>
<button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" />
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="mail_resend_message_action" model="ir.actions.act_window">
<field name="name">Resend mail</field>
<field name="res_model">mail.resend.message</field>
@@ -55,13 +38,5 @@
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="mail_resend_cancel_action" model="ir.actions.act_window">
<field name="name">Discard mail delivery failures</field>
<field name="res_model">mail.resend.cancel</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
@@ -28,6 +28,6 @@ class PhoneValidationMixin(models.AbstractModel):
number,
country.code if country else None,
country.phone_code if country else None,
always_international=always_international,
force_format='INTERNATIONAL' if always_international else 'NATIONAL',
raise_exception=False
)
@@ -26,14 +26,21 @@ try:
return phone_nbr
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
""" Format the given phone number according to the localisation and international options.
:param number: number to convert
:param country_code: the ISO country code in two chars
:type country_code: str
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
:type country_phone_code: int
:rtype: str
:param number: number to convert
:param country_code: the ISO country code in two chars
:type country_code: str
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
:type country_phone_code: int
:param force_format: stringified version of format globals (see
https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/phonenumbers/phonenumberutil.py)
'E164' = 0
'INTERNATIONAL' = 1
'NATIONAL' = 2
'RFC3966' = 3
:type force_format: str
:rtype: str
"""
try:
phone_nbr = phone_parse(number, country_code)
@@ -43,7 +50,11 @@ try:
else:
_logger.warning(_('Unable to format %s:\n%s'), number, e)
return number
if always_international or phone_nbr.country_code != country_phone_code:
if force_format == 'E164':
phone_fmt = phonenumbers.PhoneNumberFormat.E164
elif force_format == 'RFC3966':
phone_fmt = phonenumbers.PhoneNumberFormat.RFC3966
elif force_format == 'INTERNATIONAL' or phone_nbr.country_code != country_phone_code:
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
else:
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
@@ -54,7 +65,7 @@ except ImportError:
def phone_parse(number, country_code):
return False
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
global _phonenumbers_lib_warning
if not _phonenumbers_lib_warning:
_logger.warning(
@@ -63,3 +74,56 @@ except ImportError:
)
_phonenumbers_lib_warning = True
return number
def phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format='E164'):
valid, invalid, void_count = [], [], 0
for number in numbers:
if not number:
void_count += 1
continue
try:
sanitized = phone_format(
number, country_code, country_phone_code,
force_format=force_format, raise_exception=True)
except Exception as e:
invalid.append(number)
else:
valid.append(sanitized)
return valid, invalid, void_count
def phone_sanitize_numbers_w_record(numbers, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
if not country_code or not country_phone_code:
country = False
if record and record_country_fname in record and record[record_country_fname]:
country = record[record_country_fname]
elif record:
country = record.env.company.country_id
if country:
country_code = country_code if country_code else country.code
country_phone_code = country_phone_code if country_phone_code else country.phone_code
return phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format=force_format)
def phone_sanitize_numbers_string_w_record(numbers_str, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
found_numbers = [number.strip() for number in numbers_str.split(',')]
return phone_sanitize_numbers_w_record(found_numbers, country_code, country_phone_code, record, record_country_fname, force_format=force_format)
def phone_get_sanitized_records_number(records, number_fname='mobile', country_fname='country_id', force_format='E164'):
res = dict.fromkeys(records.ids, False)
for record in records:
number = record[number_fname]
valid, invalid, void_count = phone_sanitize_numbers_w_record([number], None, None, records, country_fname,force_format=force_format)
if valid:
res[record.id] = valid[0]
elif void_count:
res[record.id] = False
else:
res[record.id] = False
return res
def phone_get_sanitized_record_number(record, number_fname='mobile', country_fname='country_id', force_format='E164'):
return phone_get_sanitized_records_number(record, number_fname, country_fname, force_format=force_format)[record.id]
+1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import tests
from . import wizard
+3 -3
View File
@@ -10,12 +10,12 @@ This module gives a framework for SMS text messaging
The service is provided by the In App Purchase Odoo platform.
""",
'depends': ['base', 'iap', 'mail'],
'depends': ['base', 'iap', 'mail', 'phone_validation'],
'data': [
'wizard/send_sms_views.xml',
'wizard/sms_composer_views.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/templates.xml',
'views/assets.xml',
],
'qweb': [
'static/src/xml/sms_widget.xml',
+40 -9
View File
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo import api, models
from odoo.addons.iap.models import iap
DEFAULT_ENDPOINT = 'https://iap-sms.odoo.com'
@@ -13,15 +12,47 @@ class SmsApi(models.AbstractModel):
_description = 'SMS API'
@api.model
def _send_sms(self, numbers, message):
""" Send sms
"""
def _contact_iap(self, local_endpoint, params):
account = self.env['iap.account'].get('sms')
params['account_token'] = account.account_token
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
# TODO PRO, the default timeout is 15, do we have to increase it ?
return iap.jsonrpc(endpoint + local_endpoint, params=params)
@api.model
def _send_sms(self, numbers, message):
""" Send a single message to several numbers
:param numbers: list of E164 formatted phone numbers
:param message: content to send
:raises ? TDE FIXME
"""
params = {
'account_token': account.account_token,
'numbers': numbers,
'message': message,
}
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
r = iap.jsonrpc(endpoint + '/iap/message_send', params=params)
return True
return self._contact_iap('/iap/message_send', params)
@api.model
def _send_sms_batch(self, messages):
""" Send SMS using IAP in batch mode
:param messages: list of SMS to send, structured as dict [{
'res_id': integer: ID of sms.sms,
'number': string: E164 formatted phone number,
'content': string: content to send
}]
:return: return of /iap/sms/1/send controller which is a list of dict [{
'res_id': integer: ID of sms.sms,
'state': string: 'insufficient_credit' or 'wrong_format_number' or 'success',
'credit': integer: number of credits spent to send this SMS,
}]
:raises: normally none
"""
params = {
'messages': messages
}
return self._contact_iap('/iap/sms/1/send', params)
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
+71
View File
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from contextlib import contextmanager
from unittest.mock import patch
from odoo import exceptions
from odoo.tests import common
from odoo.addons.sms.models.sms_api import SmsApi
class MockSMS(common.BaseCase):
def tearDown(self):
super(MockSMS, self).tearDown()
self._clear_sms_sent
@contextmanager
def mockSMSGateway(self, sim_error=None, nbr_t_error=None):
self._sms = []
def _contact_iap(local_endpoint, params):
# mock single sms sending
if local_endpoint == '/iap/message_send':
self._sms += [{
'number': number,
'body': params['message'],
} for number in params['numbers']]
return True # send_message v0 API returns always True
# mock batch sending
if local_endpoint == '/iap/sms/1/send':
result = []
for to_send in params['messages']:
res = {'res_id': to_send['res_id'], 'state': 'success', 'credit': 1}
error = sim_error or (nbr_t_error and nbr_t_error.get(to_send['number']))
if error and error == 'credit':
res.update(credit=0, state='insufficient_credit')
elif error and error == 'wrong_format_number':
res.update(state='wrong_format_number')
elif error and error == 'jsonrpc_exception':
raise exceptions.AccessError(
'The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + local_endpoint
)
result.append(res)
if res['state'] == 'success':
self._sms.append({
'number': to_send['number'],
'body': to_send['content'],
})
return result
try:
with patch.object(SmsApi, '_contact_iap', side_effect=_contact_iap) as contact_iap_mock:
yield
finally:
pass
def assertSMSSent(self, numbers, content):
""" Check sent SMS. Order is not checked. Each number should have received
the same content. Usefull to check batch sending.
:param numbers: list of numbers;
:param content: content to check for each number;
"""
self.assertEqual(len(self._sms), len(numbers))
for number in numbers:
sent_sms = next((sms for sms in self._sms if sms['number'] == number), None)
self.assertTrue(bool(sent_sms), 'Number %s not found in %s' % (number, repr([s['number'] for s in self._sms])))
self.assertEqual(sent_sms['body'], content)
def _clear_sms_sent(self):
self._sms = []
+4 -4
View File
@@ -2,8 +2,8 @@
<odoo>
<!-- Add action entry in the Action Menu for Partners -->
<record id="partner_form_send_sms_form_view" model="ir.ui.view">
<field name="name">res.partner.form.send.sms</field>
<record id="res_partner_view_form" model="ir.ui.view">
<field name="name">res.partner.view.form.inherit.sms</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority">10</field>
@@ -14,7 +14,7 @@
<field name="phone" widget="phone"/>
<button
type="action"
name="%(sms.send_sms_form_action)d"
name="%(sms.sms_composer_action_form)d"
class="btn-sm btn-link mb4 fa fa-envelope-o"
aria-label="Send SMS"
title="Send SMS"
@@ -29,7 +29,7 @@
<field name="mobile" widget="phone"/>
<button
type="action"
name="%(sms.send_sms_form_action)d"
name="%(sms.sms_composer_action_form)d"
class="btn-sm btn-link mb4 fa fa-envelope-o"
aria-label="Send SMS"
title="Send SMS"
+1 -1
View File
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
from . import send_sms
from . import sms_composer
@@ -23,7 +23,7 @@ except ImportError:
class SendSMS(models.TransientModel):
_name = 'sms.send_sms'
_name = 'sms.composer'
_description = 'Send SMS'
recipients = fields.Char('Recipients', required=True)
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="send_sms_view_form" model="ir.ui.view">
<field name="name">sms_send_sms.form</field>
<field name="model">sms.send_sms</field>
<record id="sms_composer_view_form" model="ir.ui.view">
<field name="name">sms.composer.view.form</field>
<field name="model">sms.composer</field>
<field name="arch" type="xml">
<form string="Send an SMS">
<sheet>
@@ -23,18 +22,18 @@
</form>
</field>
</record>
<record id="send_sms_form_action" model="ir.actions.act_window">
<record id="sms_composer_action_form" model="ir.actions.act_window">
<field name="name">Send SMS</field>
<field name="res_model">sms.send_sms</field>
<field name="res_model">sms.composer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Add action entry in the Action Menu for Partners -->
<act_window id="send_sms_action"
<act_window id="sms_composer_act_window"
name="Send SMS"
binding_model="res.partner"
res_model="sms.send_sms"
res_model="sms.composer"
view_mode="form"
target="new"/>
+58 -30
View File
@@ -37,6 +37,17 @@ class BaseFunctionalTest(common.SavepointCase):
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
@classmethod
def _reset_mail_context(cls, record):
return record.with_context(
mail_create_nolog=False,
mail_create_nosubscribe=False,
mail_notrack=False
)
def _clear_bus(self):
self.env['bus.bus'].search([]).unlink()
@contextmanager
def assertNotifications(self, **counters):
""" Counters: 'partner_attribute': 'inbox' or 'email' """
@@ -83,41 +94,57 @@ class BaseFunctionalTest(common.SavepointCase):
if hasattr(self, 'assertEmails') and len(new_messages) == 1:
self.assertEmails(new_messages.author_id, new_notifications.filtered(lambda n: n.is_email).mapped('res_partner_id'))
def assertBusNotification(self, channels, message_dicts=None, init=True):
def assertBusNotification(self, channels, message_items=None, init=True):
""" Check for bus notifications. Basic check is about used channels.
Verifying content is optional.
:param channels: list of channel
:param messages: if given, list of message making a valid pair (channel,
:param message_items: if given, list of message making a valid pair (channel,
message) to be found in bus.bus
"""
def check_content(returned_value, expected_value):
if isinstance(expected_value, list):
done = []
for expected_item in expected_value:
for returned_item in returned_value:
if check_content(returned_item, expected_item):
done.append(expected_item)
break
else:
return False
return len(done) == len(expected_value)
elif isinstance(expected_value, dict):
return all(k in returned_value for k in expected_value.keys()) and all(
check_content(returned_value[key], val)
for key, val in expected_value.items()
)
else:
return returned_value == expected_value
if init:
self.assertEqual(len(self.env['bus.bus'].search([])), len(channels))
notifications = self.env['bus.bus'].search([('channel', 'in', [json_dump(channel) for channel in channels])])
notif_messages = [json.loads(n.message) for n in notifications]
self.assertEqual(len(notifications), len(channels))
if message_dicts:
notif_messages = [json.loads(n.message) for n in notifications]
for expected in message_dicts:
found = False
for returned in notif_messages:
for key, val in expected.items():
if key not in returned:
continue
if isinstance(returned[key], list):
if set(returned[key]) != set(val):
continue
else:
if returned[key] != val:
continue
found = True
break
if found:
break
if not found:
raise AssertionError("Bus notification content %s not found" % (repr(expected)))
for expected in message_items or []:
for notification in notif_messages:
found_keys, not_found_keys = [], []
if not all(k in notification for k in expected.keys()):
continue
for expected_key, expected_value in expected.items():
done = check_content(notification[expected_key], expected_value)
if done:
found_keys.append(expected_key)
else:
not_found_keys.append(expected_key)
if set(found_keys) == set(expected.keys()):
break
else:
raise AssertionError('Keys %s not found (expected: %s - returned: %s)' % (not_found_keys, repr(expected), repr(notif_messages)))
@contextmanager
def sudoAs(self, login):
def sudo(self, login):
old_uid = self.uid
try:
user = self.env['res.users'].sudo().search([('login', '=', login)])
@@ -125,13 +152,11 @@ class BaseFunctionalTest(common.SavepointCase):
# switch user
self.uid = user.id
self.env = self.env(user=self.uid)
self.test_record = self.test_record.sudo(self.uid)
yield
finally:
# back
self.uid = old_uid
self.env = self.env(user=self.uid)
self.test_record = self.test_record_old
class TestRecipients(common.SavepointCase):
@@ -147,10 +172,16 @@ class TestRecipients(common.SavepointCase):
})
cls.partner_1 = Partner.create({
'name': 'Valid Lelitre',
'email': 'valid.lelitre@agrolait.com'})
'email': 'valid.lelitre@agrolait.com',
'country_id': cls.env.ref('base.be').id,
'mobile': '0475001122',
})
cls.partner_2 = Partner.create({
'name': 'Valid Poilvache',
'email': 'valid.other@gmail.com'})
'email': 'valid.other@gmail.com',
'country_id': cls.env.ref('base.be').id,
'mobile': '+32 475 22 11 00',
})
class MockEmails(common.SingleTransactionCase):
@@ -308,6 +339,3 @@ class Moderation(MockEmails, BaseFunctionalTest):
'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id
})
return message
def _clear_bus(self):
self.env['bus.bus'].search([]).unlink()
+4 -3
View File
@@ -90,7 +90,7 @@ class TestActivityRights(TestActivityCommon):
class TestActivityFlow(TestActivityCommon):
def test_activity_flow_employee(self):
with self.sudoAs('ernest'):
with self.sudo('ernest'):
test_record = self.env['mail.test.activity'].browse(self.test_record.id)
self.assertEqual(test_record.activity_ids, self.env['mail.activity'])
@@ -121,7 +121,7 @@ class TestActivityFlow(TestActivityCommon):
def test_activity_flow_portal(self):
portal_user = mail_new_test_user(self.env, login='chell', groups='base.group_portal', name='Chell Gladys')
with self.sudoAs('chell'):
with self.sudo('chell'):
test_record = self.env['mail.test.activity'].browse(self.test_record.id)
with self.assertRaises(exceptions.AccessError):
self.env['mail.activity'].create({
@@ -164,7 +164,8 @@ class TestActivityMixin(TestActivityCommon):
def test_activity_mixin(self):
self.user_employee.tz = self.user_admin.tz
with self.sudoAs('ernest'):
with self.sudo('ernest'):
self.test_record = self.env['mail.test.activity'].browse(self.test_record.id)
self.assertEqual(self.test_record.env.user, self.user_employee)
now_utc = datetime.now(pytz.UTC)
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import tests
+29
View File
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
{
'name': 'Mail Tests (Full)',
'version': '1.0',
'category': 'Hidden',
'sequence': 9876,
'summary': 'Mail Tests: performances and tests specific to mail with all sub-modules',
'description': """This module contains tests related to various mail features
and mail-related sub modules. Those tests are contained in a separate module as it
contains models used only to perform tests independently to functional aspects of
real applications. """,
'depends': [
'test_mail',
'mail',
'mail_bot',
# 'snailmail',
'mass_mailing',
'phone_validation',
'sms',
],
'data': [
'security/ir.model.access.csv',
],
'demo': [
],
'installable': True,
'application': False,
}
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import test_mail_models
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MailTestSMS(models.Model):
""" A model inheriting from mail.thread with some fields used for SMS
gateway, like a partner, a specific mobile phone, ... """
_description = 'Chatter Model for SMS Gateway'
_name = 'mail.test.sms'
_inherit = ['mail.thread']
name = fields.Char()
subject = fields.Char()
email_from = fields.Char()
phone_nbr = fields.Char()
mobile_nbr = fields.Char()
customer_id = fields.Many2one('res.partner', 'Customer')
@api.multi
def _get_default_sms_recipients(self):
return self.mapped('customer_id')
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0
access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mail_test_sms_all mail.test.sms.all model_mail_test_sms 0 0 0 0
3 access_mail_test_sms_user mail.test.sms.user model_mail_test_sms base.group_user 1 1 1 1
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import common
from . import test_sms_composer
from . import test_sms_post
+14
View File
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo.addons.test_mail.tests import common as test_mail_common
class BaseFunctionalTest(test_mail_common.BaseFunctionalTest):
@classmethod
def setUpClass(cls):
super(BaseFunctionalTest, cls).setUpClass()
cls.user_employee.write({'login': 'employee'})
# update country to belgium in order to test sanitization of numbers
cls.user_employee.company_id.write({'country_id': cls.env.ref('base.be').id})
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sms.tests import common as sms_common
from odoo.addons.test_mail.tests import common as test_mail_common
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
from odoo.addons.phone_validation.tools.phone_validation import phone_format
class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
@classmethod
def setUpClass(cls):
super(TestSMSComposer, cls).setUpClass()
cls._test_body = 'VOID CONTENT'
cls.partner_numbers = [
phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
for partner in (cls.partner_1 | cls.partner_2)
]
cls.random_numbers_str = '+32475998877, 0475997788'
cls.random_numbers = [phone_format(number, 'BE', '32', force_format='E164') for number in ['+32475998877', '0475997788']]
def test_composer_no_model(self):
composer = self.env['sms.composer'].with_context().create({
'message': self._test_body,
'recipients': '+32475998877, 0475997788'
})
with self.mockSMSGateway():
composer.action_send_sms()
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
def test_composer_no_mail_thread(self):
composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({
'message': self._test_body,
'recipients': '+32475998877, 0475997788'
})
with self.mockSMSGateway():
composer.action_send_sms()
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
def test_composer_partners_active_domain(self):
partners = self.partner_1 | self.partner_2
composer = self.env['sms.composer'].with_context(
active_model='res.partner',
active_domain=[('id', 'in', partners.ids)]
).create({
'message': self._test_body,
})
with self.mockSMSGateway():
composer.action_send_sms()
self.assertSMSSent(self.partner_numbers, self._test_body)
def test_composer_partners_sanitize(self):
partner_incorrect = self.env['res.partner'].create({
'name': 'Jean-Claude Incorrect',
'email': 'jean.claude@example.com',
'mobile': 'coincoin',
})
partners = self.partner_1 | self.partner_2 | partner_incorrect
composer = self.env['sms.composer'].with_context(
active_model='res.partner',
active_domain=[('id', 'in', partners.ids)]
).create({
'message': self._test_body,
})
with self.mockSMSGateway():
composer.action_send_sms()
# self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) # TDE FIXME: actually sanitizer does not work in current master (saas 12.23))
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sms.tests import common as sms_common
from odoo.addons.test_mail.tests import common as test_mail_common
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
@classmethod
def setUpClass(cls):
super(TestSMSPost, cls).setUpClass()
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
'name': 'Test',
'customer_id': cls.partner_1.id,
})
cls.test_record = cls._reset_mail_context(cls.test_record)
def test_message_post_with_sms_on_partners(self):
test_body = 'Void body'
with self.mockSMSGateway():
(self.partner_1 | self.partner_2).message_post_send_sms(test_body)
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
def test_message_post_with_sms_w_default(self):
test_body = 'Void body'
with self.mockSMSGateway():
self.test_record.message_post_send_sms(test_body)
self.assertSMSSent(self.partner_1.mapped('mobile'), test_body)
self.assertIn(test_body, self.test_record.message_ids.body)
def test_message_post_with_sms_w_numbers(self):
test_body = 'Void body'
test_numbers = ['0475114477', '0475225588']
with self.mockSMSGateway():
self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
self.assertSMSSent(test_numbers, test_body)
self.assertIn(test_body, self.test_record.message_ids.body)
def test_message_post_with_sms_w_numbers_duplicate(self):
test_body = 'Void body'
test_numbers = ['0475114477', '0475225588', '0475114477']
with self.mockSMSGateway():
self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
self.assertSMSSent(test_numbers, test_body)
self.assertIn(test_body, self.test_record.message_ids.body)
def test_message_post_with_sms_w_partners(self):
test_body = 'Void body'
with self.mockSMSGateway():
self.test_record.message_post_send_sms(test_body, partners=self.partner_1 | self.partner_2)
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
self.assertIn(test_body, self.test_record.message_ids.body)