Browse Source

Sendgrid code corrections for v10 + Add tests

pull/185/head
Emanuel Cino 7 years ago
parent
commit
824de6bde4
  1. 2
      mail_sendgrid/README.rst
  2. 2
      mail_sendgrid/__manifest__.py
  3. 7
      mail_sendgrid/controllers/sendgrid_event_webhook.py
  4. 4
      mail_sendgrid/models/email_lang_template.py
  5. 44
      mail_sendgrid/models/email_template.py
  6. 38
      mail_sendgrid/models/email_tracking.py
  7. 104
      mail_sendgrid/models/mail_mail.py
  8. 2
      mail_sendgrid/models/mail_tracking_event.py
  9. 8
      mail_sendgrid/models/sendgrid_template.py
  10. 112
      mail_sendgrid/tests/test_mail_sendgrid.py
  11. 2
      mail_sendgrid/views/email_template_view.xml
  12. 2
      mail_sendgrid/views/sendgrid_email_view.xml
  13. 5
      mail_sendgrid/views/sendgrid_template_view.xml
  14. 12
      mail_sendgrid/wizards/email_template_preview.py
  15. 50
      mail_sendgrid_mass_mailing/README.rst
  16. 2
      mail_sendgrid_mass_mailing/__manifest__.py
  17. 74
      mail_sendgrid_mass_mailing/models/mass_mailing.py
  18. 102
      mail_sendgrid_mass_mailing/tests/test_mass_mailing.py
  19. 6
      mail_sendgrid_mass_mailing/views/mass_mailing_view.xml

2
mail_sendgrid/README.rst

@ -35,7 +35,7 @@ You can add the following system parameters to configure the usage of SendGrid:
suffix for SendGrid Substitution Tags.
``}`` is used by default.
* ``mail_sendgrid.send_method`` Use value 'sendgrid' to override the traditional SMTP server used to send e-mails with sendgrid.
Use any other value to disable traditional e-mail sending. By default, SendGrid will co-exist with traditional system
By default, SendGrid will co-exist with traditional system
(two buttons for sending either normally or with SendGrid).
In order to use this module, the following variables have to be defined in the

2
mail_sendgrid/__manifest__.py

@ -7,7 +7,7 @@
'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'website': 'http://www.compassion.ch',
'website': 'https://github.com/OCA/social',
'depends': ['mail_tracking'],
'data': [
'security/ir.model.access.csv',

7
mail_sendgrid/controllers/sendgrid_event_webhook.py

@ -11,14 +11,13 @@ _logger = logging.getLogger(__name__)
class SendgridTrackingController(MailTrackingController):
"""
Sendgrid is posting JSON so we must define a new route for tracking.
"""
"""Sendgrid is posting JSON so we must define a new route for tracking."""
@http.route('/mail/tracking/sendgrid/<string:db>',
type='json', auth='none', csrf=False)
def mail_tracking_sendgrid(self, db, **kw):
try:
_env_get(db, self._tracking_event, None, None, **kw)
return {'status': 200}
except:
except Exception as e:
_logger.error(e.message, exc_info=True)
return {'status': 400}

4
mail_sendgrid/models/email_lang_template.py

@ -13,10 +13,10 @@ class LanguageTemplate(models.Model):
_name = 'sendgrid.email.lang.template'
email_template_id = fields.Many2one('mail.template', 'E-mail Template')
lang = fields.Selection('_lang_get', 'Language', required=True)
lang = fields.Selection('_select_lang', 'Language', required=True)
sendgrid_template_id = fields.Many2one(
'sendgrid.template', 'Sendgrid Template', required=True)
def _lang_get(self):
def _select_lang(self):
languages = self.env['res.lang'].search([])
return [(language.code, language.name) for language in languages]

44
mail_sendgrid/models/email_template.py

@ -3,14 +3,12 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from collections import defaultdict
class EmailTemplate(models.Model):
_inherit = 'mail.template'
##########################################################################
# FIELDS #
##########################################################################
substitution_ids = fields.One2many(
'sendgrid.substitution', 'email_template_id', 'Substitutions')
sendgrid_template_ids = fields.One2many(
@ -30,25 +28,27 @@ class EmailTemplate(models.Model):
@api.multi
def update_substitutions(self):
self.ensure_one()
new_substitutions = list()
for language_template in self.sendgrid_template_ids:
sendgrid_template = language_template.sendgrid_template_id
lang = language_template.lang
substitutions = self.substitution_ids.filtered(
lambda s: s.lang == lang)
keywords = sendgrid_template.get_keywords()
# Add new keywords from the sendgrid template
for key in keywords:
if key not in substitutions.mapped('key'):
substitution_vals = {
'key': key,
'lang': lang,
'email_template_id': self.id
}
new_substitutions.append((0, 0, substitution_vals))
for template in self:
new_substitutions = []
for language_template in template.sendgrid_template_ids:
sendgrid_template = language_template.sendgrid_template_id
lang = language_template.lang
substitutions = template.substitution_ids.filtered(
lambda s: s.lang == lang)
keywords = sendgrid_template.get_keywords()
# Add new keywords from the sendgrid template
for key in keywords:
if key not in substitutions.mapped('key'):
substitution_vals = {
'key': key,
'lang': lang,
'email_template_id': template.id
}
new_substitutions.append((0, 0, substitution_vals))
template.write({'substitution_ids': new_substitutions})
return self.write({'substitution_ids': new_substitutions})
return True
@api.multi
def render_substitutions(self, res_ids):
@ -64,7 +64,7 @@ class EmailTemplate(models.Model):
res_ids = [res_ids]
substitutions = self.substitution_ids.filtered(
lambda s: s.lang == self.env.context.get('lang', 'en_US'))
substitution_vals = {res_id: list() for res_id in res_ids}
substitution_vals = defaultdict(list)
for substitution in substitutions:
values = self.render_template(
substitution.value, self.model, res_ids)

38
mail_sendgrid/models/email_tracking.py

@ -17,18 +17,20 @@ class MailTrackingEmail(models.Model):
"""
_inherit = 'mail.tracking.email'
click_count = fields.Integer(compute='_compute_clicks', store=True)
click_count = fields.Integer(
compute='_compute_clicks', store=True, readonly=True)
@api.depends('tracking_event_ids')
def _compute_clicks(self):
for mail in self:
mail.click_count = len(mail.tracking_event_ids.filtered(
lambda event: event.event_type == 'click'))
mail.click_count = self.env['mail.tracking.event'].search_count([
('event_type', '=', 'click'),
('tracking_email_id', '=', mail.id)
])
@property
def _sendgrid_mandatory_fields(self):
return ('event', 'sg_event_id', 'timestamp',
'odoo_id', 'odoo_db')
return ('event', 'timestamp', 'odoo_id', 'odoo_db')
@property
def _sendgrid_event_type_mapping(self):
@ -56,7 +58,7 @@ class MailTrackingEmail(models.Model):
# OK, event type is valid
return True
def _db_verify(self, event):
def _sendgrid_db_verify(self, event):
event = event or {}
odoo_db = event.get('odoo_db')
current_db = self.env.cr.dbname
@ -70,10 +72,10 @@ class MailTrackingEmail(models.Model):
def _sendgrid_metadata(self, sendgrid_event_type, event, metadata):
# Get sendgrid timestamp when found
ts = event.get('timestamp', False)
ts = event.get('timestamp')
try:
ts = float(ts)
except:
except ValueError:
ts = False
if ts:
dt = datetime.utcfromtimestamp(ts)
@ -102,10 +104,12 @@ class MailTrackingEmail(models.Model):
'android', 'iphone', 'ipad']
})
# Mapping for special events
if sendgrid_event_type == 'bounced':
if sendgrid_event_type == 'bounce':
metadata.update({
'error_type': event.get('type', False),
'bounce_type': event.get('type', False),
'error_description': event.get('reason', False),
'bounce_description': event.get('reason', False),
'error_details': event.get('status', False),
})
elif sendgrid_event_type == 'dropped':
@ -138,7 +142,7 @@ class MailTrackingEmail(models.Model):
if self._event_is_from_sendgrid(event):
if not self._sendgrid_event_type_verify(event):
res = 'ERROR: Event type not supported'
elif not self._db_verify(event):
elif not self._sendgrid_db_verify(event):
res = 'ERROR: Invalid DB'
else:
res = 'OK'
@ -149,14 +153,14 @@ class MailTrackingEmail(models.Model):
if not mapped_event_type:
res = 'ERROR: Bad event'
tracking = self._sendgrid_tracking_get(event)
if not tracking:
if tracking:
# Complete metadata with sendgrid event info
metadata = self._sendgrid_metadata(
sendgrid_event_type, event, metadata)
# Create event
tracking.event_create(mapped_event_type, metadata)
else:
res = 'ERROR: Tracking not found'
if res == 'OK':
# Complete metadata with sendgrid event info
metadata = self._sendgrid_metadata(
sendgrid_event_type, event, metadata)
# Create event
tracking.event_create(mapped_event_type, metadata)
if res != 'NONE':
if event_type:
_logger.info(

104
mail_sendgrid/models/mail_mail.py

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, exceptions, tools, _
from odoo import models, fields, api, tools
from odoo.tools.config import config
from odoo.tools.safe_eval import safe_eval
@ -32,9 +32,6 @@ class MailMessage(models.Model):
"""
_inherit = 'mail.message'
##########################################################################
# FIELDS #
##########################################################################
body_text = fields.Text(help='Text only version of the body')
sent_date = fields.Datetime(copy=False)
substitution_ids = fields.Many2many(
@ -43,9 +40,6 @@ class MailMessage(models.Model):
'sendgrid.template', 'Sendgrid Template')
send_method = fields.Char(compute='_compute_send_method')
##########################################################################
# FIELDS METHODS #
##########################################################################
@api.multi
def _compute_send_method(self):
""" Check whether to use traditional send method, sendgrid or disable.
@ -56,13 +50,10 @@ class MailMessage(models.Model):
email.send_method = send_method
class OdooMail(models.Model):
class MailMail(models.Model):
""" Email message sent through SendGrid """
_inherit = 'mail.mail'
##########################################################################
# FIELDS #
##########################################################################
tracking_email_ids = fields.One2many(
'mail.tracking.email', 'mail_id', string='Registered events',
readonly=True)
@ -77,75 +68,74 @@ class OdooMail(models.Model):
'tracking_email_ids.state')
def _compute_tracking(self):
for email in self:
email.click_count = sum(email.tracking_email_ids.mapped(
click_count = sum(email.tracking_email_ids.mapped(
'click_count'))
opened = len(email.tracking_email_ids.filtered(
lambda t: t.state == 'opened'))
email.opened = opened > 0
opened = self.env['mail.tracking.email'].search_count([
('state', '=', 'opened'),
('mail_id', '=', email.id)
])
email.update({
'click_count': click_count,
'opened': opened > 0
})
def _compute_events(self):
for email in self:
email.tracking_event_ids = email.tracking_email_ids.mapped(
'tracking_event_ids')
##########################################################################
# PUBLIC METHODS #
##########################################################################
@api.multi
def send(self, auto_commit=False, raise_exception=False):
""" Override send to select the method to send the e-mail. """
traditional = self.filtered(lambda e: e.send_method == 'traditional')
sendgrid = self.filtered(lambda e: e.send_method == 'sendgrid')
if traditional:
super(OdooMail, traditional).send(auto_commit, raise_exception)
super(MailMail, traditional).send(auto_commit, raise_exception)
if sendgrid:
sendgrid.send_sendgrid()
unknown = self - traditional - sendgrid
if unknown:
_logger.warning(
"Traditional e-mails are disabled. Please remove system "
"parameter mail_sendgrid.send_method if you want to send "
"e-mails through your configured SMTP.")
unknown.write({'state': 'exception'})
return True
@api.multi
def send_sendgrid(self):
""" Use sendgrid transactional e-mails : e-mails are sent one by
one. """
outgoing = self.filtered(lambda em: em.state == 'outgoing')
api_key = config.get('sendgrid_api_key')
if not api_key:
raise exceptions.UserError(
_('Missing sendgrid_api_key in conf file'))
if outgoing and not api_key:
_logger.error(
'Missing sendgrid_api_key in conf file. Skipping Sendgrid '
'send.'
)
return
sg = SendGridAPIClient(apikey=api_key)
for email in self.filtered(lambda em: em.state == 'outgoing'):
# Commit at each e-mail processed to avoid any errors
# invalidating state.
with self.env.cr.savepoint():
try:
response = sg.client.mail.send.post(
request_body=email._prepare_sendgrid_data().get())
except Exception as e:
_logger.error(e.message)
continue
status = response.status_code
msg = response.body
if status == STATUS_OK:
_logger.info(str(msg))
email._track_sendgrid_emails()
email.write({
'sent_date': fields.Datetime.now(),
'state': 'sent'
})
else:
_logger.error("Failed to send email: {}".format(str(msg)))
##########################################################################
# PRIVATE METHODS #
##########################################################################
for email in outgoing:
try:
response = sg.client.mail.send.post(
request_body=email._prepare_sendgrid_data().get())
except Exception as e:
_logger.error(e.message or "mail not sent.")
continue
status = response.status_code
msg = response.body
if status == STATUS_OK:
_logger.info("e-mail sent. " + str(msg))
email._track_sendgrid_emails()
email.write({
'sent_date': fields.Datetime.now(),
'state': 'sent'
})
if not self.env.context.get('test_mode'):
# Commit at each e-mail processed to avoid any errors
# invalidating state.
self.env.cr.commit() # pylint: disable=invalid-commit
email._postprocess_sent_message(mail_sent=True)
else:
email._postprocess_sent_message(mail_sent=False)
_logger.error("Failed to send email: {}".format(str(msg)))
def _prepare_sendgrid_data(self):
"""
Prepare and creates the Sendgrid Email object
@ -177,7 +167,7 @@ class OdooMail(models.Model):
p = re.compile(r'<.*?>') # Remove HTML markers
text_only = self.body_text or p.sub('', html.replace('<br/>', '\n'))
s_mail.add_content(Content("text/plain", text_only))
s_mail.add_content(Content("text/plain", text_only or ' '))
s_mail.add_content(Content("text/html", html))
test_address = config.get('sendgrid_test_address')

2
mail_sendgrid/models/mail_tracking_event.py

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.com>
# Copyright 2017 Emanuel Cino - <ecino@compassion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api

8
mail_sendgrid/models/sendgrid_template.py

@ -39,7 +39,7 @@ class SendgridTemplate(models.Model):
self.detected_keywords = ';'.join(keywords)
@api.model
def update(self):
def update_templates(self):
api_key = config.get('sendgrid_api_key')
if not api_key:
raise exceptions.UserError(
@ -77,8 +77,8 @@ class SendgridTemplate(models.Model):
def get_keywords(self):
""" Search in the Sendgrid template for keywords included with the
following syntax: {keyword_name} and returns the list of keywords.
keyword_name shouldn't be longer than 20 characters and only contain
alphanumeric characters (underscore is allowed).
keyword_name shouldn't be longer than 50 characters and not contain
whitespaces.
You can replace the substitution prefix and suffix by adding values
in the system parameters
- mail_sendgrid.substitution_prefix
@ -92,5 +92,5 @@ class SendgridTemplate(models.Model):
suffix = params.search([
('key', '=', 'mail_sendgrid.substitution_suffix')
]) or '}'
pattern = prefix + r'\w{0,20}' + suffix
pattern = prefix + r'\S{1,50}' + suffix
return list(set(re.findall(pattern, self.html_content)))

112
mail_sendgrid/tests/test_mail_sendgrid.py

@ -1,17 +1,31 @@
# -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import json
import mock
from odoo.tests.common import TransactionCase
from odoo.tests.common import HttpCase
from ..controllers.json_request import RESTJsonRequest
mock_base_send = 'openerp.addons.mail.models.mail_mail.MailMail.send'
mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail'
mock_base_send = 'odoo.addons.mail.models.mail_mail.MailMail.send'
mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail'
'.SendGridAPIClient')
mock_sendgrid_send = ('openerp.addons.mail_sendgrid.models.mail_mail.'
'OdooMail.send_sendgrid')
mock_config = ('openerp.addons.mail_sendgrid.models.mail_mail.'
mock_sendgrid_send = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'MailMail.send_sendgrid')
mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'config')
mock_config_template = ('odoo.addons.mail_sendgrid.models.sendgrid_template.'
'config')
mock_template_api_client = ('odoo.addons.mail_sendgrid.models.'
'sendgrid_template.sendgrid.SendGridAPIClient')
mock_json_request = 'odoo.http.Root.get_request'
def side_effect_json(http_request):
return RESTJsonRequest(http_request)
class FakeClient(object):
""" Mock Sendgrid APIClient """
@ -33,7 +47,31 @@ class FakeRequest(object):
self.jsonrequest = [data]
class TestMailSendgrid(TransactionCase):
class FakeTemplateClient(object):
""" Simulate the Sendgrid Template api"""
def __init__(self):
self.client = self
self.templates = self
self.body = json.dumps({
"templates": [{
"id": "fake_id",
"name": "Fake Template"
}],
"versions": [{
"active": True,
"html_content": "<h1>fake</h1>",
"plain_content": "fake",
}],
})
def get(self):
return self
def _(self, id):
return self
class TestMailSendgrid(HttpCase):
def setUp(self):
super(TestMailSendgrid, self).setUp()
self.sendgrid_template = self.env['sendgrid.template'].create({
@ -56,7 +94,7 @@ class TestMailSendgrid(TransactionCase):
'composition_mode': 'comment',
'model': 'res.partner',
'res_id': self.recipient.id
})
}).with_context(active_id=self.recipient.id)
self.mail_wizard.onchange_template_id_wrapper()
self.timestamp = u'1471021089'
self.event = {
@ -80,7 +118,22 @@ class TestMailSendgrid(TransactionCase):
mail_vals['recipient_ids'] = [(6, 0, self.recipient.ids)]
if vals is not None:
mail_vals.update(vals)
return self.env['mail.mail'].create(mail_vals)
return self.env['mail.mail'].with_context(test_mode=True).create(
mail_vals)
def test_preview(self):
"""
Test the preview email_template is getting the Sendgrid template
"""
preview_wizard = self.env['email_template.preview'].with_context(
template_id=self.mail_template.id,
default_res_id=self.recipient.id
).create({})
# For a strange reason, res_id is converted to string
preview_wizard.res_id = self.recipient.id
preview_wizard.on_change_res_id()
self.assertIn(u'<h1>Test Sendgrid</h1>', preview_wizard.body_html)
self.assertIn(self.recipient.name, preview_wizard.body_html)
def test_substitutions(self):
""" Test substitutions in templates. """
@ -135,9 +188,11 @@ class TestMailSendgrid(TransactionCase):
""" Test various tracking events. """
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'sendgrid')
mail = self.create_email()
mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = "ushuwejhfkj"
# Send mail
mail = self.create_email()
mail.send()
self.assertEqual(mock_sendgrid.called, True)
self.assertEqual(mail.state, 'sent')
@ -176,3 +231,40 @@ class TestMailSendgrid(TransactionCase):
self.request, self.event, self.metadata)
self.assertEqual(mail_tracking.state, 'opened')
self.assertEqual(mail.click_count, 1)
# Test events are linked to e-mail
self.assertEquals(len(mail.tracking_event_ids), 4)
def test_controller(self):
""" Check the controller is working """
event_data = [self.event]
with mock.patch(mock_json_request,
side_effect=side_effect_json) as json_mock:
json_mock.return_value = True
result = self.url_open(
'/mail/tracking/sendgrid/' + self.session.db,
json.dumps(event_data)
)
self.assertTrue(json_mock.called)
self.assertTrue(result)
# Invalid request
self.url_open(
'/mail/tracking/sendgrid/' + self.session.db,
"[{'invalid': True}]"
)
@mock.patch(mock_template_api_client)
@mock.patch(mock_config_template)
def test_update_templates(self, m_config, m_sendgrid):
m_config.return_value = "ldkfjsOIWJRksfj"
m_sendgrid.return_value = FakeTemplateClient()
self.env['sendgrid.template'].update_templates()
template = self.env['sendgrid.template'].search([
('remote_id', '=', 'fake_id')
])
self.assertTrue(template)
def tearDown(self):
super(TestMailSendgrid, self).tearDown()
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'traditional')

2
mail_sendgrid/views/email_template_view.xml

@ -6,7 +6,7 @@
<field name="model">mail.template</field>
<field name="inherit_id" ref="mail.email_template_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email_from']/../.." position="after">
<xpath expr="//field[@name='email_from']/ancestor::page" position="after">
<page string="SendGrid">
<group>
<field name="sendgrid_template_ids">

2
mail_sendgrid/views/sendgrid_email_view.xml

@ -14,7 +14,7 @@
<field name="body_html" position="attributes">
<attribute name="widget">html</attribute>
</field>
<xpath expr="//field[@name='attachment_ids']/.." position="after">
<xpath expr="//field[@name='attachment_ids']/ancestor::page" position="after">
<page string="SendGrid">
<group>
<field name="sendgrid_template_id"/>

5
mail_sendgrid/views/sendgrid_template_view.xml

@ -48,8 +48,9 @@
<record model="ir.actions.server" id="update_sendgrid_templates">
<field name="name">Update Sendgrid Templates</field>
<field name="model_id" ref="model_sendgrid_template"/>
<field name="code">object.update()
<field name="model_id" ref="mail_sendgrid.model_sendgrid_template"/>
<field name="code">
env['sendgrid.template'].update_templates()
action = {
'name': 'Sendgrid templates',
'type': 'ir.actions.act_window',

12
mail_sendgrid/wizards/email_template_preview.py

@ -9,15 +9,15 @@ class EmailTemplatePreview(models.TransientModel):
""" Put the preview inside sendgrid template """
_inherit = 'email_template.preview'
@api.onchange('res_id')
@api.multi
def on_change_res_id(self, res_id):
result = super(EmailTemplatePreview, self).on_change_res_id(res_id)
body_html = result['value']['body_html']
def on_change_res_id(self):
result = super(EmailTemplatePreview, self).on_change_res_id()
body_html = self.body_html
template_id = self.env.context.get('template_id')
template = self.env['sendgrid'].browse(template_id)
template = self.env['mail.template'].browse(template_id)
sendgrid_template = template.sendgrid_localized_template
if sendgrid_template:
body_html = sendgrid_template.html_content.replace(
self.body_html = sendgrid_template.html_content.replace(
'<%body%>', body_html)
result['value']['body_html'] = body_html
return result

50
mail_sendgrid_mass_mailing/README.rst

@ -1,5 +1,6 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-3
=========================
SendGrid for mass mailing
@ -11,18 +12,31 @@ e-emails (not to mix up with Sendgrid marketing campaigns)
Installation
============
This addon will be automatically installed when 'mail_sendgrid' and
'mass_mailing' are both installed.
Configuration
=============
None
Usage
=====
From mass mailing, you can use Sendgrid templates.
- If you select a Sendgrid template, the campaign will be sent through
Sendgrid. Otherwise it will use what you set in your system preference
(see module sendgrid).
- You can force usage of a language for the template.
#. If you select a Sendgrid template, the campaign will be sent through
Sendgrid. Otherwise it will use what you set in your system preference
(see module sendgrid).
#. You can force usage of a language for the template.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/10.0
.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt
.. branch is "8.0" for example
Known issues / Roadmap
======================
@ -32,19 +46,33 @@ Known issues / Roadmap
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/social/issues/new?body=module:%20mail_sendgrid_mass_mailing%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://odoo-community.org/logo.png>`_.
Contributors
------------
* Emanuel Cino <ecino@compassion.ch>
Do not contact contributors directly about support or help with technical issues.
Funders
-------
The development of this module has been financially supported by:
* Compassion Switzerland
Maintainer
----------
@ -58,4 +86,4 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.
To contribute to this module, please visit https://odoo-community.org.

2
mail_sendgrid_mass_mailing/__manifest__.py

@ -7,7 +7,7 @@
'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'website': 'http://www.compassion.ch',
'website': 'https://github.com/OCA/social',
'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'],
'data': [
'views/mass_mailing_view.xml'

74
mail_sendgrid_mass_mailing/models/mass_mailing.py

@ -23,6 +23,8 @@ class MassMailing(models.Model):
# Trick to save html when taken from the e-mail template
html_copy = fields.Html(
compute='_compute_sendgrid_view', inverse='_inverse_html_copy')
# Trick to display another widget when using Sendgrid
html_unframe = fields.Html(related='body_html')
enable_unsubscribe = fields.Boolean()
unsubscribe_text = fields.Char(
default='If you would like to unsubscribe and stop receiving these '
@ -88,41 +90,51 @@ class MassMailing(models.Model):
@api.multi
def send_mail(self):
self.ensure_one()
if self.email_template_id:
sendgrid = self.filtered('email_template_id')
emails = self.env['mail.mail']
for mailing in sendgrid:
# use E-mail Template
res_ids = self.get_recipients()
res_ids = mailing.get_recipients()
if not res_ids:
raise UserError(_('Please select recipients.'))
template = self.email_template_id
composer_values = {
'template_id': template.id,
'composition_mode': 'mass_mail',
'model': template.model,
'author_id': self.env.user.partner_id.id,
'res_id': res_ids[0],
'attachment_ids': [(4, attachment.id) for attachment in
self.attachment_ids],
'email_from': self.email_from,
'body': self.body_html,
'subject': self.name,
'record_name': False,
'mass_mailing_id': self.id,
'mailing_list_ids': [(4, l.id) for l in
self.contact_list_ids],
'no_auto_thread': self.reply_to_mode != 'thread',
}
if self.reply_to_mode == 'email':
composer_values['reply_to'] = self.reply_to
lang = mailing.lang.code or self.env.context.get('lang', 'en_US')
mailing = mailing.with_context(lang=lang)
composer_values = mailing._send_mail_get_composer_values()
if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to
composer = self.env['mail.compose.message'].with_context(
lang=self.lang.code or self.env.context.get('lang', 'en_US'),
active_ids=res_ids)
emails = composer.mass_mailing_sendgrid(res_ids, composer_values)
self.write({
lang=lang, active_ids=res_ids)
emails += composer.mass_mailing_sendgrid(res_ids, composer_values)
mailing.write({
'state': 'done',
'sent_date': fields.Datetime.now(),
})
return emails
else:
# Traditional sending
return super(MassMailing, self).send_mail()
# Traditional sending
super(MassMailing, self - sendgrid).send_mail()
return emails
def _send_mail_get_composer_values(self):
"""
Get the values used for the mail.compose.message wizard that will
generate the e-mails of a mass mailing campaign.
:return: dictionary of mail.compose.message values
"""
template = self.email_template_id
author = self.mass_mailing_campaign_id.user_id.partner_id or \
self.env.user.partner_id
return {
'template_id': template.id,
'composition_mode': 'mass_mail',
'model': template.model,
'author_id': author.id,
'attachment_ids': [(4, attachment.id) for attachment in
self.attachment_ids],
'email_from': self.email_from,
'body': self.body_html,
'subject': self.name,
'record_name': False,
'mass_mailing_id': self.id,
'mailing_list_ids': [(4, l.id) for l in
self.contact_list_ids],
'no_auto_thread': self.reply_to_mode != 'thread',
}

102
mail_sendgrid_mass_mailing/tests/test_mass_mailing.py

@ -2,11 +2,11 @@
# © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail'
mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail'
'.SendGridAPIClient')
mock_config = ('openerp.addons.mail_sendgrid.models.mail_mail.'
mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'config')
@ -30,48 +30,70 @@ class FakeRequest(object):
self.jsonrequest = [data]
class TestMailSendgrid(TransactionCase):
def setUp(self):
super(TestMailSendgrid, self).setUp()
self.sendgrid_template = self.env['sendgrid.template'].create({
class TestMailSendgrid(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestMailSendgrid, cls).setUpClass()
cls.sendgrid_template = cls.env['sendgrid.template'].create({
'name': 'Test Template',
'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc',
'html_content': u'<h1>Test Sendgrid</h1><%body%>{footer}'
})
self.mail_template = self.env['mail.template'].create({
cls.mail_template = cls.env['mail.template'].create({
'name': 'Test Template',
'model_id': self.env.ref('base.model_res_partner').id,
'model_id': cls.env.ref('base.model_res_partner').id,
'subject': 'Test e-mail',
'body_html': u'Dear ${object.name}, hello!',
'sendgrid_template_ids': [
(0, 0, {'lang': 'en_US', 'sendgrid_template_id':
self.sendgrid_template.id})]
cls.sendgrid_template.id})]
})
self.recipient = self.env.ref('base.partner_demo')
self.mass_mailing = self.env['mail.mass_mailing'].create({
cls.recipient = cls.env.ref('base.partner_demo')
cls.mass_mailing = cls.env['mail.mass_mailing'].create({
'email_from': 'admin@yourcompany.example.com',
'name': 'Test Mass Mailing Sendgrid',
'mailing_model': 'res.partner',
'mailing_domain': "[('id', '=', %d)]" % self.recipient.id,
'email_template_id': self.mail_template.id,
'mailing_domain': "[('id', '=', %d)]" % cls.recipient.id,
'email_template_id': cls.mail_template.id,
'body_html': u'Dear ${object.name}, hello!',
'reply_to_mode': 'thread',
})
self.timestamp = u'1471021089'
self.event = {
'timestamp': self.timestamp,
'reply_to_mode': 'email',
'enable_unsubscribe': True,
'unsubscribe_tag': '[unsub]'
}).with_context(lang='en_US', test_mode=True)
cls.timestamp = u'1471021089'
cls.event = {
'timestamp': cls.timestamp,
'sg_event_id': u"f_JoKtrLQaOXUc4thXgROg",
'email': self.recipient.email,
'odoo_db': self.env.cr.dbname,
'email': cls.recipient.email,
'odoo_db': cls.env.cr.dbname,
'odoo_id': u'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>'
}
self.metadata = {
cls.metadata = {
'ip': '127.0.0.1',
'user_agent': False,
'os_family': False,
'ua_family': False,
}
self.request = FakeRequest(self.event)
cls.request = FakeRequest(cls.event)
def test_sendgrid_preview(self):
"""
Test the preview field is getting the Sendgrid template
"""
self.mass_mailing.html_copy = self.mass_mailing.body_html
preview = self.mass_mailing.body_sendgrid
self.assertIn(u'<h1>Test Sendgrid</h1>', preview)
self.assertIn('hello!', preview)
def test_change_language(self):
"""
Test changing the language is changing the domain
"""
domain = self.mass_mailing.mailing_domain
self.mass_mailing.lang = self.env['res.lang'].search([], limit=1)
self.mass_mailing.onchange_lang()
self.assertTrue(len(self.mass_mailing.mailing_domain) > len(domain))
@mock.patch(mock_sendgrid_api_client)
@mock.patch(mock_config)
@ -84,7 +106,20 @@ class TestMailSendgrid(TransactionCase):
'mail_sendgrid.send_method', 'sendgrid')
mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = 'we4iorujeriu'
# Test campaign
self.mass_mailing.action_test_mailing()
self.env['mail.mass_mailing.test'].create({
'mass_mailing_id': self.mass_mailing.id,
'email_to': 'test@sendgrid.com'
}).with_context(lang='en_US', test_mode=True).send_mail_test()
self.assertTrue(mock_sendgrid.called)
mock_sendgrid.reset_mock()
# Send campaign
emails = self.mass_mailing.send_mail()
# Dont delete emails sent
emails.write({'auto_delete': False})
self.assertEqual(len(emails), 1)
self.assertEqual(emails.state, 'outgoing')
self.assertEqual(emails.sendgrid_template_id.id,
@ -98,7 +133,7 @@ class TestMailSendgrid(TransactionCase):
self.assertFalse(mail_tracking.state)
stats = self.mass_mailing.statistics_ids
self.assertEqual(len(stats), 1)
self.assertFalse(stats.sent)
self.assertTrue(stats.sent)
# Test delivered
self.event.update({
@ -113,11 +148,26 @@ class TestMailSendgrid(TransactionCase):
self.event.update({
'event': 'click',
})
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(emails.click_count, 1)
events = stats.tracking_event_ids
self.assertEqual(len(events), 2)
self.assertEqual(events[0].event_type, 'delivered')
self.assertEqual(events[1].event_type, 'click')
self.assertIn('delivered', events.mapped('event_type'))
self.assertIn('click', events.mapped('event_type'))
self.assertEqual(stats.state, 'sent')
# Test reject
self.event.update({
'event': 'dropped',
})
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(stats.state, 'exception')
@classmethod
def tearDownClass(cls):
cls.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'traditional')
super(TestMailSendgrid, cls).tearDownClass()

6
mail_sendgrid_mass_mailing/views/mass_mailing_view.xml

@ -6,6 +6,12 @@
<field name="model">mail.mass_mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml">
<field name="body_html" position="attributes">
<attribute name="attrs">{'invisible': [('email_template_id', '!=', False)]}</attribute>
</field>
<field name="body_html" position="after">
<field name="html_unframe" widget="html" attrs="{'invisible': [('email_template_id', '=', False)]}"/>
</field>
<xpath expr="//field[@name='mailing_model']/.." position="after">
<field name="email_template_id" domain="[('model', '=', mailing_model), ('sendgrid_template_ids', '!=', False)]"/>
<field name="lang" attrs="{'invisible': [('email_template_id', '=', False)]}"/>

Loading…
Cancel
Save