From 918afce46708e2fcfeb5e40f252d792c0a2cae00 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Tue, 19 Sep 2023 16:15:36 -0300 Subject: [PATCH 1/9] Add settings section to docs --- README.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.rst b/README.rst index d5122aa..488c871 100644 --- a/README.rst +++ b/README.rst @@ -360,6 +360,27 @@ Methods: a celery's task for example or to handle errors. +Settings +============= + +You can configure Django-Templated-Email by setting the following settings + +.. code-block:: python + + TEMPLATED_EMAIL_FROM_EMAIL # String containing the email to send the email from - fallback to DEFAULT_FROM_EMAIL + TEMPLATED_EMAIL_BACKEND # The backend that will send the email + TEMPLATED_EMAIL_TEMPLATE_DIR # The directory containing the templates, use '' if using the top level + TEMPLATED_EMAIL_FILE_EXTENSION # The file extension of the template files + TEMPLATED_EMAIL_AUTO_PLAIN # Disable the behavior of calculating the plain part from the html part of the email when `html2text ` is installed + TEMPLATED_EMAIL_PLAIN_FUNCTION # Specify a custom function that converts from HTML to the plain part + + # vanilla_django + TEMPLATED_EMAIL_DJANGO_SUBJECTS # Deprecated, prefer to use **{% block subject %}** + + # anymail + TEMPLATED_EMAIL_EMAIL_MESSAGE_CLASS # Replaces django.core.mail.EmailMessage + TEMPLATED_EMAIL_EMAIL_MULTIALTERNATIVES_CLASS # Replaces django.core.mail.EmailMultiAlternatives + Future Plans ============= From 742a339caf1cc6bcb57b31bc63c163e31a7f5c87 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Wed, 20 Sep 2023 14:18:20 -0300 Subject: [PATCH 2/9] Update settings section to docs --- README.rst | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 488c871..bb0cc4e 100644 --- a/README.rst +++ b/README.rst @@ -367,19 +367,16 @@ You can configure Django-Templated-Email by setting the following settings .. code-block:: python - TEMPLATED_EMAIL_FROM_EMAIL # String containing the email to send the email from - fallback to DEFAULT_FROM_EMAIL - TEMPLATED_EMAIL_BACKEND # The backend that will send the email - TEMPLATED_EMAIL_TEMPLATE_DIR # The directory containing the templates, use '' if using the top level - TEMPLATED_EMAIL_FILE_EXTENSION # The file extension of the template files - TEMPLATED_EMAIL_AUTO_PLAIN # Disable the behavior of calculating the plain part from the html part of the email when `html2text ` is installed - TEMPLATED_EMAIL_PLAIN_FUNCTION # Specify a custom function that converts from HTML to the plain part - - # vanilla_django - TEMPLATED_EMAIL_DJANGO_SUBJECTS # Deprecated, prefer to use **{% block subject %}** - - # anymail - TEMPLATED_EMAIL_EMAIL_MESSAGE_CLASS # Replaces django.core.mail.EmailMessage - TEMPLATED_EMAIL_EMAIL_MULTIALTERNATIVES_CLASS # Replaces django.core.mail.EmailMultiAlternatives + TEMPLATED_EMAIL_FROM_EMAIL = None # String containing the email to send the email from - fallback to DEFAULT_FROM_EMAIL + TEMPLATED_EMAIL_BACKEND = TemplateBackend # The backend class that will send the email, as a string like 'foo.bar.TemplateBackend' or the class reference itself + TEMPLATED_EMAIL_TEMPLATE_DIR = 'templated_email/' # The directory containing the templates, use '' if using the top level + TEMPLATED_EMAIL_FILE_EXTENSION = 'email' # The file extension of the template files + TEMPLATED_EMAIL_AUTO_PLAIN = True # Set to false to disable the behavior of calculating the plain part from the html part of the email when `html2text ` is installed + TEMPLATED_EMAIL_PLAIN_FUNCTION = None # Specify a custom function that converts from HTML to the plain part + + # Specific for anymail integration: + TEMPLATED_EMAIL_EMAIL_MESSAGE_CLASS = 'django.core.mail.EmailMessage' # Replaces django.core.mail.EmailMessage + TEMPLATED_EMAIL_EMAIL_MULTIALTERNATIVES_CLASS = 'django.core.mail.EmailMultiAlternatives' # Replaces django.core.mail.EmailMultiAlternatives Future Plans ============= From b687cb071162bb7d2b9d1eb71be2991b4edbf0c6 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Wed, 20 Sep 2023 17:51:48 -0300 Subject: [PATCH 3/9] Update tested python versions --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b0e0ae..5734b21 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 From eb732aec3713b9ddbe97c76bc40f433c0f3b294f Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Thu, 21 Sep 2023 11:15:24 -0300 Subject: [PATCH 4/9] Use `storages['DEFAULT']` instead of the depreacted `get_storage_class` --- tests/backends/test_vanilla_django_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/backends/test_vanilla_django_backend.py b/tests/backends/test_vanilla_django_backend.py index db4bc7e..4e2bfec 100644 --- a/tests/backends/test_vanilla_django_backend.py +++ b/tests/backends/test_vanilla_django_backend.py @@ -5,7 +5,7 @@ from django.test import TestCase, override_settings from django.core.mail import EmailMessage, EmailMultiAlternatives -from django.core.files.storage import get_storage_class +from django.core.files.storage import storages from django.template import TemplateDoesNotExist from django.core import mail @@ -69,7 +69,7 @@ class TemplateBackendTestCase(MockedNetworkTestCaseMixin, template_backend_klass = TemplateBackend def setUp(self): - self.storage_mock = Mock(wraps=get_storage_class())() + self.storage_mock = Mock(wraps=storages["default"])() self.storage_mock.save = Mock() self.backend = self.template_backend_klass() self.context = {'username': 'vintasoftware', From 511773ac029557f5762fbb29b57659f0402b02d2 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Mon, 25 Sep 2023 16:02:31 -0300 Subject: [PATCH 5/9] Update django/python versions on tox.ini --- tox.ini | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tox.ini b/tox.ini index 6f86fae..6487626 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,30 @@ [tox] -envlist = - py36-{flake8}, - {py36,py37,py38,py39}-django32, - {py38,py39,py310,py311}-django{40,41,main}, +envlist = {linux}-py{38,39,310,311}-django{32,40,41,42} [gh-actions] python = - 3.6: py36 - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 + +[gh-actions:env] +OS = + ubuntu-latest: linux +DJANGO = + 3.2: django32 + 4.0: django40 + 4.1: django41 + 4.2: django42 + [testenv] commands= py.test --cov=templated_email tests/ deps= -rtox-requirements.txt pytest-cov - django32: Django==3.2 - django40: Django>=4.0,<4.1 - django41: Django>=4.1,<4.2 - djangomain: https://github.com/django/django/archive/main.tar.gz - -[testenv:py36-flake8] -commands = flake8 templated_email tests --ignore=E501 -deps = - flake8 + django32: django~=3.2 + django40: django~=4.0 + django41: django~=4.1 + django42: django~=4.2 From 41576c1b3e0609df97241fa8676effe2cf6ce6d9 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Mon, 25 Sep 2023 16:13:47 -0300 Subject: [PATCH 6/9] Use default_storage directly --- tests/backends/test_vanilla_django_backend.py | 6 +++--- tests/test_get_templated_mail.py | 2 +- tests/test_send_templated_mail.py | 2 +- tox-requirements.txt | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/backends/test_vanilla_django_backend.py b/tests/backends/test_vanilla_django_backend.py index 4e2bfec..8c09411 100644 --- a/tests/backends/test_vanilla_django_backend.py +++ b/tests/backends/test_vanilla_django_backend.py @@ -5,12 +5,12 @@ from django.test import TestCase, override_settings from django.core.mail import EmailMessage, EmailMultiAlternatives -from django.core.files.storage import storages +from django.core.files.storage import default_storage from django.template import TemplateDoesNotExist from django.core import mail import pytest -from mock import patch, Mock +from unittest.mock import patch, Mock from anymail.message import AnymailMessage from templated_email.backends.vanilla_django import TemplateBackend, EmailRenderException @@ -69,7 +69,7 @@ class TemplateBackendTestCase(MockedNetworkTestCaseMixin, template_backend_klass = TemplateBackend def setUp(self): - self.storage_mock = Mock(wraps=storages["default"])() + self.storage_mock = Mock(wraps=default_storage)() self.storage_mock.save = Mock() self.backend = self.template_backend_klass() self.context = {'username': 'vintasoftware', diff --git a/tests/test_get_templated_mail.py b/tests/test_get_templated_mail.py index 0a2f1a7..96ac3d7 100644 --- a/tests/test_get_templated_mail.py +++ b/tests/test_get_templated_mail.py @@ -1,6 +1,6 @@ from django.test import TestCase -from mock import patch +from unittest.mock import patch from templated_email import get_templated_mail diff --git a/tests/test_send_templated_mail.py b/tests/test_send_templated_mail.py index 0c77b7e..c37ae3f 100644 --- a/tests/test_send_templated_mail.py +++ b/tests/test_send_templated_mail.py @@ -1,6 +1,6 @@ from django.test import TestCase -from mock import patch, Mock +from unittest.mock import patch, Mock from templated_email import send_templated_mail diff --git a/tox-requirements.txt b/tox-requirements.txt index 871945f..5f3db7b 100644 --- a/tox-requirements.txt +++ b/tox-requirements.txt @@ -2,7 +2,6 @@ django-anymail django-pytest django-render-block html2text -mock pytest pytest-django pytest-pythonpath From 650cbe7fd83b6c5e89d71824540278ededbd6a9b Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Mon, 25 Sep 2023 16:17:55 -0300 Subject: [PATCH 7/9] Use unittest.mock instead of external mock --- tests/generic_views/test_views.py | 2 +- tests/test_utils.py | 2 +- tests/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/generic_views/test_views.py b/tests/generic_views/test_views.py index 168b8c9..71248e9 100644 --- a/tests/generic_views/test_views.py +++ b/tests/generic_views/test_views.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core import mail -import mock +import unittest.mock from templated_email.generic_views import TemplatedEmailFormViewMixin from tests.generic_views.views import AuthorCreateView diff --git a/tests/test_utils.py b/tests/test_utils.py index 10d80ae..f21dfeb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,4 @@ -import mock +import unittest.mock from django.test import TestCase diff --git a/tests/utils.py b/tests/utils.py index 75d7693..63e4b7c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,4 @@ -import mock +import unittest.mock class MockedNetworkTestCaseMixin(object): From 3106c41fd5a32eda8ea962f6e7e6338505e2190b Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Mon, 25 Sep 2023 18:43:07 -0300 Subject: [PATCH 8/9] Fix storage mocks --- tests/backends/test_vanilla_django_backend.py | 66 ++++++++++--------- tests/generic_views/test_views.py | 10 +-- tests/test_utils.py | 8 +-- tests/utils.py | 2 +- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/tests/backends/test_vanilla_django_backend.py b/tests/backends/test_vanilla_django_backend.py index 8c09411..bc4e711 100644 --- a/tests/backends/test_vanilla_django_backend.py +++ b/tests/backends/test_vanilla_django_backend.py @@ -5,7 +5,14 @@ from django.test import TestCase, override_settings from django.core.mail import EmailMessage, EmailMultiAlternatives -from django.core.files.storage import default_storage +try: + from django.core.files.storage import get_storage_class +except ImportError: + from django.core.files.storage import storages + + def get_storage_class(s): + storages[s] + from django.template import TemplateDoesNotExist from django.core import mail @@ -69,17 +76,11 @@ class TemplateBackendTestCase(MockedNetworkTestCaseMixin, template_backend_klass = TemplateBackend def setUp(self): - self.storage_mock = Mock(wraps=default_storage)() - self.storage_mock.save = Mock() self.backend = self.template_backend_klass() self.context = {'username': 'vintasoftware', 'joindate': date(2016, 8, 22), 'full_name': 'Foo Bar'} - def patch_storage(self): - return patch('django.core.files.storage.default_storage._wrapped', - self.storage_mock) - def test_inexistent_base_email(self): try: self.backend._render_email('inexistent_base.email', {}) @@ -168,22 +169,23 @@ def test_get_email_message_with_create_link(self, mocked): uuid=uuid) self.assertEqual(saved_email.content, HTML_RESULT) + @patch('django.core.files.storage.FileSystemStorage.save') + @patch('django.core.files.storage.FileSystemStorage.url') @patch.object( template_backend_klass, '_render_email', return_value={'html': HTML_RESULT, 'plain': PLAIN_RESULT, 'subject': SUBJECT_RESULT} ) - def test_get_email_message_with_inline_image(self, mocked): - self.storage_mock.save = Mock(return_value='saved_url') - with self.patch_storage(): - self.backend.get_email_message( - 'foo.email', {'an_image': InlineImage('file.png', b'foo', - subtype='png')}, - from_email='from@example.com', cc=['cc@example.com'], - bcc=['bcc@example.com'], to=['to@example.com'], - create_link=True) - second_call_context = mocked.call_args_list[1][0][1] - self.assertEqual(second_call_context['an_image'], '/media/saved_url') + def test_get_email_message_with_inline_image(self, mock_render_email, mock_url, mock_save): + mock_url.return_value = 'media/saved_url' + self.backend.get_email_message( + 'foo.email', {'an_image': InlineImage('file.png', b'foo', + subtype='png')}, + from_email='from@example.com', cc=['cc@example.com'], + bcc=['bcc@example.com'], to=['to@example.com'], + create_link=True) + second_call_context = mock_render_email.call_args_list[1][0][1] + self.assertEqual(second_call_context['an_image'], 'media/saved_url') @override_settings(TEMPLATED_EMAIL_EMAIL_MESSAGE_CLASS='anymail.message.AnymailMessage') @patch.object( @@ -496,30 +498,32 @@ def test_removal_of_legacy(self): except TemplateDoesNotExist as e: self.assertEqual(e.args[0], 'templated_email/legacy.email') - def test_host_inline_image_if_not_exist(self): + @patch('django.core.files.storage.FileSystemStorage.url') + @patch('django.core.files.storage.FileSystemStorage.save') + def test_host_inline_image_if_not_exist(self, mock_save, mock_url): + mock_url.return_value = 'media/saved_url' inline_image = InlineImage('foo.jpg', b'bar') - self.storage_mock.save = Mock(return_value='saved_url') - with self.patch_storage(): - filename = self.backend.host_inline_image(inline_image) - self.assertEqual(filename, '/media/saved_url') - self.storage_mock.save.assert_called_once() - name, content = self.storage_mock.save.call_args[0] + filename = self.backend.host_inline_image(inline_image) + self.assertEqual(filename, 'media/saved_url') + mock_save.assert_called_once() + name, content = mock_save.call_args[0] self.assertEqual( name, 'templated_email/37b51d194a7513e45b56f6524f2d51f2foo.jpg') self.assertTrue(isinstance(content, BytesIO)) - def test_host_inline_image_if_exist(self): + @patch('django.core.files.storage.FileSystemStorage.exists') + @patch('django.core.files.storage.FileSystemStorage.save') + def test_host_inline_image_if_exist(self, mock_save, mock_exists): inline_image = InlineImage('foo.jpg', b'bar') - self.storage_mock.exists = Mock(return_value=True) + mock_exists.return_value = True - with self.patch_storage(): - filename = self.backend.host_inline_image(inline_image) + filename = self.backend.host_inline_image(inline_image) self.assertEqual( filename, '/media/templated_email/37b51d194a7513e45b56f6524f2d51f2foo.jpg') - self.storage_mock.save.assert_not_called() - self.storage_mock.exists.assert_called_once_with( + mock_save.assert_not_called() + mock_exists.assert_called_once_with( 'templated_email/37b51d194a7513e45b56f6524f2d51f2foo.jpg') diff --git a/tests/generic_views/test_views.py b/tests/generic_views/test_views.py index 71248e9..ffb6150 100644 --- a/tests/generic_views/test_views.py +++ b/tests/generic_views/test_views.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core import mail -import unittest.mock +from unittest.mock import patch from templated_email.generic_views import TemplatedEmailFormViewMixin from tests.generic_views.views import AuthorCreateView @@ -44,10 +44,10 @@ def test_templated_email_get_recipients(self): self.mixin_object.templated_email_get_recipients, form=None) - @mock.patch.object(TemplatedEmailFormViewMixin, + @patch.object(TemplatedEmailFormViewMixin, 'templated_email_get_template_names', return_value=['template']) - @mock.patch.object(TemplatedEmailFormViewMixin, + @patch.object(TemplatedEmailFormViewMixin, 'templated_email_get_recipients', return_value=['foo@example.com']) def test_templated_email_get_send_email_kwargs_valid( @@ -65,10 +65,10 @@ class FakeForm(object): self.assertEqual(kwargs['recipient_list'], ['foo@example.com']) self.assertEqual(kwargs['context'], {'form_data': 'foo'}) - @mock.patch.object(TemplatedEmailFormViewMixin, + @patch.object(TemplatedEmailFormViewMixin, 'templated_email_get_template_names', return_value=['template']) - @mock.patch.object(TemplatedEmailFormViewMixin, + @patch.object(TemplatedEmailFormViewMixin, 'templated_email_get_recipients', return_value=['foo@example.com']) def test_templated_email_get_send_email_kwargs_not_valid( diff --git a/tests/test_utils.py b/tests/test_utils.py index f21dfeb..e74175d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,4 @@ -import unittest.mock +from unittest.mock import patch, Mock from django.test import TestCase @@ -23,11 +23,11 @@ def test_generate_cid(self): str(self.inline_image) self.assertIsNotNone(self.inline_image._content_id) - @mock.patch('templated_email.utils.make_msgid', return_value='foo') + @patch('templated_email.utils.make_msgid', return_value='foo') def test_str(self, mocked): self.assertEqual(str(self.inline_image), 'cid:foo') - @mock.patch('templated_email.utils.make_msgid', return_value='foo') + @patch('templated_email.utils.make_msgid', return_value='foo') def test_should_cache_cid(self, mocked): str(self.inline_image) str(self.inline_image) @@ -45,7 +45,7 @@ def test_changing_content_should_generate_new_cid(self): self.assertNotEqual(cid2, cid3) def test_attach_to_message(self): - message = mock.Mock() + message = Mock() self.inline_image.attach_to_message(message) mimeimage = message.attach.call_args[0][0] self.assertEqual(mimeimage.get('Content-ID'), diff --git a/tests/utils.py b/tests/utils.py index 63e4b7c..0f742fd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,7 @@ class MockedNetworkTestCaseMixin(object): # See: https://code.djangoproject.com/ticket/24380 @classmethod def setUpClass(cls): - cls.getfqdn_patcher = mock.patch( + cls.getfqdn_patcher = unittest.mock.patch( 'django.core.mail.utils.socket.getfqdn', return_value='vinta.local') cls.getfqdn_patcher.start() From 119e33d6285f2866457c4fb2b1f3aa7e05531219 Mon Sep 17 00:00:00 2001 From: Vitor Rangel Date: Mon, 25 Sep 2023 18:43:21 -0300 Subject: [PATCH 9/9] Only test on push --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5734b21..b05d908 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,6 @@ name: Run tests on: - push - - pull_request jobs: build: