From 1372ef21ebab5a653c9b1e51b3155d5b354d827d Mon Sep 17 00:00:00 2001 From: medmunds Date: Sat, 30 Apr 2016 10:21:11 -0700 Subject: [PATCH] SendGrid: merge 'filters' in esp_extra Previously, setting esp_extra['x-smtpapi']['filters'] would override the entire filters setting, potentially undoing other Anymail options that use SendGrid filters (like track_opens). Now, 'filters' is special-cased, and merged with any other Anymail filter options. (We don't do a fully deep merge, because otherwise there would be no way to use esp_extra to *clear* Anymail settings.) --- anymail/backends/sendgrid.py | 8 +++++++- docs/esps/sendgrid.rst | 11 ++++++++++- tests/test_sendgrid_backend.py | 21 +++++++++++++++++++-- tests/utils.py | 30 ++++++++++++++++-------------- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/anymail/backends/sendgrid.py b/anymail/backends/sendgrid.py index 56b45bb..e97e319 100644 --- a/anymail/backends/sendgrid.py +++ b/anymail/backends/sendgrid.py @@ -91,7 +91,13 @@ class SendGridPayload(RequestsPayload): # If esp_extra was also used to set x-smtpapi, need to merge it if "x-smtpapi" in self.data: esp_extra_smtpapi = self.data["x-smtpapi"] - self.smtpapi.update(esp_extra_smtpapi) # need to make this deep merge (for filters)! + for key, value in esp_extra_smtpapi.items(): + if key == "filters": + # merge filters (else it's difficult to mix esp_extra with other features) + self.smtpapi.setdefault(key, {}).update(value) + else: + # all other keys replace any current value + self.smtpapi[key] = value self.data["x-smtpapi"] = self.serialize_json(self.smtpapi) elif "x-smtpapi" in self.data: self.data["x-smtpapi"] = self.serialize_json(self.data["x-smtpapi"]) diff --git a/docs/esps/sendgrid.rst b/docs/esps/sendgrid.rst index bedf991..e9decca 100644 --- a/docs/esps/sendgrid.rst +++ b/docs/esps/sendgrid.rst @@ -117,7 +117,16 @@ Example: message.esp_extra = { 'x-smtpapi': { "asm_group": 1, # Assign SendGrid unsubscribe group for this message - "asm_groups_to_display": [1, 2, 3] + "asm_groups_to_display": [1, 2, 3], + "filters": { + "subscriptiontrack": { # Insert SendGrid subscription management links + "settings": { + "text/html": "If you would like to unsubscribe <% click here %>.", + "text/plain": "If you would like to unsubscribe click here: <% %>.", + "enable": 1 + } + } + } } } diff --git a/tests/test_sendgrid_backend.py b/tests/test_sendgrid_backend.py index 01b9b9d..40ea809 100644 --- a/tests/test_sendgrid_backend.py +++ b/tests/test_sendgrid_backend.py @@ -423,18 +423,35 @@ class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase): def test_esp_extra(self): self.message.tags = ["tag"] + self.message.track_clicks = True self.message.esp_extra = { - 'x-smtpapi': {'asm_group_id': 1}, + 'x-smtpapi': { + # Most SendMail options go in the 'x-smtpapi' block... + 'asm_group_id': 1, + 'filters': { + # If you add a filter, you must supply all required settings for it. + 'subscriptiontrack': { + 'settings': { + 'enable': 1, + 'replace': '[unsubscribe_url]', + }, + }, + }, + }, 'newthing': "some param not supported by Anymail", } self.message.send() # Additional send params: data = self.get_api_call_data() self.assertEqual(data['newthing'], "some param not supported by Anymail") - # Should merge x-smtpapi + # Should merge x-smtpapi, and merge filters within x-smtpapi smtpapi = self.get_smtpapi() self.assertEqual(smtpapi['category'], ["tag"]) self.assertEqual(smtpapi['asm_group_id'], 1) + self.assertEqual(smtpapi['filters']['subscriptiontrack'], + {'settings': {'enable': 1, 'replace': '[unsubscribe_url]'}}) # esp_extra merged + self.assertEqual(smtpapi['filters']['clicktrack'], + {'settings': {'enable': 1}}) # Anymail message option preserved # noinspection PyUnresolvedReferences def test_send_attaches_anymail_status(self): diff --git a/tests/utils.py b/tests/utils.py index 94f8e6e..bd505d5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,10 +1,8 @@ # Anymail test utils import sys -import unittest import os import re -import six import warnings from base64 import b64decode from contextlib import contextmanager @@ -60,19 +58,23 @@ class AnymailTestMixin: finally: warnings.resetwarnings() - # Plus these methods added below: - # assertCountEqual - # assertRaisesRegex - # assertRegex + def assertCountEqual(self, *args, **kwargs): + try: + return super(AnymailTestMixin, self).assertCountEqual(*args, **kwargs) + except TypeError: + return self.assertItemsEqual(*args, **kwargs) # Python 2 -# Add the Python 3 TestCase assertions, if they're not already there. -# (The six implementations cause infinite recursion if installed on -# a py3 TestCase.) -for method in ('assertCountEqual', 'assertRaisesRegex', 'assertRegex'): - try: - getattr(unittest.TestCase, method) - except AttributeError: - setattr(AnymailTestMixin, method, getattr(six, method)) + def assertRaisesRegex(self, *args, **kwargs): + try: + return super(AnymailTestMixin, self).assertRaisesRegex(*args, **kwargs) + except TypeError: + return self.assertRaisesRegexp(*args, **kwargs) # Python 2 + + def assertRegex(self, *args, **kwargs): + try: + return super(AnymailTestMixin, self).assertRegex(*args, **kwargs) + except TypeError: + return self.assertRegexpMatches(*args, **kwargs) # Python 2 # Backported from python 3.5