├── .gitignore
├── .travis.yml
├── CHANGELOG.rst
├── LICENSE
├── MANIFEST.in
├── Makefile
├── Pipfile
├── Pipfile.lock
├── README.rst
├── docs
├── Makefile
├── make.bat
└── source
│ ├── conf.py
│ ├── getting_started.rst
│ ├── index.rst
│ ├── sample_usage.rst
│ ├── settings.rst
│ └── templates_syntax.rst
├── examples
├── __init__.py
├── config
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── simple
│ ├── __init__.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── urls.py
│ └── views.py
└── templates
│ ├── html_mail.html
│ ├── text_and_html_mail.html
│ └── text_mail.html
├── requirements
├── base
└── test
├── setup.py
├── templated_mail
├── __init__.py
└── mail.py
├── tests
├── __init__.py
├── helpers.py
├── settings.py
├── templates
│ ├── extends.html
│ ├── html_mail.html
│ ├── nested_extends.html
│ ├── text_and_html_mail.html
│ └── text_mail.html
└── test_mail.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | # database files
2 | *.sqlite3
3 |
4 | # pycharm files
5 | .idea/
6 |
7 | # Distribution
8 | build/
9 | dist/
10 |
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # Translations
17 | *.mo
18 | *.pot
19 |
20 | # Test files
21 | .cache/
22 | .tox/
23 | .coverage
24 | htmlcov/
25 |
26 | # dotenv
27 | .env
28 |
29 | # virtualenv
30 | env/
31 | .venv/
32 | venv/
33 | ENV/
34 |
35 | # osx
36 | *.DS_Store
37 |
38 | # logs
39 | *.log
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: python
4 | python:
5 | - "2.7"
6 | - "3.4"
7 | - "3.5"
8 | - "3.6"
9 | - "3.7-dev"
10 |
11 | env:
12 | - DJANGO=1.11
13 | - DJANGO=2.0
14 | - DJANGO=2.1
15 | - DJANGO=master
16 |
17 | matrix:
18 | fast_finish: true
19 | exclude:
20 | - python: "2.7"
21 | env: DJANGO=2.0
22 | - python: "2.7"
23 | env: DJANGO=2.1
24 | - python: "2.7"
25 | env: DJANGO=master
26 | - python: "3.4"
27 | env: DJANGO=2.1
28 | - python: "3.4"
29 | env: DJANGO=master
30 | allow_failures:
31 | - env: DJANGO=master
32 | - python: "3.7-dev"
33 |
34 | install:
35 | - travis_retry pip install -U tox-travis
36 |
37 | script:
38 | - tox
39 |
40 | after_success:
41 | - travis_retry pip install -U codecov
42 | - codecov
43 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Change Log
3 | ==========
4 |
5 | This document records all notable changes to django-templated-mail.
6 | This project adheres to `Semantic Versioning `_.
7 |
8 | ---------------------
9 | `1.1.1`_ (2017-02-01)
10 | ---------------------
11 |
12 | * Bugfix: ``from_email`` does not fallback to ``DEFAULT_FROM_EMAIL``
13 |
14 |
15 | ---------------------
16 | `1.1.0`_ (2017-01-29)
17 | ---------------------
18 |
19 | * Add support for ``reply_to`` parameter in ``BaseEmailMessage.send`` method
20 | * Add support for ``from_email`` parameter in ``BaseEmailMessage.send`` method
21 | * Add support for Django 2.0
22 | * Remove support for Django 1.10
23 | * Fix passing context to email classes with context provided via mixin
24 | * Fix invalid release years in release notes
25 |
26 | ---------------------
27 | `1.0.0`_ (2017-10-06)
28 | ---------------------
29 |
30 | * Breaking API: Update ``set_context_data`` to ``get_context_data``
31 | * Add basic documentation
32 | * Add basic examples
33 | * Update templates rendering to happen on send
34 | * Update dependencies
35 | * Remove Python 3.3 from supported versions
36 |
37 | ---------------------
38 | `0.2.0`_ (2017-09-22)
39 | ---------------------
40 |
41 | * Add support for CC and BCC
42 | * Update name of ``BaseEmailMessage.send_to`` to ``BaseEmailMessage.send``
43 |
44 | ---------------------
45 | `0.1.1`_ (2017-09-15)
46 | ---------------------
47 |
48 | * Bugfix: Issue with template nodes requiring template to be bound to context
49 | * Bugfix: Issue with whitespaces around content blocks
50 |
51 | ---------------------
52 | `0.1.0`_ (2017-09-15)
53 | ---------------------
54 |
55 | * Initial release of the project. Its goal is to provide simple API for sending
56 | emails using Django template system. For more information and to get started see
57 | `README `_.
58 |
59 |
60 | .. _0.1.0: https://github.com/sunscrapers/django-templated-mail/compare/3bc71b3...0.1.0
61 | .. _0.1.1: https://github.com/sunscrapers/django-templated-mail/compare/0.1.0...0.1.1
62 | .. _0.2.0: https://github.com/sunscrapers/django-templated-mail/compare/0.1.1...0.2.0
63 | .. _1.0.0: https://github.com/sunscrapers/django-templated-mail/compare/0.2.0...1.0.0
64 | .. _1.1.0: https://github.com/sunscrapers/django-templated-mail/compare/1.0.0...1.1.0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 SUNSCRAPERS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | init:
2 | pipenv install --dev
3 | pipenv run pip install -e .
4 |
5 | test:
6 | pipenv run py.test --ds=tests.settings --capture=no --cov-report term-missing --cov-report html --cov=templated_mail tests
7 | pipenv run flake8 .
8 |
9 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.python.org/simple"
3 | verify_ssl = true
4 |
5 | [packages]
6 | django = "*"
7 |
8 |
9 |
10 | [dev-packages]
11 |
12 | ipython = "*"
13 | ipdb = "*"
14 | pytest = "*"
15 | "pytest-cov" = "*"
16 | "pytest-django" = "*"
17 | "flake8" = "*"
18 | "pytest-pythonpath" = "*"
19 | twine = "*"
20 | wheel = "*"
21 | sphinx = "*"
22 | "sphinx-rtd-theme" = "*"
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "8c45cd4fef47bf79225773acb1db2d4aadc400a1db629f7884a7514499de4cf6"
5 | },
6 | "host-environment-markers": {
7 | "implementation_name": "cpython",
8 | "implementation_version": "3.6.2",
9 | "os_name": "posix",
10 | "platform_machine": "x86_64",
11 | "platform_python_implementation": "CPython",
12 | "platform_release": "4.13.3-1-ARCH",
13 | "platform_system": "Linux",
14 | "platform_version": "#1 SMP PREEMPT Thu Sep 21 20:33:16 CEST 2017",
15 | "python_full_version": "3.6.2",
16 | "python_version": "3.6",
17 | "sys_platform": "linux"
18 | },
19 | "pipfile-spec": 6,
20 | "requires": {},
21 | "sources": [
22 | {
23 | "url": "https://pypi.python.org/simple",
24 | "verify_ssl": true
25 | }
26 | ]
27 | },
28 | "default": {
29 | "django": {
30 | "hashes": [
31 | "sha256:7ab6a9c798a5f9f359ee6da3677211f883fb02ef32cebe9b29751eb7a871febf",
32 | "sha256:c3b42ca1efa1c0a129a9e863134cc3fe705c651dea3a04a7998019e522af0c60"
33 | ],
34 | "version": "==1.11.6"
35 | },
36 | "pytz": {
37 | "hashes": [
38 | "sha256:c883c2d6670042c7bc1688645cac73dd2b03193d1f7a6847b6154e96890be06d",
39 | "sha256:03c9962afe00e503e2d96abab4e8998a0f84d4230fa57afe1e0528473698cdd9",
40 | "sha256:487e7d50710661116325747a9cd1744d3323f8e49748e287bc9e659060ec6bf9",
41 | "sha256:43f52d4c6a0be301d53ebd867de05e2926c35728b3260157d274635a0a947f1c",
42 | "sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67",
43 | "sha256:54a935085f7bf101f86b2aff75bd9672b435f51c3339db2ff616e66845f2b8f9",
44 | "sha256:39504670abb5dae77f56f8eb63823937ce727d7cdd0088e6909e6dcac0f89043",
45 | "sha256:ddc93b6d41cfb81266a27d23a79e13805d4a5521032b512643af8729041a81b4",
46 | "sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589"
47 | ],
48 | "version": "==2017.2"
49 | }
50 | },
51 | "develop": {
52 | "alabaster": {
53 | "hashes": [
54 | "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
55 | "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
56 | ],
57 | "version": "==0.7.10"
58 | },
59 | "babel": {
60 | "hashes": [
61 | "sha256:f20b2acd44f587988ff185d8949c3e208b4b3d5d20fcab7d91fe481ffa435528",
62 | "sha256:6007daf714d0cd5524bbe436e2d42b3c20e68da66289559341e48d2cd6d25811"
63 | ],
64 | "version": "==2.5.1"
65 | },
66 | "certifi": {
67 | "hashes": [
68 | "sha256:54a07c09c586b0e4c619f02a5e94e36619da8e2b053e20f594348c0611803704",
69 | "sha256:40523d2efb60523e113b44602298f0960e900388cf3bb6043f645cf57ea9e3f5"
70 | ],
71 | "version": "==2017.7.27.1"
72 | },
73 | "chardet": {
74 | "hashes": [
75 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
76 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
77 | ],
78 | "version": "==3.0.4"
79 | },
80 | "coverage": {
81 | "hashes": [
82 | "sha256:c1456f66c536010cf9e4633a8853a9153e8fd588393695295afd4d0fc16c1d74",
83 | "sha256:97a7ec51cdde3a386e390b159b20f247ccb478084d925c75f1faa3d26c01335e",
84 | "sha256:83e955b975666b5a07d217135e7797857ce844eb340a99e46cc25525120417c4",
85 | "sha256:483ed14080c5301048128bb027b77978c632dd9e92e3ecb09b7e28f5b92abfcf",
86 | "sha256:ef574ab9640bcfa2f3c671831faf03f65788945fdf8efa4d4a1fffc034838e2a",
87 | "sha256:c5a205b4da3c624f5119dc4d84240789b5906bb8468902ec22dcc4aad8aa4638",
88 | "sha256:5dea90ed140e7fa9bc00463313f9bc4a6e6aff297b4969615e7a688615c4c4d2",
89 | "sha256:f9e83b39d29c2815a38e4118d776b482d4082b5bf9c9147fbc99a3f83abe480a",
90 | "sha256:700040c354f0230287906b1276635552a3def4b646e0145555bc9e2e5da9e365",
91 | "sha256:7f1eacae700c66c3d7362a433b228599c9d94a5a3a52613dddd9474e04deb6bc",
92 | "sha256:13ef9f799c8fb45c446a239df68034de3a6f3de274881b088bebd7f5661f79f8",
93 | "sha256:dfb011587e2b7299112f08a2a60d2601706aac9abde37aa1177ea825adaed923",
94 | "sha256:381be5d31d3f0d912334cf2c159bc7bea6bfe6b0e3df6061a3bf2bf88359b1f6",
95 | "sha256:83a477ac4f55a6ef59552683a0544d47b68a85ce6a80fd0ca6b3dc767f6495fb",
96 | "sha256:dfd35f1979da31bcabbe27bcf78d4284d69870731874af629082590023a77336",
97 | "sha256:9681efc2d310cfc53863cc6f63e88ebe7a48124550fa822147996cb09390b6ab",
98 | "sha256:53770b20ac5b4a12e99229d4bae57af0945be87cc257fce6c6c7571a39f0c5d4",
99 | "sha256:8801880d32f11b6df11c32a961e186774b4634ae39d7c43235f5a24368a85f07",
100 | "sha256:16db2c69a1acbcb3c13211e9f954e22b22a729909d81f983b6b9badacc466eda",
101 | "sha256:ef43a06a960b46c73c018704051e023ee6082030f145841ffafc8728039d5a88",
102 | "sha256:c3e2736664a6074fc9bd54fb643f5af0fc60bfedb2963b3d3f98c7450335e34c",
103 | "sha256:17709e22e4c9f5412ba90f446fb13b245cc20bf4a60377021bbff6c0f1f63e7c",
104 | "sha256:a2f7106d1167825c4115794c2ba57cc3b15feb6183db5328fa66f94c12902d8b",
105 | "sha256:2a08e978f402696c6956eee9d1b7e95d3ad042959b71bafe1f3e4557cbd6e0ac",
106 | "sha256:57f510bb16efaec0b6f371b64a8000c62e7e3b3e48e8b0a5745ade078d849814",
107 | "sha256:0f1883eab9c19aa243f51308751b8a2a547b9b817b721cc0ecf3efb99fafbea7",
108 | "sha256:e00fe141e22ce6e9395aa24d862039eb180c6b7e89df0bbaf9765e9aebe560a9",
109 | "sha256:ec596e4401553caa6dd2e3349ce47f9ef82c1f1bcba5d8ac3342724f0df8d6ff",
110 | "sha256:c820a533a943ebc860acc0ce6a00dd36e0fdf2c6f619ff8225755169428c5fa2",
111 | "sha256:b7f7283eb7badd2b8a9c6a9d6eeca200a0a24db6be79baee2c11398f978edcaa",
112 | "sha256:a5ed27ad3e8420b2d6b625dcbd3e59488c14ccc06030167bcf14ffb0f4189b77",
113 | "sha256:d7b70b7b4eb14d0753d33253fe4f121ca99102612e2719f0993607deb30c6f33",
114 | "sha256:4047dc83773869701bde934fb3c4792648eda7c0e008a77a0aec64157d246801",
115 | "sha256:7a9c44400ee0f3b4546066e0710e1250fd75831adc02ab99dda176ad8726f424",
116 | "sha256:0f649e68db74b1b5b8ca4161d08eb2b8fa8ae11af1ebfb80e80e112eb0ef5300",
117 | "sha256:52964fae0fafef8bd283ad8e9a9665205a9fdf912535434defc0ec3def1da26b",
118 | "sha256:36aa6c8db83bc27346ddcd8c2a60846a7178ecd702672689d3ea1828eb1a4d11",
119 | "sha256:9824e15b387d331c0fc0fef905a539ab69784368a1d6ac3db864b4182e520948",
120 | "sha256:4a678e1b9619a29c51301af61ab84122e2f8cc7a0a6b40854b808ac6be604300",
121 | "sha256:8bb7c8dca54109b61013bc4114d96effbf10dea136722c586bce3a5d9fc4e730",
122 | "sha256:1a41d621aa9b6ab6457b557a754d50aaff0813fad3453434de075496fca8a183",
123 | "sha256:0fa423599fc3d9e18177f913552cdb34a8d9ad33efcf52a98c9d4b644edb42c5",
124 | "sha256:e61a4ba0b2686040cb4828297c7e37bcaf3a1a1c0bc0dbe46cc789dde51a80fa",
125 | "sha256:ce9ef0fc99d11d418662e36fd8de6d71b19ec87c2eab961a117cc9d087576e72"
126 | ],
127 | "version": "==4.4.1"
128 | },
129 | "decorator": {
130 | "hashes": [
131 | "sha256:95a26b17806e284452bfd97fa20aa1e8cb4ee23542bda4dbac5e4562aa1642cd",
132 | "sha256:7cb64d38cb8002971710c8899fbdfb859a23a364b7c99dab19d1f719c2ba16b5"
133 | ],
134 | "version": "==4.1.2"
135 | },
136 | "docutils": {
137 | "hashes": [
138 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6",
139 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
140 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274"
141 | ],
142 | "version": "==0.14"
143 | },
144 | "flake8": {
145 | "hashes": [
146 | "sha256:f1a9d8886a9cbefb52485f4f4c770832c7fb569c084a9a314fb1eaa37c0c2c86",
147 | "sha256:c20044779ff848f67f89c56a0e4624c04298cd476e25253ac0c36f910a1a11d8"
148 | ],
149 | "version": "==3.4.1"
150 | },
151 | "idna": {
152 | "hashes": [
153 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
154 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
155 | ],
156 | "version": "==2.6"
157 | },
158 | "imagesize": {
159 | "hashes": [
160 | "sha256:6ebdc9e0ad188f9d1b2cdd9bc59cbe42bf931875e829e7a595e6b3abdc05cdfb",
161 | "sha256:0ab2c62b87987e3252f89d30b7cedbec12a01af9274af9ffa48108f2c13c6062"
162 | ],
163 | "version": "==0.7.1"
164 | },
165 | "ipdb": {
166 | "hashes": [
167 | "sha256:9ea256b4280fbe12840fb9dfc3ce498c6c6de03352eca293e4400b0dfbed2b28"
168 | ],
169 | "version": "==0.10.3"
170 | },
171 | "ipython": {
172 | "hashes": [
173 | "sha256:fcc6d46f08c3c4de7b15ae1c426e15be1b7932bcda9d83ce1a4304e8c1129df3",
174 | "sha256:51c158a6c8b899898d1c91c6b51a34110196815cc905f9be0fa5878e19355608"
175 | ],
176 | "version": "==6.2.1"
177 | },
178 | "ipython-genutils": {
179 | "hashes": [
180 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
181 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
182 | ],
183 | "version": "==0.2.0"
184 | },
185 | "jedi": {
186 | "hashes": [
187 | "sha256:3af518490ffcd00a3074c135b42511e081575e9abd115c216a34491411ceebb0",
188 | "sha256:f6d5973573e76b1fd2ea75f6dcd6445d02d41ff3af5fc61b275b4e323d1dd396"
189 | ],
190 | "version": "==0.11.0"
191 | },
192 | "jinja2": {
193 | "hashes": [
194 | "sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054",
195 | "sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff"
196 | ],
197 | "version": "==2.9.6"
198 | },
199 | "markupsafe": {
200 | "hashes": [
201 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
202 | ],
203 | "version": "==1.0"
204 | },
205 | "mccabe": {
206 | "hashes": [
207 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
208 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
209 | ],
210 | "version": "==0.6.1"
211 | },
212 | "parso": {
213 | "hashes": [
214 | "sha256:b573acb69f66a970197b5fdbbdfad3b8a417a520e383133b2b4e708f104bfc9a",
215 | "sha256:c5279916bb417aa2bf634648ff895cf35dce371d7319744884827bfad06f8d7b"
216 | ],
217 | "version": "==0.1.0"
218 | },
219 | "pexpect": {
220 | "hashes": [
221 | "sha256:f853b52afaf3b064d29854771e2db509ef80392509bde2dd7a6ecf2dfc3f0018",
222 | "sha256:3d132465a75b57aa818341c6521392a06cc660feb3988d7f1074f39bd23c9a92"
223 | ],
224 | "markers": "sys_platform != 'win32'",
225 | "version": "==4.2.1"
226 | },
227 | "pickleshare": {
228 | "hashes": [
229 | "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5",
230 | "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b"
231 | ],
232 | "version": "==0.7.4"
233 | },
234 | "pkginfo": {
235 | "hashes": [
236 | "sha256:31a49103180ae1518b65d3f4ce09c784e2bc54e338197668b4fb7dc539521024",
237 | "sha256:bb1a6aeabfc898f5df124e7e00303a5b3ec9a489535f346bfbddb081af93f89e"
238 | ],
239 | "version": "==1.4.1"
240 | },
241 | "prompt-toolkit": {
242 | "hashes": [
243 | "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4",
244 | "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381",
245 | "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917"
246 | ],
247 | "version": "==1.0.15"
248 | },
249 | "ptyprocess": {
250 | "hashes": [
251 | "sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a",
252 | "sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365"
253 | ],
254 | "version": "==0.5.2"
255 | },
256 | "py": {
257 | "hashes": [
258 | "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
259 | "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"
260 | ],
261 | "version": "==1.4.34"
262 | },
263 | "pycodestyle": {
264 | "hashes": [
265 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9",
266 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766"
267 | ],
268 | "version": "==2.3.1"
269 | },
270 | "pyflakes": {
271 | "hashes": [
272 | "sha256:cc5eadfb38041f8366128786b4ca12700ed05bbf1403d808e89d57d67a3875a7",
273 | "sha256:aa0d4dff45c0cc2214ba158d29280f8fa1129f3e87858ef825930845146337f4"
274 | ],
275 | "version": "==1.5.0"
276 | },
277 | "pygments": {
278 | "hashes": [
279 | "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
280 | "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
281 | ],
282 | "version": "==2.2.0"
283 | },
284 | "pytest": {
285 | "hashes": [
286 | "sha256:81a25f36a97da3313e1125fce9e7bbbba565bc7fec3c5beb14c262ddab238ac1",
287 | "sha256:27fa6617efc2869d3e969a3e75ec060375bfb28831ade8b5cdd68da3a741dc3c"
288 | ],
289 | "version": "==3.2.3"
290 | },
291 | "pytest-cov": {
292 | "hashes": [
293 | "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec",
294 | "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d"
295 | ],
296 | "version": "==2.5.1"
297 | },
298 | "pytest-django": {
299 | "hashes": [
300 | "sha256:00995c2999b884a38ae9cd30a8c00ed32b3d38c1041250ea84caf18085589662",
301 | "sha256:038ccc5a9daa1b1b0eb739ab7dce54e495811eca5ea3af4815a2a3ac45152309"
302 | ],
303 | "version": "==3.1.2"
304 | },
305 | "pytest-pythonpath": {
306 | "hashes": [
307 | "sha256:2d506b8d7dbc2535a16c888211b7319ad32b3e73444bd9dbb1dd19427a6c7414"
308 | ],
309 | "version": "==0.7.1"
310 | },
311 | "pytz": {
312 | "hashes": [
313 | "sha256:c883c2d6670042c7bc1688645cac73dd2b03193d1f7a6847b6154e96890be06d",
314 | "sha256:03c9962afe00e503e2d96abab4e8998a0f84d4230fa57afe1e0528473698cdd9",
315 | "sha256:487e7d50710661116325747a9cd1744d3323f8e49748e287bc9e659060ec6bf9",
316 | "sha256:43f52d4c6a0be301d53ebd867de05e2926c35728b3260157d274635a0a947f1c",
317 | "sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67",
318 | "sha256:54a935085f7bf101f86b2aff75bd9672b435f51c3339db2ff616e66845f2b8f9",
319 | "sha256:39504670abb5dae77f56f8eb63823937ce727d7cdd0088e6909e6dcac0f89043",
320 | "sha256:ddc93b6d41cfb81266a27d23a79e13805d4a5521032b512643af8729041a81b4",
321 | "sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589"
322 | ],
323 | "version": "==2017.2"
324 | },
325 | "requests": {
326 | "hashes": [
327 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
328 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
329 | ],
330 | "version": "==2.18.4"
331 | },
332 | "requests-toolbelt": {
333 | "hashes": [
334 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
335 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
336 | ],
337 | "version": "==0.8.0"
338 | },
339 | "simplegeneric": {
340 | "hashes": [
341 | "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"
342 | ],
343 | "version": "==0.8.1"
344 | },
345 | "six": {
346 | "hashes": [
347 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
348 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
349 | ],
350 | "version": "==1.11.0"
351 | },
352 | "snowballstemmer": {
353 | "hashes": [
354 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89",
355 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128"
356 | ],
357 | "version": "==1.2.1"
358 | },
359 | "sphinx": {
360 | "hashes": [
361 | "sha256:3e70eb94f7e81b47e0545ebc26b758193b6c8b222e152ded99b9c972e971c731",
362 | "sha256:f101efd87fbffed8d8aca6ef307fec57693334f39d32efcbc2fc96ed129f4a3e"
363 | ],
364 | "version": "==1.6.4"
365 | },
366 | "sphinx-rtd-theme": {
367 | "hashes": [
368 | "sha256:62ee4752716e698bad7de8a18906f42d33664128eea06c46b718fc7fbd1a9f5c",
369 | "sha256:2df74b8ff6fae6965c527e97cca6c6c944886aae474b490e17f92adfbe843417"
370 | ],
371 | "version": "==0.2.4"
372 | },
373 | "sphinxcontrib-websupport": {
374 | "hashes": [
375 | "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2",
376 | "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9"
377 | ],
378 | "version": "==1.0.1"
379 | },
380 | "tqdm": {
381 | "hashes": [
382 | "sha256:8c281e644b62dee309259a6fe49e2d7eb3d6fefea978855394aeb3c099b13201",
383 | "sha256:5e6db2f1f73b97139da33268838966337d19baf539926ca3f7116620aacb7a00"
384 | ],
385 | "version": "==4.19.1.post1"
386 | },
387 | "traitlets": {
388 | "hashes": [
389 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9",
390 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835"
391 | ],
392 | "version": "==4.3.2"
393 | },
394 | "twine": {
395 | "hashes": [
396 | "sha256:d3ce5c480c22ccfb761cd358526e862b32546d2fe4bc93d46b5cf04ea3cc46ca",
397 | "sha256:caa45b7987fc96321258cd7668e3be2ff34064f5c66d2d975b641adca659c1ab"
398 | ],
399 | "version": "==1.9.1"
400 | },
401 | "urllib3": {
402 | "hashes": [
403 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
404 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
405 | ],
406 | "version": "==1.22"
407 | },
408 | "wcwidth": {
409 | "hashes": [
410 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c",
411 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"
412 | ],
413 | "version": "==0.1.7"
414 | },
415 | "wheel": {
416 | "hashes": [
417 | "sha256:e721e53864f084f956f40f96124a74da0631ac13fbbd1ba99e8e2b5e9cafdf64",
418 | "sha256:9515fe0a94e823fd90b08d22de45d7bde57c90edce705b22f5e1ecf7e1b653c8"
419 | ],
420 | "version": "==0.30.0"
421 | }
422 | }
423 | }
424 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =====================
2 | django-templated-mail
3 | =====================
4 |
5 | .. image:: https://img.shields.io/pypi/v/django-templated-mail.svg
6 | :target: https://pypi.org/project/django-templated-mail
7 |
8 | .. image:: https://img.shields.io/travis/sunscrapers/django-templated-mail.svg
9 | :target: https://travis-ci.org/sunscrapers/django-templated-mail
10 |
11 | .. image:: https://img.shields.io/codecov/c/github/sunscrapers/django-templated-mail.svg
12 | :target: https://codecov.io/gh/sunscrapers/django-templated-mail
13 |
14 | .. image:: https://img.shields.io/scrutinizer/g/sunscrapers/django-templated-mail.svg
15 | :target: https://scrutinizer-ci.com/g/sunscrapers/django-templated-mail
16 |
17 | A simple wrapper for ``django.core.mail.EmailMultiAlternatives`` which makes
18 | use of Django template system to store email content in separate file.
19 |
20 | Developed by `SUNSCRAPERS `_ with passion & patience.
21 |
22 | Installation
23 | ============
24 |
25 | Simply install using ``pip``:
26 |
27 | .. code-block:: bash
28 |
29 | $ pip install -U django-templated-mail
30 |
31 | Documentation
32 | =============
33 |
34 | Documentation is available to study at
35 | `http://django-templated-mail.readthedocs.io `_
36 | and in ``docs`` directory.
37 |
38 | Contributing and development
39 | ============================
40 |
41 | To start developing on **django-templated-mail**, clone the repository:
42 |
43 | .. code-block:: bash
44 |
45 | $ git clone git@github.com:sunscrapers/django-templated-mail.git
46 |
47 | If you are a **pipenv** user you can quickly setup testing environment by
48 | using Make commands:
49 |
50 | .. code-block:: bash
51 |
52 | $ make init
53 | $ make test
54 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = django-templated-mail
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=python -msphinx
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 | set SPHINXPROJ=django-templated-mail
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # django-templated-mail documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Oct 6 16:39:21 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | # import os
21 | # import sys
22 | # sys.path.insert(0, os.path.abspath('.'))
23 |
24 |
25 | # -- General configuration ------------------------------------------------
26 |
27 | # If your documentation needs a minimal Sphinx version, state it here.
28 | #
29 | # needs_sphinx = '1.0'
30 |
31 | # Add any Sphinx extension module names here, as strings. They can be
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
33 | # ones.
34 | extensions = []
35 |
36 | # Add any paths that contain templates here, relative to this directory.
37 | templates_path = ['_templates']
38 |
39 | # The suffix(es) of source filenames.
40 | # You can specify multiple suffix as a list of string:
41 | #
42 | # source_suffix = ['.rst', '.md']
43 | source_suffix = '.rst'
44 |
45 | # The master toctree document.
46 | master_doc = 'index'
47 |
48 | # General information about the project.
49 | project = 'django-templated-mail'
50 | copyright = '2018, Sunscrapers'
51 | author = 'Sunscrapers'
52 |
53 | # The version info for the project you're documenting, acts as replacement for
54 | # |version| and |release|, also used in various other places throughout the
55 | # built documents.
56 | #
57 | # The short X.Y version.
58 | version = '1.1'
59 | # The full version, including alpha/beta/rc tags.
60 | release = '1.1.1'
61 |
62 | # The language for content autogenerated by Sphinx. Refer to documentation
63 | # for a list of supported languages.
64 | #
65 | # This is also used if you do content translation via gettext catalogs.
66 | # Usually you set "language" from the command line for these cases.
67 | language = None
68 |
69 | # List of patterns, relative to source directory, that match files and
70 | # directories to ignore when looking for source files.
71 | # This patterns also effect to html_static_path and html_extra_path
72 | exclude_patterns = []
73 |
74 | # The name of the Pygments (syntax highlighting) style to use.
75 | pygments_style = 'sphinx'
76 |
77 | # If true, `todo` and `todoList` produce output, else they produce nothing.
78 | todo_include_todos = False
79 |
80 |
81 | # -- Options for HTML output ----------------------------------------------
82 |
83 | # The theme to use for HTML and HTML Help pages. See the documentation for
84 | # a list of builtin themes.
85 | #
86 | try:
87 | import sphinx_rtd_theme
88 | except:
89 | html_theme = 'default'
90 | else:
91 | html_theme = 'sphinx_rtd_theme'
92 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
93 |
94 | # Theme options are theme-specific and customize the look and feel of a theme
95 | # further. For a list of options available for each theme, see the
96 | # documentation.
97 | #
98 | # html_theme_options = {}
99 |
100 | # Add any paths that contain custom static files (such as style sheets) here,
101 | # relative to this directory. They are copied after the builtin static files,
102 | # so a file named "default.css" will overwrite the builtin "default.css".
103 | html_static_path = ['_static']
104 |
105 | # Custom sidebar templates, must be a dictionary that maps document names
106 | # to template names.
107 | #
108 | # This is required for the alabaster theme
109 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
110 | html_sidebars = {
111 | '**': [
112 | 'about.html',
113 | 'navigation.html',
114 | 'relations.html', # needs 'show_related': True theme option to display
115 | 'searchbox.html',
116 | 'donate.html',
117 | ]
118 | }
119 |
120 |
121 | # -- Options for HTMLHelp output ------------------------------------------
122 |
123 | # Output file base name for HTML help builder.
124 | htmlhelp_basename = 'django-templated-maildoc'
125 |
126 |
127 | # -- Options for LaTeX output ---------------------------------------------
128 |
129 | latex_elements = {
130 | # The paper size ('letterpaper' or 'a4paper').
131 | #
132 | # 'papersize': 'letterpaper',
133 |
134 | # The font size ('10pt', '11pt' or '12pt').
135 | #
136 | # 'pointsize': '10pt',
137 |
138 | # Additional stuff for the LaTeX preamble.
139 | #
140 | # 'preamble': '',
141 |
142 | # Latex figure (float) alignment
143 | #
144 | # 'figure_align': 'htbp',
145 | }
146 |
147 | # Grouping the document tree into LaTeX files. List of tuples
148 | # (source start file, target name, title,
149 | # author, documentclass [howto, manual, or own class]).
150 | latex_documents = [(
151 | master_doc, 'django-templated-mail.tex',
152 | 'django-templated-mail Documentation', 'Sunscrapers', 'manual'
153 | )]
154 |
155 |
156 | # -- Options for manual page output ---------------------------------------
157 |
158 | # One entry per manual page. List of tuples
159 | # (source start file, name, description, authors, manual section).
160 | man_pages = [(
161 | master_doc, 'django-templated-mail',
162 | 'django-templated-mail Documentation', [author], 1
163 | )]
164 |
165 |
166 | # -- Options for Texinfo output -------------------------------------------
167 |
168 | # Grouping the document tree into Texinfo files. List of tuples
169 | # (source start file, target name, title, author,
170 | # dir menu entry, description, category)
171 | texinfo_documents = [(
172 | master_doc, 'django-templated-mail',
173 | 'django-templated-mail Documentation', author, 'django-templated-mail',
174 | 'One line description of project.', 'Miscellaneous'
175 | )]
176 |
--------------------------------------------------------------------------------
/docs/source/getting_started.rst:
--------------------------------------------------------------------------------
1 | Getting started
2 | ===============
3 |
4 | Supported Python versions
5 | -------------------------
6 |
7 | * Python 2.7
8 | * Python 3.4
9 | * Python 3.5
10 | * Python 3.6
11 |
12 | Supported Django versions
13 | -------------------------
14 |
15 | * Django 1.8
16 | * Django 1.11
17 | * Django 2.0
18 |
19 | Installation
20 | ------------
21 |
22 | .. code-block:: bash
23 |
24 | $ pip install -U django-templated-mail
25 |
26 | No need to add it to your ``INSTALLED_APPS``.
27 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. django-templated-mail documentation master file, created by
2 | sphinx-quickstart on Fri Oct 6 16:39:21 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to django-templated-mail documentation!
7 | ===============================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 | getting_started
13 | settings
14 | templates_syntax
15 | sample_usage
16 |
17 | Indices and tables
18 | ==================
19 |
20 | * :ref:`genindex`
21 | * :ref:`search`
22 |
23 |
--------------------------------------------------------------------------------
/docs/source/sample_usage.rst:
--------------------------------------------------------------------------------
1 | Sample usage
2 | ============
3 |
4 | At first let's discuss the simplest possible use case, where you just wish to
5 | send an email to a given address and using the given template.
6 |
7 | .. code-block:: python
8 |
9 | from templated_mail.mail import BaseEmailMessage
10 |
11 | BaseEmailMessage(template_name='email.html').send(to=['foo@bar.tld'])
12 |
13 | This one-liner will do all of the work required to render proper template blocks
14 | and assign the results to proper email pieces. It will also determine appropriate
15 | content type (including support for MIME) and send the output message to provided
16 | list of email address'.
17 |
18 | You might also wish to define your own subclass of
19 | ``templated_mail.mail.BaseEmailMessage`` to customize a thing or two.
20 | What might be most interesting for you is the ``get_context_data`` method,
21 | which returns context used during template rendering.
22 |
23 | .. code-block:: python
24 |
25 | class MyEmailMessage(BaseEmailMessage):
26 | def get_context_data(self):
27 | context = super(MyEmailMessage, self).get_context_data()
28 | context['foo'] = 'bar'
29 | return context
30 |
31 | You might also provide custom context data using the ``context`` parameter.
32 |
33 | .. code-block:: python
34 |
35 | from templated_mail.mail import BaseEmailMessage
36 |
37 | BaseEmailMessage(context={'foo': 'bar'}, template_name='email.html').send(to=['foo@bar.tld'])
38 |
39 | In other cases you might notice that some of your emails use common ``template_name``
40 | and so to save some space you might wish to override the base class' attribute.
41 |
42 | .. code-block:: python
43 |
44 | class MyEmailMessage(BaseEmailMessage):
45 | template_name = 'email.html'
46 |
--------------------------------------------------------------------------------
/docs/source/settings.rst:
--------------------------------------------------------------------------------
1 | Settings
2 | ========
3 |
4 | You may optionally provide following settings:
5 |
6 | .. code-block:: python
7 |
8 | 'DOMAIN': 'example.com'
9 | 'SITE_NAME': 'Foo Website'
10 |
11 | DOMAIN
12 | ------
13 |
14 | Used in email template context. In most cases it is used to simplify building URLs,
15 | when frontend and backend are hosted under different address'. If not provided
16 | the current site's domain will be used.
17 |
18 |
19 | **Required**: ``False``
20 |
21 | SITE_NAME
22 | ---------
23 |
24 | Used in email template context. Usually it will contain the desired title of your
25 | app. If not provided the current site's name will be used.
26 |
27 |
28 | **Default**: ``False``
29 |
--------------------------------------------------------------------------------
/docs/source/templates_syntax.rst:
--------------------------------------------------------------------------------
1 | Templates syntax
2 | ================
3 |
4 | Email templates can be built using three simple blocks:
5 |
6 | - ``subject`` - used for subject of an email message
7 | - ``text_body`` - used for plaintext body of an email message (not required)
8 | - ``html_body`` - used for html body of an email message (not required)
9 |
10 | Examples
11 | --------
12 |
13 | .. code-block:: html
14 |
15 | {% block subject %}Text and HTML mail subject{% endblock %}
16 |
17 | {% block text_body %}Foobar email content{% endblock %}
18 |
19 | .. code-block:: html
20 |
21 | {% block subject %}Text and HTML mail subject{% endblock %}
22 |
23 | {% block text_body %}Foobar email content{% endblock %}
24 | {% block html_body %}Foobar email content
{% endblock %}
25 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/__init__.py
--------------------------------------------------------------------------------
/examples/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/config/__init__.py
--------------------------------------------------------------------------------
/examples/config/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 |
6 |
7 | # Quick-start development settings - unsuitable for production
8 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
9 |
10 | # SECURITY WARNING: keep the secret key used in production secret!
11 | SECRET_KEY = '()!xq2ufm%j@n^!v^p08&w87vvg)rk^=aoj8u1(4xho5iuh-)l'
12 |
13 | # SECURITY WARNING: don't run with debug turned on in production!
14 | DEBUG = True
15 |
16 | ALLOWED_HOSTS = []
17 |
18 |
19 | # Application definition
20 |
21 | INSTALLED_APPS = [
22 | 'django.contrib.admin',
23 | 'django.contrib.auth',
24 | 'django.contrib.contenttypes',
25 | 'django.contrib.sessions',
26 | 'django.contrib.messages',
27 | 'django.contrib.staticfiles',
28 |
29 | 'templated_mail',
30 |
31 | 'simple',
32 | ]
33 |
34 | MIDDLEWARE = [
35 | 'django.middleware.security.SecurityMiddleware',
36 | 'django.contrib.sessions.middleware.SessionMiddleware',
37 | 'django.middleware.common.CommonMiddleware',
38 | 'django.middleware.csrf.CsrfViewMiddleware',
39 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
40 | 'django.contrib.messages.middleware.MessageMiddleware',
41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
42 | ]
43 |
44 | ROOT_URLCONF = 'config.urls'
45 |
46 | TEMPLATES = [
47 | {
48 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
49 | 'DIRS': ['templates'],
50 | 'APP_DIRS': True,
51 | 'OPTIONS': {
52 | 'context_processors': [
53 | 'django.template.context_processors.debug',
54 | 'django.template.context_processors.request',
55 | 'django.contrib.auth.context_processors.auth',
56 | 'django.contrib.messages.context_processors.messages',
57 | ],
58 | },
59 | },
60 | ]
61 |
62 | WSGI_APPLICATION = 'config.wsgi.application'
63 |
64 |
65 | # Database
66 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
67 |
68 | DATABASES = {
69 | 'default': {
70 | 'ENGINE': 'django.db.backends.sqlite3',
71 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
72 | }
73 | }
74 |
75 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
76 |
77 | # Password validation
78 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
79 |
80 | AUTH_PASSWORD_VALIDATORS = [
81 | {
82 | 'NAME': 'django.contrib.auth.password_validation.'
83 | 'UserAttributeSimilarityValidator',
84 | },
85 | {
86 | 'NAME': 'django.contrib.auth.password_validation.'
87 | 'MinimumLengthValidator',
88 | },
89 | {
90 | 'NAME': 'django.contrib.auth.password_validation.'
91 | 'CommonPasswordValidator',
92 | },
93 | {
94 | 'NAME': 'django.contrib.auth.password_validation.'
95 | 'NumericPasswordValidator',
96 | },
97 | ]
98 |
99 |
100 | # Internationalization
101 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
102 |
103 | LANGUAGE_CODE = 'en-us'
104 |
105 | TIME_ZONE = 'UTC'
106 |
107 | USE_I18N = True
108 |
109 | USE_L10N = True
110 |
111 | USE_TZ = True
112 |
113 |
114 | # Static files (CSS, JavaScript, Images)
115 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
116 |
117 | STATIC_URL = '/static/'
118 |
--------------------------------------------------------------------------------
/examples/config/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import include, url
2 | from django.contrib import admin
3 |
4 | urlpatterns = [
5 | url(r'^admin/', admin.site.urls),
6 | url(r'^simple/', include('simple.urls', namespace='simple')),
7 | ]
8 |
--------------------------------------------------------------------------------
/examples/config/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for config project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/examples/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
7 | from django.core.management import execute_from_command_line
8 | execute_from_command_line(sys.argv)
9 |
--------------------------------------------------------------------------------
/examples/simple/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/simple/__init__.py
--------------------------------------------------------------------------------
/examples/simple/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SimpleConfig(AppConfig):
5 | name = 'simple'
6 |
--------------------------------------------------------------------------------
/examples/simple/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/simple/migrations/__init__.py
--------------------------------------------------------------------------------
/examples/simple/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from simple import views
4 |
5 |
6 | urlpatterns = [
7 | url(r'^mail/txt_and_html', views.text_and_html_mail_view),
8 | url(r'^mail/txt', views.text_mail_view),
9 | url(r'^mail/html', views.html_mail_view),
10 | ]
11 |
--------------------------------------------------------------------------------
/examples/simple/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 |
3 | from templated_mail.mail import BaseEmailMessage
4 |
5 |
6 | class TextEmailMessage(BaseEmailMessage):
7 | template_name = 'text_mail.html'
8 |
9 |
10 | class HTMLEmailMessage(BaseEmailMessage):
11 | template_name = 'html_mail.html'
12 |
13 |
14 | class TextAndHTMLEmailMessage(BaseEmailMessage):
15 | template_name = 'text_and_html_mail.html'
16 |
17 |
18 | def text_mail_view(request):
19 | recipients = ['foo@bar.tld']
20 | TextEmailMessage(request).send(to=recipients)
21 | return HttpResponse('Text mail has been sent.')
22 |
23 |
24 | def html_mail_view(request):
25 | recipients = ['foo@bar.tld']
26 | HTMLEmailMessage(request).send(to=recipients)
27 | return HttpResponse('HTML mail has been sent.')
28 |
29 |
30 | def text_and_html_mail_view(request):
31 | recipients = ['foo@bar.tld']
32 | TextAndHTMLEmailMessage(request).send(to=recipients)
33 | return HttpResponse('Text and HTML mail has been sent.')
34 |
--------------------------------------------------------------------------------
/examples/templates/html_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}HTML mail subject{% endblock %}
2 |
3 | {% block html_body %}Foobar email content
{% endblock %}
--------------------------------------------------------------------------------
/examples/templates/text_and_html_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}Text and HTML mail subject{% endblock %}
2 |
3 | {% block text_body %}Foobar email content{% endblock %}
4 | {% block html_body %}Foobar email content
{% endblock %}
--------------------------------------------------------------------------------
/examples/templates/text_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}Text mail subject{% endblock %}
2 |
3 | {% block text_body %}Foobar email content{% endblock %}
--------------------------------------------------------------------------------
/requirements/base:
--------------------------------------------------------------------------------
1 | django>=1.8
2 |
--------------------------------------------------------------------------------
/requirements/test:
--------------------------------------------------------------------------------
1 | coverage==4.4.1
2 | flake8==3.4.1
3 | pytest==3.2.2
4 | pytest-cov==2.5.1
5 | pytest-django==3.1.2
6 | pytest-pythonpath==0.7.1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | from setuptools import setup
5 |
6 |
7 | with open('README.rst', 'r') as f:
8 | readme = f.read()
9 |
10 |
11 | def get_packages(package):
12 | return [
13 | dirpath for dirpath, dirnames, filenames in os.walk(package)
14 | if os.path.exists(os.path.join(dirpath, '__init__.py'))
15 | ]
16 |
17 |
18 | setup(
19 | name='django-templated-mail',
20 | version='1.1.1',
21 | packages=get_packages('templated_mail'),
22 | license='MIT',
23 | author='Sunscrapers',
24 | description='Send emails using Django template system.',
25 | author_email='info@sunscrapers.com',
26 | long_description=readme,
27 | install_requires=[],
28 | include_package_data=True,
29 | url='https://github.com/sunscrapers/django-templated-mail',
30 | classifiers=[
31 | 'Development Status :: 5 - Production/Stable',
32 | 'Framework :: Django :: 1.11',
33 | 'Framework :: Django :: 2.0',
34 | 'Framework :: Django :: 2.1',
35 | 'Intended Audience :: Developers',
36 | 'License :: OSI Approved :: MIT License',
37 | 'Operating System :: OS Independent',
38 | 'Programming Language :: Python',
39 | 'Programming Language :: Python :: 2.7',
40 | 'Programming Language :: Python :: 3.4',
41 | 'Programming Language :: Python :: 3.5',
42 | 'Programming Language :: Python :: 3.6',
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/templated_mail/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/templated_mail/__init__.py
--------------------------------------------------------------------------------
/templated_mail/mail.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.sites.shortcuts import get_current_site
3 | from django.core import mail
4 | from django.template.context import make_context
5 | from django.template.loader import get_template
6 | from django.template.loader_tags import BlockNode, ExtendsNode
7 | from django.views.generic.base import ContextMixin
8 |
9 |
10 | class BaseEmailMessage(mail.EmailMultiAlternatives, ContextMixin):
11 | _node_map = {
12 | 'subject': 'subject',
13 | 'text_body': 'body',
14 | 'html_body': 'html',
15 | }
16 | template_name = None
17 |
18 | def __init__(self, request=None, context=None, template_name=None,
19 | *args, **kwargs):
20 | super(BaseEmailMessage, self).__init__(*args, **kwargs)
21 |
22 | self.request = request
23 | self.context = {} if context is None else context
24 | self.html = None
25 |
26 | if template_name is not None:
27 | self.template_name = template_name
28 |
29 | def get_context_data(self, **kwargs):
30 | ctx = super(BaseEmailMessage, self).get_context_data(**kwargs)
31 | context = dict(ctx, **self.context)
32 | if self.request:
33 | site = get_current_site(self.request)
34 | domain = context.get('domain') or (
35 | getattr(settings, 'DOMAIN', '') or site.domain
36 | )
37 | protocol = context.get('protocol') or (
38 | 'https' if self.request.is_secure() else 'http'
39 | )
40 | site_name = context.get('site_name') or (
41 | getattr(settings, 'SITE_NAME', '') or site.name
42 | )
43 | user = context.get('user') or self.request.user
44 | else:
45 | domain = context.get('domain') or getattr(settings, 'DOMAIN', '')
46 | protocol = context.get('protocol') or 'http'
47 | site_name = context.get('site_name') or getattr(
48 | settings, 'SITE_NAME', ''
49 | )
50 | user = context.get('user')
51 |
52 | context.update({
53 | 'domain': domain,
54 | 'protocol': protocol,
55 | 'site_name': site_name,
56 | 'user': user
57 | })
58 | return context
59 |
60 | def render(self):
61 | context = make_context(self.get_context_data(), request=self.request)
62 | template = get_template(self.template_name)
63 | with context.bind_template(template.template):
64 | blocks = self._get_blocks(template.template.nodelist, context)
65 | for block_node in blocks.values():
66 | self._process_block(block_node, context)
67 | self._attach_body()
68 |
69 | def send(self, to, *args, **kwargs):
70 | self.render()
71 |
72 | self.to = to
73 | self.cc = kwargs.pop('cc', [])
74 | self.bcc = kwargs.pop('bcc', [])
75 | self.reply_to = kwargs.pop('reply_to', [])
76 | self.from_email = kwargs.pop(
77 | 'from_email', settings.DEFAULT_FROM_EMAIL
78 | )
79 |
80 | super(BaseEmailMessage, self).send(*args, **kwargs)
81 |
82 | def _process_block(self, block_node, context):
83 | attr = self._node_map.get(block_node.name)
84 | if attr is not None:
85 | setattr(self, attr, block_node.render(context).strip())
86 |
87 | def _get_blocks(self, nodelist, context):
88 | blocks = {}
89 | for node in nodelist:
90 | if isinstance(node, ExtendsNode):
91 | parent = node.get_parent(context)
92 | blocks.update(self._get_blocks(parent.nodelist, context))
93 | blocks.update({
94 | node.name: node for node in nodelist.get_nodes_by_type(BlockNode)
95 | })
96 | return blocks
97 |
98 | def _attach_body(self):
99 | if self.body and self.html:
100 | self.attach_alternative(self.html, 'text/html')
101 | elif self.html:
102 | self.body = self.html
103 | self.content_subtype = 'html'
104 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/tests/__init__.py
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | from django.views.generic.base import ContextMixin
2 | from templated_mail.mail import BaseEmailMessage
3 |
4 |
5 | class MockMailContext(ContextMixin):
6 | def get_context_data(self, **kwargs):
7 | context = super(MockMailContext, self).get_context_data(**kwargs)
8 | context['thing'] = 42
9 | return context
10 |
11 |
12 | class MockMail(BaseEmailMessage, MockMailContext):
13 | pass
14 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4 |
5 | SECRET_KEY = 'quentin-tarantino'
6 |
7 | DEBUG = False
8 |
9 | ALLOWED_HOSTS = []
10 |
11 | INSTALLED_APPS = [
12 | 'django.contrib.admin',
13 | 'django.contrib.auth',
14 | 'django.contrib.contenttypes',
15 | 'django.contrib.sessions',
16 | 'django.contrib.messages',
17 | 'django.contrib.staticfiles',
18 |
19 | 'tests',
20 | ]
21 |
22 | MIDDLEWARE = [
23 | 'django.middleware.security.SecurityMiddleware',
24 | 'django.contrib.sessions.middleware.SessionMiddleware',
25 | 'django.middleware.common.CommonMiddleware',
26 | 'django.middleware.csrf.CsrfViewMiddleware',
27 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
28 | 'django.contrib.messages.middleware.MessageMiddleware',
29 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
30 | ]
31 |
32 | TEMPLATES = [
33 | {
34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
35 | 'DIRS': ['templates'],
36 | 'APP_DIRS': True,
37 | 'OPTIONS': {
38 | 'context_processors': [
39 | 'django.template.context_processors.debug',
40 | 'django.template.context_processors.request',
41 | 'django.contrib.auth.context_processors.auth',
42 | 'django.contrib.messages.context_processors.messages',
43 | ],
44 | },
45 | },
46 | ]
47 |
48 | DATABASES = {
49 | 'default': {
50 | 'ENGINE': 'django.db.backends.sqlite3',
51 | 'NAME': ':memory:',
52 | }
53 | }
54 |
55 | LANGUAGE_CODE = 'en-us'
56 |
57 | TIME_ZONE = 'UTC'
58 |
59 | USE_I18N = True
60 |
61 | USE_L10N = True
62 |
63 | USE_TZ = True
64 |
65 | STATIC_URL = '/static/'
66 |
--------------------------------------------------------------------------------
/tests/templates/extends.html:
--------------------------------------------------------------------------------
1 | {% extends "text_and_html_mail.html" %}
2 |
3 | {% block html_body %}Some extended HTML body{% endblock html_body %}
4 |
--------------------------------------------------------------------------------
/tests/templates/html_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}HTML mail subject{% endblock %}
2 |
3 | {% block html_body %}Foobar email content
{% endblock %}
4 |
--------------------------------------------------------------------------------
/tests/templates/nested_extends.html:
--------------------------------------------------------------------------------
1 | {% extends "extends.html" %}
2 |
3 | {% block text_body %}Some extended text body{% endblock text_body %}
4 |
--------------------------------------------------------------------------------
/tests/templates/text_and_html_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}Text and HTML mail subject{% endblock %}
2 |
3 | {% block text_body %}Foobar email content{% endblock %}
4 | {% block html_body %}Foobar email content
{% endblock %}
--------------------------------------------------------------------------------
/tests/templates/text_mail.html:
--------------------------------------------------------------------------------
1 | {% block subject %}Text mail subject{% endblock %}
2 |
3 | {% block text_body %}Foobar email content{% endblock %}
--------------------------------------------------------------------------------
/tests/test_mail.py:
--------------------------------------------------------------------------------
1 | try:
2 | from unittest import mock
3 | except ImportError:
4 | import mock
5 |
6 | from django.contrib.auth.models import AnonymousUser
7 | from django.contrib.sites.shortcuts import get_current_site
8 | from django.core import mail
9 | from django.test import override_settings, RequestFactory, TestCase
10 | from templated_mail.mail import BaseEmailMessage
11 |
12 | from .helpers import MockMail
13 |
14 |
15 | class TestBaseEmailMessage(TestCase):
16 | def setUp(self):
17 | self.factory = RequestFactory()
18 | self.recipients = ['foo@bar.tld']
19 |
20 | @mock.patch('django.core.handlers.wsgi.WSGIRequest.is_secure')
21 | def test_get_context_data_with_insecure_request(self, is_secure_mock):
22 | is_secure_mock.return_value = False
23 |
24 | request = self.factory.get('/')
25 | request.user = AnonymousUser()
26 |
27 | email_message = BaseEmailMessage(
28 | request=request, template_name='text_mail.html'
29 | )
30 | context = email_message.get_context_data()
31 | site = get_current_site(request)
32 |
33 | self.assertEquals(context['domain'], site.domain)
34 | self.assertEquals(context['protocol'], 'http')
35 | self.assertEquals(context['site_name'], site.name)
36 | self.assertEquals(context['user'], request.user)
37 |
38 | @mock.patch('django.core.handlers.wsgi.WSGIRequest.is_secure')
39 | def test_get_context_data_with_secure_request(self, is_secure_mock):
40 | is_secure_mock.return_value = True
41 |
42 | request = self.factory.get('/')
43 | request.user = AnonymousUser()
44 |
45 | email_message = BaseEmailMessage(
46 | request=request, template_name='text_mail.html'
47 | )
48 | context = email_message.get_context_data()
49 | site = get_current_site(request)
50 |
51 | self.assertEquals(context['domain'], site.domain)
52 | self.assertEquals(context['protocol'], 'https')
53 | self.assertEquals(context['site_name'], site.name)
54 | self.assertEquals(context['user'], request.user)
55 |
56 | def test_get_context_data_without_request_no_context(self):
57 | email_message = BaseEmailMessage(template_name='text_mail.html')
58 | context = email_message.get_context_data()
59 |
60 | self.assertEquals(context['domain'], '')
61 | self.assertEquals(context['protocol'], 'http')
62 | self.assertEquals(context['site_name'], '')
63 | self.assertEquals(context['user'], None)
64 |
65 | def test_get_context_data_without_request_user_context(self):
66 | user = AnonymousUser()
67 | email_message = BaseEmailMessage(
68 | context={'user': user}, template_name='text_mail.html'
69 | )
70 | context = email_message.get_context_data()
71 |
72 | self.assertEquals(context['domain'], '')
73 | self.assertEquals(context['protocol'], 'http')
74 | self.assertEquals(context['site_name'], '')
75 | self.assertEquals(context['user'], user)
76 |
77 | def test_text_mail_contains_valid_data(self):
78 | request = self.factory.get('/')
79 | request.user = AnonymousUser()
80 |
81 | BaseEmailMessage(
82 | request=request, template_name='text_mail.html'
83 | ).send(to=self.recipients)
84 |
85 | self.assertEqual(len(mail.outbox), 1)
86 | self.assertEqual(mail.outbox[0].recipients(), self.recipients)
87 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
88 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
89 | self.assertEqual(mail.outbox[0].alternatives, [])
90 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
91 |
92 | def test_html_mail_contains_valid_data(self):
93 | request = self.factory.get('/')
94 | request.user = AnonymousUser()
95 |
96 | BaseEmailMessage(
97 | request=request, template_name='html_mail.html'
98 | ).send(to=self.recipients)
99 |
100 | self.assertEqual(len(mail.outbox), 1)
101 | self.assertEqual(mail.outbox[0].recipients(), self.recipients)
102 | self.assertEqual(mail.outbox[0].subject, 'HTML mail subject')
103 | self.assertEqual(mail.outbox[0].body, 'Foobar email content
')
104 | self.assertEqual(mail.outbox[0].alternatives, [])
105 | self.assertEqual(mail.outbox[0].content_subtype, 'html')
106 |
107 | def test_text_and_html_mail_contains_valid_data(self):
108 | request = self.factory.get('/')
109 | request.user = AnonymousUser()
110 |
111 | BaseEmailMessage(
112 | request=request, template_name='text_and_html_mail.html'
113 | ).send(to=self.recipients)
114 |
115 | self.assertEqual(len(mail.outbox), 1)
116 | self.assertEqual(mail.outbox[0].recipients(), self.recipients)
117 | self.assertEqual(mail.outbox[0].subject, 'Text and HTML mail subject')
118 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
119 | self.assertEqual(
120 | mail.outbox[0].alternatives,
121 | [('Foobar email content
', 'text/html')]
122 | )
123 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
124 |
125 | def test_can_send_mail_with_none_request(self):
126 | BaseEmailMessage(
127 | request=None, template_name='text_mail.html'
128 | ).send(to=self.recipients)
129 |
130 | self.assertEqual(len(mail.outbox), 1)
131 | self.assertEqual(mail.outbox[0].recipients(), self.recipients)
132 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
133 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
134 | self.assertEqual(mail.outbox[0].alternatives, [])
135 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
136 |
137 | def test_mail_cc_is_sent_to_valid_cc(self):
138 | request = self.factory.get('/')
139 | request.user = AnonymousUser()
140 |
141 | cc = ['email@example.tld']
142 |
143 | BaseEmailMessage(
144 | request=request, template_name='text_mail.html'
145 | ).send(to=self.recipients, cc=cc)
146 |
147 | self.assertEqual(len(mail.outbox), 1)
148 | self.assertEqual(mail.outbox[0].to, self.recipients)
149 | self.assertEqual(mail.outbox[0].cc, cc)
150 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
151 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
152 | self.assertEqual(mail.outbox[0].alternatives, [])
153 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
154 |
155 | def test_mail_bcc_is_sent_to_valid_bcc(self):
156 | request = self.factory.get('/')
157 | request.user = AnonymousUser()
158 |
159 | bcc = ['email@example.tld']
160 |
161 | BaseEmailMessage(
162 | request=request, template_name='text_mail.html'
163 | ).send(to=self.recipients, bcc=bcc)
164 |
165 | self.assertEqual(len(mail.outbox), 1)
166 | self.assertEqual(mail.outbox[0].to, self.recipients)
167 | self.assertEqual(mail.outbox[0].bcc, bcc)
168 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
169 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
170 | self.assertEqual(mail.outbox[0].alternatives, [])
171 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
172 |
173 | def test_mail_reply_to_is_sent_with_valid_reply_to(self):
174 | request = self.factory.get('/')
175 | request.user = AnonymousUser()
176 |
177 | reply_to = ['email@example.tld']
178 |
179 | BaseEmailMessage(
180 | request=request, template_name='text_mail.html'
181 | ).send(to=self.recipients, reply_to=reply_to)
182 |
183 | self.assertEqual(len(mail.outbox), 1)
184 | self.assertEqual(mail.outbox[0].to, self.recipients)
185 | self.assertEqual(mail.outbox[0].reply_to, reply_to)
186 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
187 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
188 | self.assertEqual(mail.outbox[0].alternatives, [])
189 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
190 |
191 | def test_mail_from_email_is_sent_with_valid_from_email(self):
192 | request = self.factory.get('/')
193 | request.user = AnonymousUser()
194 |
195 | from_email = ''
196 |
197 | BaseEmailMessage(
198 | request=request, template_name='text_mail.html'
199 | ).send(to=self.recipients, from_email=from_email)
200 |
201 | self.assertEqual(len(mail.outbox), 1)
202 | self.assertEqual(mail.outbox[0].to, self.recipients)
203 | self.assertEqual(mail.outbox[0].from_email, from_email)
204 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject')
205 | self.assertEqual(mail.outbox[0].body, 'Foobar email content')
206 | self.assertEqual(mail.outbox[0].alternatives, [])
207 | self.assertEqual(mail.outbox[0].content_subtype, 'plain')
208 |
209 | @override_settings(DEFAULT_FROM_EMAIL='default@example.tld')
210 | def test_mail_without_from_email_is_sent_with_valid_from_email(self):
211 | BaseEmailMessage(
212 | request=None, template_name='text_mail.html'
213 | ).send(to=self.recipients)
214 |
215 | self.assertEqual(len(mail.outbox), 1)
216 | self.assertEqual(mail.outbox[0].to, self.recipients)
217 | self.assertEqual(mail.outbox[0].from_email, 'default@example.tld')
218 |
219 | def test_extending_mail_with_context_mixin(self):
220 | email_message = MockMail(
221 | template_name='text_mail.html', context={'foo': 'bar'}
222 | )
223 |
224 | context = email_message.get_context_data()
225 |
226 | self.assertEquals(context['foo'], 'bar')
227 | self.assertEquals(context['thing'], 42)
228 |
229 | def test_extending_mail_template(self):
230 | email_message = BaseEmailMessage(template_name='extends.html')
231 | email_message.render()
232 |
233 | self.assertEquals(email_message.subject, 'Text and HTML mail subject')
234 | self.assertEquals(email_message.body, 'Foobar email content')
235 | self.assertEquals(email_message.html, 'Some extended HTML body')
236 |
237 | def text_nested_extending_mail_template(self):
238 | email_message = BaseEmailMessage(template_name='nested_extends.html')
239 | email_message.render()
240 |
241 | self.assertEquals(email_message.subject, 'Text and HTML mail subject')
242 | self.assertEquals(email_message.body, 'Some extended text body')
243 | self.assertEquals(email_message.html, 'Some extended HTML body')
244 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | python_paths = .
3 |
4 | [tox]
5 | envlist =
6 | py{27,34,35,36,37}-django111
7 | py{34,35,36,37}-django20
8 | py{35,36,37}-django{21,master}
9 | flake8
10 |
11 |
12 | [travis:env]
13 | DJANGO =
14 | 1.11: django111
15 | 2.0: django20
16 | 2.1: django21
17 | master: djangomaster
18 |
19 |
20 | [testenv]
21 | passenv = CI TRAVIS TRAVIS_*
22 | basepython =
23 | py27: python2.7
24 | py34: python3.4
25 | py35: python3.5
26 | py36: python3.6
27 | py37: python3.7
28 | deps =
29 | django111: django>=1.11,<2.0
30 | django20: django>=2.0,<2.1
31 | django21: django>=2.1,<2.2
32 | djangomaster: git+git://github.com/django/django.git
33 | py27: mock
34 | -rrequirements/test
35 | commands =
36 | py.test --ds=tests.settings --capture=no --cov-report term-missing --cov-report html --cov=templated_mail tests
37 |
38 |
39 | [testenv:flake8]
40 | basepython = python3
41 | skip_install = true
42 | deps = flake8
43 | commands = flake8 .
--------------------------------------------------------------------------------