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. suffix for SendGrid Substitution Tags.
``}`` is used by default. ``}`` is used by default.
* ``mail_sendgrid.send_method`` Use value 'sendgrid' to override the traditional SMTP server used to send e-mails with sendgrid. * ``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). (two buttons for sending either normally or with SendGrid).
In order to use this module, the following variables have to be defined in the 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', 'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)', 'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3', 'license': 'AGPL-3',
'website': 'http://www.compassion.ch',
'website': 'https://github.com/OCA/social',
'depends': ['mail_tracking'], 'depends': ['mail_tracking'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',

7
mail_sendgrid/controllers/sendgrid_event_webhook.py

@ -11,14 +11,13 @@ _logger = logging.getLogger(__name__)
class SendgridTrackingController(MailTrackingController): 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>', @http.route('/mail/tracking/sendgrid/<string:db>',
type='json', auth='none', csrf=False) type='json', auth='none', csrf=False)
def mail_tracking_sendgrid(self, db, **kw): def mail_tracking_sendgrid(self, db, **kw):
try: try:
_env_get(db, self._tracking_event, None, None, **kw) _env_get(db, self._tracking_event, None, None, **kw)
return {'status': 200} return {'status': 200}
except:
except Exception as e:
_logger.error(e.message, exc_info=True)
return {'status': 400} return {'status': 400}

4
mail_sendgrid/models/email_lang_template.py

@ -13,10 +13,10 @@ class LanguageTemplate(models.Model):
_name = 'sendgrid.email.lang.template' _name = 'sendgrid.email.lang.template'
email_template_id = fields.Many2one('mail.template', 'E-mail 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_id = fields.Many2one(
'sendgrid.template', 'Sendgrid Template', required=True) 'sendgrid.template', 'Sendgrid Template', required=True)
def _lang_get(self):
def _select_lang(self):
languages = self.env['res.lang'].search([]) languages = self.env['res.lang'].search([])
return [(language.code, language.name) for language in languages] 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). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api from odoo import models, fields, api
from collections import defaultdict
class EmailTemplate(models.Model): class EmailTemplate(models.Model):
_inherit = 'mail.template' _inherit = 'mail.template'
##########################################################################
# FIELDS #
##########################################################################
substitution_ids = fields.One2many( substitution_ids = fields.One2many(
'sendgrid.substitution', 'email_template_id', 'Substitutions') 'sendgrid.substitution', 'email_template_id', 'Substitutions')
sendgrid_template_ids = fields.One2many( sendgrid_template_ids = fields.One2many(
@ -30,25 +28,27 @@ class EmailTemplate(models.Model):
@api.multi @api.multi
def update_substitutions(self): 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 @api.multi
def render_substitutions(self, res_ids): def render_substitutions(self, res_ids):
@ -64,7 +64,7 @@ class EmailTemplate(models.Model):
res_ids = [res_ids] res_ids = [res_ids]
substitutions = self.substitution_ids.filtered( substitutions = self.substitution_ids.filtered(
lambda s: s.lang == self.env.context.get('lang', 'en_US')) 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: for substitution in substitutions:
values = self.render_template( values = self.render_template(
substitution.value, self.model, res_ids) 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' _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') @api.depends('tracking_event_ids')
def _compute_clicks(self): def _compute_clicks(self):
for mail in 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 @property
def _sendgrid_mandatory_fields(self): def _sendgrid_mandatory_fields(self):
return ('event', 'sg_event_id', 'timestamp',
'odoo_id', 'odoo_db')
return ('event', 'timestamp', 'odoo_id', 'odoo_db')
@property @property
def _sendgrid_event_type_mapping(self): def _sendgrid_event_type_mapping(self):
@ -56,7 +58,7 @@ class MailTrackingEmail(models.Model):
# OK, event type is valid # OK, event type is valid
return True return True
def _db_verify(self, event):
def _sendgrid_db_verify(self, event):
event = event or {} event = event or {}
odoo_db = event.get('odoo_db') odoo_db = event.get('odoo_db')
current_db = self.env.cr.dbname current_db = self.env.cr.dbname
@ -70,10 +72,10 @@ class MailTrackingEmail(models.Model):
def _sendgrid_metadata(self, sendgrid_event_type, event, metadata): def _sendgrid_metadata(self, sendgrid_event_type, event, metadata):
# Get sendgrid timestamp when found # Get sendgrid timestamp when found
ts = event.get('timestamp', False)
ts = event.get('timestamp')
try: try:
ts = float(ts) ts = float(ts)
except:
except ValueError:
ts = False ts = False
if ts: if ts:
dt = datetime.utcfromtimestamp(ts) dt = datetime.utcfromtimestamp(ts)
@ -102,10 +104,12 @@ class MailTrackingEmail(models.Model):
'android', 'iphone', 'ipad'] 'android', 'iphone', 'ipad']
}) })
# Mapping for special events # Mapping for special events
if sendgrid_event_type == 'bounced':
if sendgrid_event_type == 'bounce':
metadata.update({ metadata.update({
'error_type': event.get('type', False), 'error_type': event.get('type', False),
'bounce_type': event.get('type', False),
'error_description': event.get('reason', False), 'error_description': event.get('reason', False),
'bounce_description': event.get('reason', False),
'error_details': event.get('status', False), 'error_details': event.get('status', False),
}) })
elif sendgrid_event_type == 'dropped': elif sendgrid_event_type == 'dropped':
@ -138,7 +142,7 @@ class MailTrackingEmail(models.Model):
if self._event_is_from_sendgrid(event): if self._event_is_from_sendgrid(event):
if not self._sendgrid_event_type_verify(event): if not self._sendgrid_event_type_verify(event):
res = 'ERROR: Event type not supported' res = 'ERROR: Event type not supported'
elif not self._db_verify(event):
elif not self._sendgrid_db_verify(event):
res = 'ERROR: Invalid DB' res = 'ERROR: Invalid DB'
else: else:
res = 'OK' res = 'OK'
@ -149,14 +153,14 @@ class MailTrackingEmail(models.Model):
if not mapped_event_type: if not mapped_event_type:
res = 'ERROR: Bad event' res = 'ERROR: Bad event'
tracking = self._sendgrid_tracking_get(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' 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 res != 'NONE':
if event_type: if event_type:
_logger.info( _logger.info(

104
mail_sendgrid/models/mail_mail.py

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) # Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # 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.config import config
from odoo.tools.safe_eval import safe_eval from odoo.tools.safe_eval import safe_eval
@ -32,9 +32,6 @@ class MailMessage(models.Model):
""" """
_inherit = 'mail.message' _inherit = 'mail.message'
##########################################################################
# FIELDS #
##########################################################################
body_text = fields.Text(help='Text only version of the body') body_text = fields.Text(help='Text only version of the body')
sent_date = fields.Datetime(copy=False) sent_date = fields.Datetime(copy=False)
substitution_ids = fields.Many2many( substitution_ids = fields.Many2many(
@ -43,9 +40,6 @@ class MailMessage(models.Model):
'sendgrid.template', 'Sendgrid Template') 'sendgrid.template', 'Sendgrid Template')
send_method = fields.Char(compute='_compute_send_method') send_method = fields.Char(compute='_compute_send_method')
##########################################################################
# FIELDS METHODS #
##########################################################################
@api.multi @api.multi
def _compute_send_method(self): def _compute_send_method(self):
""" Check whether to use traditional send method, sendgrid or disable. """ Check whether to use traditional send method, sendgrid or disable.
@ -56,13 +50,10 @@ class MailMessage(models.Model):
email.send_method = send_method email.send_method = send_method
class OdooMail(models.Model):
class MailMail(models.Model):
""" Email message sent through SendGrid """ """ Email message sent through SendGrid """
_inherit = 'mail.mail' _inherit = 'mail.mail'
##########################################################################
# FIELDS #
##########################################################################
tracking_email_ids = fields.One2many( tracking_email_ids = fields.One2many(
'mail.tracking.email', 'mail_id', string='Registered events', 'mail.tracking.email', 'mail_id', string='Registered events',
readonly=True) readonly=True)
@ -77,75 +68,74 @@ class OdooMail(models.Model):
'tracking_email_ids.state') 'tracking_email_ids.state')
def _compute_tracking(self): def _compute_tracking(self):
for email in self: for email in self:
email.click_count = sum(email.tracking_email_ids.mapped(
click_count = sum(email.tracking_email_ids.mapped(
'click_count')) '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): def _compute_events(self):
for email in self: for email in self:
email.tracking_event_ids = email.tracking_email_ids.mapped( email.tracking_event_ids = email.tracking_email_ids.mapped(
'tracking_event_ids') 'tracking_event_ids')
##########################################################################
# PUBLIC METHODS #
##########################################################################
@api.multi @api.multi
def send(self, auto_commit=False, raise_exception=False): def send(self, auto_commit=False, raise_exception=False):
""" Override send to select the method to send the e-mail. """ """ Override send to select the method to send the e-mail. """
traditional = self.filtered(lambda e: e.send_method == 'traditional') traditional = self.filtered(lambda e: e.send_method == 'traditional')
sendgrid = self.filtered(lambda e: e.send_method == 'sendgrid') sendgrid = self.filtered(lambda e: e.send_method == 'sendgrid')
if traditional: if traditional:
super(OdooMail, traditional).send(auto_commit, raise_exception)
super(MailMail, traditional).send(auto_commit, raise_exception)
if sendgrid: if sendgrid:
sendgrid.send_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 return True
@api.multi @api.multi
def send_sendgrid(self): def send_sendgrid(self):
""" Use sendgrid transactional e-mails : e-mails are sent one by """ Use sendgrid transactional e-mails : e-mails are sent one by
one. """ one. """
outgoing = self.filtered(lambda em: em.state == 'outgoing')
api_key = config.get('sendgrid_api_key') 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) 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): def _prepare_sendgrid_data(self):
""" """
Prepare and creates the Sendgrid Email object Prepare and creates the Sendgrid Email object
@ -177,7 +167,7 @@ class OdooMail(models.Model):
p = re.compile(r'<.*?>') # Remove HTML markers p = re.compile(r'<.*?>') # Remove HTML markers
text_only = self.body_text or p.sub('', html.replace('<br/>', '\n')) 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)) s_mail.add_content(Content("text/html", html))
test_address = config.get('sendgrid_test_address') test_address = config.get('sendgrid_test_address')

2
mail_sendgrid/models/mail_tracking_event.py

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- 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). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api 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) self.detected_keywords = ';'.join(keywords)
@api.model @api.model
def update(self):
def update_templates(self):
api_key = config.get('sendgrid_api_key') api_key = config.get('sendgrid_api_key')
if not api_key: if not api_key:
raise exceptions.UserError( raise exceptions.UserError(
@ -77,8 +77,8 @@ class SendgridTemplate(models.Model):
def get_keywords(self): def get_keywords(self):
""" Search in the Sendgrid template for keywords included with the """ Search in the Sendgrid template for keywords included with the
following syntax: {keyword_name} and returns the list of keywords. 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 You can replace the substitution prefix and suffix by adding values
in the system parameters in the system parameters
- mail_sendgrid.substitution_prefix - mail_sendgrid.substitution_prefix
@ -92,5 +92,5 @@ class SendgridTemplate(models.Model):
suffix = params.search([ suffix = params.search([
('key', '=', 'mail_sendgrid.substitution_suffix') ('key', '=', 'mail_sendgrid.substitution_suffix')
]) or '}' ]) or '}'
pattern = prefix + r'\w{0,20}' + suffix
pattern = prefix + r'\S{1,50}' + suffix
return list(set(re.findall(pattern, self.html_content))) return list(set(re.findall(pattern, self.html_content)))

112
mail_sendgrid/tests/test_mail_sendgrid.py

@ -1,17 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.ch> # © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import json
import mock 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') '.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') '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): class FakeClient(object):
""" Mock Sendgrid APIClient """ """ Mock Sendgrid APIClient """
@ -33,7 +47,31 @@ class FakeRequest(object):
self.jsonrequest = [data] 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): def setUp(self):
super(TestMailSendgrid, self).setUp() super(TestMailSendgrid, self).setUp()
self.sendgrid_template = self.env['sendgrid.template'].create({ self.sendgrid_template = self.env['sendgrid.template'].create({
@ -56,7 +94,7 @@ class TestMailSendgrid(TransactionCase):
'composition_mode': 'comment', 'composition_mode': 'comment',
'model': 'res.partner', 'model': 'res.partner',
'res_id': self.recipient.id 'res_id': self.recipient.id
})
}).with_context(active_id=self.recipient.id)
self.mail_wizard.onchange_template_id_wrapper() self.mail_wizard.onchange_template_id_wrapper()
self.timestamp = u'1471021089' self.timestamp = u'1471021089'
self.event = { self.event = {
@ -80,7 +118,22 @@ class TestMailSendgrid(TransactionCase):
mail_vals['recipient_ids'] = [(6, 0, self.recipient.ids)] mail_vals['recipient_ids'] = [(6, 0, self.recipient.ids)]
if vals is not None: if vals is not None:
mail_vals.update(vals) 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): def test_substitutions(self):
""" Test substitutions in templates. """ """ Test substitutions in templates. """
@ -135,9 +188,11 @@ class TestMailSendgrid(TransactionCase):
""" Test various tracking events. """ """ Test various tracking events. """
self.env['ir.config_parameter'].set_param( self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'sendgrid') 'mail_sendgrid.send_method', 'sendgrid')
mail = self.create_email()
mock_sendgrid.return_value = FakeClient() mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = "ushuwejhfkj" m_config.get.return_value = "ushuwejhfkj"
# Send mail
mail = self.create_email()
mail.send() mail.send()
self.assertEqual(mock_sendgrid.called, True) self.assertEqual(mock_sendgrid.called, True)
self.assertEqual(mail.state, 'sent') self.assertEqual(mail.state, 'sent')
@ -176,3 +231,40 @@ class TestMailSendgrid(TransactionCase):
self.request, self.event, self.metadata) self.request, self.event, self.metadata)
self.assertEqual(mail_tracking.state, 'opened') self.assertEqual(mail_tracking.state, 'opened')
self.assertEqual(mail.click_count, 1) 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="model">mail.template</field>
<field name="inherit_id" ref="mail.email_template_form"/> <field name="inherit_id" ref="mail.email_template_form"/>
<field name="arch" type="xml"> <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"> <page string="SendGrid">
<group> <group>
<field name="sendgrid_template_ids"> <field name="sendgrid_template_ids">

2
mail_sendgrid/views/sendgrid_email_view.xml

@ -14,7 +14,7 @@
<field name="body_html" position="attributes"> <field name="body_html" position="attributes">
<attribute name="widget">html</attribute> <attribute name="widget">html</attribute>
</field> </field>
<xpath expr="//field[@name='attachment_ids']/.." position="after">
<xpath expr="//field[@name='attachment_ids']/ancestor::page" position="after">
<page string="SendGrid"> <page string="SendGrid">
<group> <group>
<field name="sendgrid_template_id"/> <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"> <record model="ir.actions.server" id="update_sendgrid_templates">
<field name="name">Update Sendgrid Templates</field> <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 = { action = {
'name': 'Sendgrid templates', 'name': 'Sendgrid templates',
'type': 'ir.actions.act_window', '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 """ """ Put the preview inside sendgrid template """
_inherit = 'email_template.preview' _inherit = 'email_template.preview'
@api.onchange('res_id')
@api.multi @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_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 sendgrid_template = template.sendgrid_localized_template
if sendgrid_template: if sendgrid_template:
body_html = sendgrid_template.html_content.replace(
self.body_html = sendgrid_template.html_content.replace(
'<%body%>', body_html) '<%body%>', body_html)
result['value']['body_html'] = body_html
return result 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 SendGrid for mass mailing
@ -11,18 +12,31 @@ e-emails (not to mix up with Sendgrid marketing campaigns)
Installation Installation
============ ============
This addon will be automatically installed when 'mail_sendgrid' and This addon will be automatically installed when 'mail_sendgrid' and
'mass_mailing' are both installed. 'mass_mailing' are both installed.
Configuration
=============
None
Usage Usage
===== =====
From mass mailing, you can use Sendgrid templates. 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 Known issues / Roadmap
====================== ======================
@ -32,19 +46,33 @@ Known issues / Roadmap
Bug Tracker 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 Credits
======= =======
Images
------
* Odoo Community Association: `Icon <https://odoo-community.org/logo.png>`_.
Contributors Contributors
------------ ------------
* Emanuel Cino <ecino@compassion.ch> * 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 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 mission is to support the collaborative development of Odoo features and
promote its widespread use. 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', 'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)', 'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3', 'license': 'AGPL-3',
'website': 'http://www.compassion.ch',
'website': 'https://github.com/OCA/social',
'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'], 'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'],
'data': [ 'data': [
'views/mass_mailing_view.xml' '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 # Trick to save html when taken from the e-mail template
html_copy = fields.Html( html_copy = fields.Html(
compute='_compute_sendgrid_view', inverse='_inverse_html_copy') 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() enable_unsubscribe = fields.Boolean()
unsubscribe_text = fields.Char( unsubscribe_text = fields.Char(
default='If you would like to unsubscribe and stop receiving these ' default='If you would like to unsubscribe and stop receiving these '
@ -88,41 +90,51 @@ class MassMailing(models.Model):
@api.multi @api.multi
def send_mail(self): 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 # use E-mail Template
res_ids = self.get_recipients()
res_ids = mailing.get_recipients()
if not res_ids: if not res_ids:
raise UserError(_('Please select recipients.')) 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( 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', 'state': 'done',
'sent_date': fields.Datetime.now(), '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> # © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock 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') '.SendGridAPIClient')
mock_config = ('openerp.addons.mail_sendgrid.models.mail_mail.'
mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'config') 'config')
@ -30,48 +30,70 @@ class FakeRequest(object):
self.jsonrequest = [data] 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', 'name': 'Test Template',
'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc', 'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc',
'html_content': u'<h1>Test Sendgrid</h1><%body%>{footer}' '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', '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', 'subject': 'Test e-mail',
'body_html': u'Dear ${object.name}, hello!', 'body_html': u'Dear ${object.name}, hello!',
'sendgrid_template_ids': [ 'sendgrid_template_ids': [
(0, 0, {'lang': 'en_US', 'sendgrid_template_id': (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', 'email_from': 'admin@yourcompany.example.com',
'name': 'Test Mass Mailing Sendgrid', 'name': 'Test Mass Mailing Sendgrid',
'mailing_model': 'res.partner', '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!', '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", '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>' 'odoo_id': u'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>'
} }
self.metadata = {
cls.metadata = {
'ip': '127.0.0.1', 'ip': '127.0.0.1',
'user_agent': False, 'user_agent': False,
'os_family': False, 'os_family': False,
'ua_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_sendgrid_api_client)
@mock.patch(mock_config) @mock.patch(mock_config)
@ -84,7 +106,20 @@ class TestMailSendgrid(TransactionCase):
'mail_sendgrid.send_method', 'sendgrid') 'mail_sendgrid.send_method', 'sendgrid')
mock_sendgrid.return_value = FakeClient() mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = 'we4iorujeriu' 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() emails = self.mass_mailing.send_mail()
# Dont delete emails sent
emails.write({'auto_delete': False})
self.assertEqual(len(emails), 1) self.assertEqual(len(emails), 1)
self.assertEqual(emails.state, 'outgoing') self.assertEqual(emails.state, 'outgoing')
self.assertEqual(emails.sendgrid_template_id.id, self.assertEqual(emails.sendgrid_template_id.id,
@ -98,7 +133,7 @@ class TestMailSendgrid(TransactionCase):
self.assertFalse(mail_tracking.state) self.assertFalse(mail_tracking.state)
stats = self.mass_mailing.statistics_ids stats = self.mass_mailing.statistics_ids
self.assertEqual(len(stats), 1) self.assertEqual(len(stats), 1)
self.assertFalse(stats.sent)
self.assertTrue(stats.sent)
# Test delivered # Test delivered
self.event.update({ self.event.update({
@ -113,11 +148,26 @@ class TestMailSendgrid(TransactionCase):
self.event.update({ self.event.update({
'event': 'click', 'event': 'click',
}) })
self.env['mail.tracking.email'].event_process( self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata) self.request, self.event, self.metadata)
self.assertEqual(emails.click_count, 1) self.assertEqual(emails.click_count, 1)
events = stats.tracking_event_ids events = stats.tracking_event_ids
self.assertEqual(len(events), 2) 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') 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="model">mail.mass_mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/> <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml"> <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"> <xpath expr="//field[@name='mailing_model']/.." position="after">
<field name="email_template_id" domain="[('model', '=', mailing_model), ('sendgrid_template_ids', '!=', False)]"/> <field name="email_template_id" domain="[('model', '=', mailing_model), ('sendgrid_template_ids', '!=', False)]"/>
<field name="lang" attrs="{'invisible': [('email_template_id', '=', False)]}"/> <field name="lang" attrs="{'invisible': [('email_template_id', '=', False)]}"/>

Loading…
Cancel
Save