├── emailtemplates ├── migrations │ ├── __init__.py │ ├── 0008_auto_20201120_1056.py │ ├── 0006_auto_20201110_1151.py │ ├── 0012_auto_20221103_1506.py │ ├── 0003_auto_20180523_1027.py │ ├── 0011_auto_20221026_0953.py │ ├── 0009_auto_20220111_1011.py │ ├── 0005_auto_20201110_1115.py │ ├── 0010_auto_20220803_1419.py │ ├── 0004_auto_20180523_1608.py │ ├── 0007_auto_20201113_1354.py │ ├── 0002_auto_20170428_1442.py │ └── 0001_initial.py ├── templates │ ├── mass_email.html │ └── admin │ │ └── emailtemplates │ │ ├── _helptext.html │ │ └── massemailmessage │ │ └── change_form.html ├── tests │ ├── __init__.py │ ├── data │ │ └── example_file.txt │ ├── test_helpers.py │ ├── test_models.py │ ├── test_template_registry.py │ └── test_email.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── pl │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── apps.py ├── __init__.py ├── shortcuts.py ├── urls.py ├── admin.py ├── forms.py ├── views.py ├── helpers.py ├── models.py ├── registry.py └── email.py ├── MANIFEST.in ├── AUTHORS ├── .gitignore ├── tox.ini ├── .github └── workflows │ └── test.yml ├── LICENSE ├── setup.py ├── runtests.py └── README.rst /emailtemplates/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /emailtemplates/templates/mass_email.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /emailtemplates/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | -------------------------------------------------------------------------------- /emailtemplates/tests/data/example_file.txt: -------------------------------------------------------------------------------- 1 | Some content of example file. -------------------------------------------------------------------------------- /emailtemplates/templates/admin/emailtemplates/_helptext.html: -------------------------------------------------------------------------------- 1 | {{ registration_item.as_form_help_text|safe }} 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include emailtemplates/static * 4 | recursive-include emailtemplates/templates * 5 | -------------------------------------------------------------------------------- /emailtemplates/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deployed/django-emailtemplates/HEAD/emailtemplates/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /emailtemplates/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deployed/django-emailtemplates/HEAD/emailtemplates/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Thanks to the following people for contributing to django-emailtemplates. 5 | 6 | Michal Czuba 7 | Dariusz Rzepka 8 | Agnieszka Rydzyńska 9 | Mariusz Korzekwa 10 | Emilia Marczyk 11 | Lukasz Taczuk 12 | Piotr Roksela -------------------------------------------------------------------------------- /emailtemplates/templates/admin/emailtemplates/massemailmessage/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | {% block object-tools-items %} 4 |
  • 5 | {% trans "Send to all users" %} 6 |
  • 7 | {{ block.super }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /emailtemplates/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.apps import AppConfig 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class EmailtempatesConfig(AppConfig): 7 | name = "emailtemplates" 8 | verbose_name = _("E-MAIL TEMPLATES") 9 | default_auto_field = "django.db.models.BigAutoField" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Mr Developer 31 | .mr.developer.cfg 32 | .project 33 | .pydevproject 34 | 35 | .idea 36 | info.txt 37 | -------------------------------------------------------------------------------- /emailtemplates/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "emailtemplates.apps.EmailtempatesConfig" 2 | 3 | VERSION = (1, 1, 14) 4 | 5 | # Dynamically calculate the version based on VERSION tuple 6 | if len(VERSION) > 2 and VERSION[2] is not None: 7 | if isinstance(VERSION[2], int): 8 | str_version = "%s.%s.%s" % VERSION[:3] 9 | else: 10 | str_version = "%s.%s_%s" % VERSION[:3] 11 | else: 12 | str_version = "%s.%s" % VERSION[:2] 13 | 14 | __version__ = str_version 15 | -------------------------------------------------------------------------------- /emailtemplates/shortcuts.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from .email import EmailFromTemplate 3 | 4 | 5 | def send_email(name, ctx_dict, send_to=None, subject="Subject", **kwargs): 6 | """ 7 | Shortcut function for EmailFromTemplate class 8 | 9 | @return: None 10 | """ 11 | 12 | eft = EmailFromTemplate(name=name) 13 | eft.subject = subject 14 | eft.context = ctx_dict 15 | eft.get_object() 16 | eft.render_message() 17 | eft.send_email(send_to=send_to, **kwargs) 18 | -------------------------------------------------------------------------------- /emailtemplates/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from packaging.version import parse 3 | 4 | 5 | django_version = parse(django.get_version()) 6 | 7 | if django_version < parse('4.0'): 8 | from django.conf.urls import url 9 | else: 10 | from django.urls import re_path as url 11 | 12 | from emailtemplates.views import email_preview_view, send_mass_email_view 13 | 14 | urlpatterns = [ 15 | url(r"^email-preview/(?P\d+)/$", email_preview_view, name="email_preview"), 16 | url( 17 | r"^send-mass-email/(?P\d+)/$", send_mass_email_view, name="send_mass_email" 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{38,39}-django32 4 | py{310,311,312}-django{40,41,42,50,51} 5 | 6 | [testenv] 7 | setenv = 8 | PYTHONPATH = {toxinidir} 9 | commands = 10 | coverage run {toxinidir}/runtests.py 11 | coverage report -m 12 | deps = 13 | packaging 14 | coverage 15 | mock 16 | django32: Django>=3.2,<3.3 17 | django40: Django>=4.0,<4.1 18 | django41: Django>=4.1,<4.2 19 | django42: Django>=4.2,<5.0 20 | django50: Django>=5.0,<5.1 21 | django51: Django>=5.1,<5.2 22 | 23 | [gh-actions] 24 | python = 25 | 3.8: py38 26 | 3.9: py39 27 | 3.10: py310 28 | 3.11: py311 29 | 3.12: py312 30 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0008_auto_20201120_1056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-20 10:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("emailtemplates", "0007_auto_20201113_1354"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="emailattachment", 15 | name="send_as_link", 16 | field=models.BooleanField(default=True, verbose_name="Send as link"), 17 | ), 18 | migrations.AddField( 19 | model_name="massemailattachment", 20 | name="send_as_link", 21 | field=models.BooleanField(default=True, verbose_name="Send as link"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Test 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python: ['3.8', '3.9', '3.10', '3.11', '3.12'] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 1 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v1 25 | with: 26 | python-version: ${{ matrix.python }} 27 | 28 | - name: Install dependencies 29 | run: python -m pip install --upgrade pip tox tox-gh-actions 30 | 31 | - name: Run tests 32 | run: tox 33 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0006_auto_20201110_1151.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-10 11:51 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("emailtemplates", "0005_auto_20201110_1115"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="emailtemplate", 16 | name="created", 17 | field=models.DateTimeField( 18 | default=django.utils.timezone.now, verbose_name="created" 19 | ), 20 | ), 21 | migrations.AlterField( 22 | model_name="emailtemplate", 23 | name="modified", 24 | field=models.DateTimeField( 25 | default=django.utils.timezone.now, verbose_name="modified" 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0012_auto_20221103_1506.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.13 on 2022-11-03 14:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("emailtemplates", "0011_auto_20221026_0953"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="emailattachment", 15 | name="attachment_file", 16 | field=models.FileField( 17 | max_length=255, 18 | upload_to="emails/attachments/", 19 | verbose_name="Attachment file", 20 | ), 21 | ), 22 | migrations.AlterField( 23 | model_name="massemailattachment", 24 | name="attachment_file", 25 | field=models.FileField( 26 | max_length=255, 27 | upload_to="emails/attachments/", 28 | verbose_name="Attachment file", 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /emailtemplates/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from django.contrib.auth import get_user_model 3 | from django.test import TestCase, override_settings 4 | from emailtemplates.helpers import mass_mailing_recipients 5 | 6 | 7 | def recipients_test_function(): 8 | return ["user@example.com", "another@example.com"] 9 | 10 | 11 | class HelpersTest(TestCase): 12 | def test_mass_mailing_recipients(self): 13 | User = get_user_model() 14 | User.objects.create(username="mike", email="mike@example.com", is_active=True) 15 | User.objects.create(username="john", email="john@example.com", is_active=False) 16 | User.objects.create(username="paul", is_active=True) 17 | self.assertEqual(list(mass_mailing_recipients()), ["mike@example.com"]) 18 | 19 | @override_settings( 20 | MASS_EMAIL_RECIPIENTS="emailtemplates.tests.test_helpers.recipients_test_function" 21 | ) 22 | def test_mass_mailing_recipients_from_settings(self): 23 | self.assertEqual( 24 | mass_mailing_recipients(), ["user@example.com", "another@example.com"] 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Deployed Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0003_auto_20180523_1027.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-05-23 10:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("emailtemplates", "0002_auto_20170428_1442"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="MassEmailMessage", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("subject", models.CharField(max_length=255, verbose_name="subject")), 28 | ("content", models.TextField(verbose_name="content")), 29 | ( 30 | "date_sent", 31 | models.DateTimeField(blank=True, null=True, verbose_name="sent"), 32 | ), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0011_auto_20221026_0953.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.13 on 2022-10-26 07:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("emailtemplates", "0010_auto_20220803_1419"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="emailtemplate", 14 | options={ 15 | "ordering": ("ordering",), 16 | "verbose_name": "Email template", 17 | "verbose_name_plural": "Email templates", 18 | }, 19 | ), 20 | migrations.AddField( 21 | model_name="emailtemplate", 22 | name="ordering", 23 | field=models.PositiveIntegerField(default=1, verbose_name="ordering"), 24 | ), 25 | migrations.AlterField( 26 | model_name="emailtemplate", 27 | name="subject", 28 | field=models.CharField( 29 | blank=True, 30 | help_text="you can use variables from table", 31 | max_length=255, 32 | verbose_name="subject", 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0009_auto_20220111_1011.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.11 on 2022-01-11 09:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("emailtemplates", "0008_auto_20201120_1056"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="emailattachment", 15 | name="id", 16 | field=models.BigAutoField( 17 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="emailtemplate", 22 | name="id", 23 | field=models.BigAutoField( 24 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="massemailattachment", 29 | name="id", 30 | field=models.BigAutoField( 31 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name="massemailmessage", 36 | name="id", 37 | field=models.BigAutoField( 38 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | install_requires = [ 10 | 'Django>=1.11', 11 | 'packaging', 12 | ] 13 | 14 | tests_require = [ 15 | 'mock', 16 | ] 17 | 18 | setup( 19 | name='django-emailtemplates', 20 | version='1.1.17', 21 | packages=find_packages(), 22 | package_data={'emailtemplates': ['locale/*/LC_MESSAGES/*.po', 'locale/*/LC_MESSAGES/*.mo']}, 23 | include_package_data=True, 24 | license='MIT License', 25 | description='A simple Django app to create emails based on database or filesystem templates.', 26 | long_description=README, 27 | url='https://github.com/deployed/django-emailtemplates', 28 | author='Wiktor Kolodziej', 29 | classifiers=[ 30 | 'Environment :: Web Environment', 31 | 'Framework :: Django', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2.7', 37 | 'Topic :: Internet :: WWW/HTTP', 38 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 39 | ], 40 | install_requires=install_requires, 41 | tests_require=tests_require 42 | ) 43 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0005_auto_20201110_1115.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-10 11:15 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("emailtemplates", "0004_auto_20180523_1608"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="emailtemplate", 15 | options={ 16 | "verbose_name": "Email template", 17 | "verbose_name_plural": "Email templates", 18 | }, 19 | ), 20 | migrations.AlterModelOptions( 21 | name="massemailmessage", 22 | options={ 23 | "verbose_name": "Mass email message", 24 | "verbose_name_plural": "Mass email messages", 25 | }, 26 | ), 27 | migrations.AlterField( 28 | model_name="massemailattachment", 29 | name="attachment_file", 30 | field=models.FileField(upload_to="", verbose_name="Attachment file"), 31 | ), 32 | migrations.AlterField( 33 | model_name="massemailattachment", 34 | name="mass_email_message", 35 | field=models.ForeignKey( 36 | on_delete=django.db.models.deletion.CASCADE, 37 | related_name="attachments", 38 | to="emailtemplates.massemailmessage", 39 | ), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import django 4 | from django.conf import settings 5 | from django.test.utils import get_runner 6 | 7 | if __name__ == "__main__": 8 | settings.configure() 9 | settings.SECRET_KEY = "secret-key" 10 | settings.INSTALLED_APPS = ( 11 | "django.contrib.auth", 12 | "django.contrib.contenttypes", 13 | "django.contrib.sessions", 14 | "django.contrib.admin", 15 | "django.contrib.messages", 16 | "emailtemplates", 17 | ) 18 | settings.MIDDLEWARE = ( 19 | "django.contrib.auth.middleware.AuthenticationMiddleware", 20 | "django.contrib.sessions.middleware.SessionMiddleware", 21 | "django.contrib.messages.middleware.MessageMiddleware", 22 | ) 23 | settings.DATABASES = { 24 | "default": { 25 | "ENGINE": "django.db.backends.sqlite3", 26 | "NAME": ":memory:", 27 | } 28 | } 29 | settings.TEMPLATES = [ 30 | { 31 | "BACKEND": "django.template.backends.django.DjangoTemplates", 32 | "OPTIONS": { 33 | "context_processors": [ 34 | "django.contrib.auth.context_processors.auth", 35 | "django.contrib.messages.context_processors.messages", 36 | ], 37 | }, 38 | } 39 | ] 40 | django.setup() 41 | TestRunner = get_runner(settings) 42 | test_runner = TestRunner(verbosity=3) 43 | failures = test_runner.run_tests(["emailtemplates"]) 44 | sys.exit(bool(failures)) 45 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0010_auto_20220803_1419.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.13 on 2022-08-03 12:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("emailtemplates", "0009_auto_20220111_1011"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="emailattachment", 15 | options={ 16 | "ordering": ["ordering"], 17 | "verbose_name": "Attachment", 18 | "verbose_name_plural": "Attachments", 19 | }, 20 | ), 21 | migrations.AlterModelOptions( 22 | name="massemailattachment", 23 | options={ 24 | "ordering": ["ordering"], 25 | "verbose_name": "Attachment", 26 | "verbose_name_plural": "Attachments", 27 | }, 28 | ), 29 | migrations.AddField( 30 | model_name="emailattachment", 31 | name="comment", 32 | field=models.TextField( 33 | blank=True, verbose_name="Comment", help_text="visible only in admin" 34 | ), 35 | ), 36 | migrations.AddField( 37 | model_name="emailattachment", 38 | name="ordering", 39 | field=models.PositiveIntegerField(default=0, verbose_name="Ordering"), 40 | ), 41 | migrations.AddField( 42 | model_name="massemailattachment", 43 | name="comment", 44 | field=models.TextField( 45 | blank=True, verbose_name="Comment", help_text="visible only in admin" 46 | ), 47 | ), 48 | migrations.AddField( 49 | model_name="massemailattachment", 50 | name="ordering", 51 | field=models.PositiveIntegerField(default=0, verbose_name="Ordering"), 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0004_auto_20180523_1608.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-05-23 16:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | from django.conf import settings 8 | 9 | 10 | class Migration(migrations.Migration): 11 | dependencies = [ 12 | ("emailtemplates", "0003_auto_20180523_1027"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="MassEmailAttachment", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ( 29 | "attachment_file", 30 | models.FileField(upload_to=b"", verbose_name="Attachment file"), 31 | ), 32 | ( 33 | "mass_email_message", 34 | models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | to="emailtemplates.MassEmailMessage", 37 | ), 38 | ), 39 | ], 40 | ), 41 | migrations.AlterField( 42 | model_name="emailtemplate", 43 | name="language", 44 | field=models.CharField( 45 | choices=settings.LANGUAGES, 46 | default=settings.LANGUAGE_CODE, 47 | max_length=10, 48 | verbose_name="language", 49 | ), 50 | ), 51 | migrations.AlterField( 52 | model_name="emailtemplate", 53 | name="title", 54 | field=models.CharField(max_length=255, verbose_name="template"), 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /emailtemplates/admin.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from django.contrib import admin 3 | from django.urls import reverse 4 | from django.utils.safestring import mark_safe 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from .forms import EmailTemplateAdminForm, MassEmailMessageForm, MassEmailAttachmentForm 8 | from .models import ( 9 | EmailTemplate, 10 | MassEmailMessage, 11 | MassEmailAttachment, 12 | EmailAttachment, 13 | ) 14 | 15 | 16 | class EmailTemplateAttachmentInline(admin.TabularInline): 17 | model = EmailTemplate.attachments.through 18 | extra = 1 19 | verbose_name = _("Attachment") 20 | verbose_name_plural = _("Attachments") 21 | 22 | 23 | class EmailTemplateAdmin(admin.ModelAdmin): 24 | """ 25 | Admin view of EmailTemplate 26 | """ 27 | 28 | list_display = ( 29 | "title", 30 | "language", 31 | "subject", 32 | ) 33 | list_display_links = ("title",) 34 | list_filter = ( 35 | "title", 36 | "language", 37 | ) 38 | search_fields = ("title", "subject") 39 | form = EmailTemplateAdminForm 40 | save_on_top = True 41 | save_as = True 42 | readonly_fields = ["show_links", "created", "modified"] 43 | inlines = [EmailTemplateAttachmentInline] 44 | 45 | def show_links(self, obj): 46 | if not obj.pk: 47 | return "" 48 | return mark_safe( 49 | '%s' 50 | % (reverse("email_preview", kwargs={"pk": obj.pk}), _("Show email preview")) 51 | ) 52 | 53 | show_links.allow_tags = True 54 | show_links.short_description = _("Actions") 55 | 56 | 57 | admin.site.register(EmailTemplate, EmailTemplateAdmin) 58 | 59 | 60 | class EmailAttachmentAdmin(admin.ModelAdmin): 61 | list_display = ["name", "comment", "ordering"] 62 | search_fields = ["name", "comment"] 63 | 64 | 65 | admin.site.register(EmailAttachment, EmailAttachmentAdmin) 66 | 67 | 68 | class MassEmailAttachmentInline(admin.TabularInline): 69 | model = MassEmailAttachment 70 | form = MassEmailAttachmentForm 71 | 72 | 73 | class MassEmailMessageAdmin(admin.ModelAdmin): 74 | list_display = ("subject", "date_sent") 75 | readonly_fields = ["date_sent"] 76 | form = MassEmailMessageForm 77 | inlines = [MassEmailAttachmentInline] 78 | 79 | 80 | admin.site.register(MassEmailMessage, MassEmailMessageAdmin) 81 | -------------------------------------------------------------------------------- /emailtemplates/forms.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from django import forms 5 | from django.core.exceptions import ValidationError 6 | from django.template import Template 7 | from django.template import TemplateSyntaxError 8 | from django.utils.functional import lazy 9 | from django.utils.safestring import mark_safe 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from emailtemplates.models import EmailTemplate, MassEmailAttachment, MassEmailMessage 13 | from emailtemplates.registry import email_templates 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class EmailTemplateAdminForm(forms.ModelForm): 19 | title = forms.ChoiceField( 20 | choices=lazy(email_templates.email_template_choices, list), label=_("template") 21 | ) 22 | 23 | class Meta: 24 | model = EmailTemplate 25 | fields = [ 26 | "title", 27 | "subject", 28 | "content", 29 | "language", 30 | "ordering", 31 | "created", 32 | "modified", 33 | ] 34 | 35 | def __init__(self, *args, **kwargs): 36 | super(EmailTemplateAdminForm, self).__init__(*args, **kwargs) 37 | self.fields["title"].help_text = mark_safe( 38 | email_templates.get_form_help_text(self.initial.get("title")) 39 | ) 40 | if self.instance.pk: 41 | self.fields["title"].widget = forms.TextInput( 42 | attrs={"readonly": "readonly", "style": "width:480px"} 43 | ) 44 | else: 45 | self.fields["content"].widget = forms.HiddenInput() 46 | self.fields["content"].required = False 47 | self.fields["subject"].widget = forms.HiddenInput() 48 | 49 | def clean_content(self): 50 | content = self.cleaned_data["content"] 51 | try: 52 | Template(content) 53 | except TemplateSyntaxError as e: 54 | raise ValidationError("Syntax error in custom email template: %s" % e) 55 | return content 56 | 57 | 58 | class MassEmailAttachmentForm(forms.ModelForm): 59 | class Meta: 60 | model = MassEmailAttachment 61 | fields = ["attachment_file"] 62 | 63 | 64 | class MassEmailMessageForm(forms.ModelForm): 65 | class Meta: 66 | model = MassEmailMessage 67 | fields = [ 68 | "subject", 69 | "content", 70 | "date_sent", 71 | ] 72 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0007_auto_20201113_1354.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-11-13 13:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("emailtemplates", "0006_auto_20201110_1151"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="EmailAttachment", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ( 26 | "name", 27 | models.CharField(blank=True, max_length=50, verbose_name="name"), 28 | ), 29 | ( 30 | "attachment_file", 31 | models.FileField( 32 | upload_to="emails/attachments/", verbose_name="Attachment file" 33 | ), 34 | ), 35 | ], 36 | options={ 37 | "verbose_name": "Attachment", 38 | "verbose_name_plural": "Attachments", 39 | "abstract": False, 40 | }, 41 | ), 42 | migrations.AlterModelOptions( 43 | name="massemailattachment", 44 | options={ 45 | "verbose_name": "Attachment", 46 | "verbose_name_plural": "Attachments", 47 | }, 48 | ), 49 | migrations.AddField( 50 | model_name="massemailattachment", 51 | name="name", 52 | field=models.CharField(blank=True, max_length=50, verbose_name="name"), 53 | ), 54 | migrations.AlterField( 55 | model_name="massemailattachment", 56 | name="attachment_file", 57 | field=models.FileField( 58 | upload_to="emails/attachments/", verbose_name="Attachment file" 59 | ), 60 | ), 61 | migrations.AddField( 62 | model_name="emailtemplate", 63 | name="attachments", 64 | field=models.ManyToManyField( 65 | blank=True, 66 | to="emailtemplates.EmailAttachment", 67 | verbose_name="attachments", 68 | ), 69 | ), 70 | ] 71 | -------------------------------------------------------------------------------- /emailtemplates/views.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | from django.urls import reverse 4 | 5 | from django.contrib import messages 6 | from django.contrib.admin.views.decorators import staff_member_required 7 | from django.http import HttpResponse, HttpResponseRedirect 8 | from django.shortcuts import get_object_or_404, redirect 9 | from django.template import Template, Context 10 | from django.views import View 11 | from django.utils.translation import gettext as _ 12 | 13 | from emailtemplates.models import EmailTemplate, MassEmailMessage 14 | from emailtemplates.registry import email_templates 15 | 16 | 17 | class EmailPreviewView(View): 18 | def get_email_template(self): 19 | return get_object_or_404(EmailTemplate, pk=self.kwargs["pk"]) 20 | 21 | def get_context_data(self): 22 | email_template = self.get_email_template() 23 | return email_templates.get_help_content(email_template.title) 24 | 25 | def get(self, request, *args, **kwargs): 26 | email_template = self.get_email_template() 27 | email_content = Template(email_template.content) 28 | return HttpResponse( 29 | email_content.render(Context(self.get_context_data())), 30 | content_type="text/html; charset=utf-8", 31 | ) 32 | 33 | 34 | email_preview_view = staff_member_required(EmailPreviewView.as_view()) 35 | 36 | 37 | class SendMassEmailView(View): 38 | def get_mass_email_message(self): 39 | return get_object_or_404(MassEmailMessage, pk=self.kwargs["pk"]) 40 | 41 | def redirect_back(self): 42 | return HttpResponseRedirect( 43 | reverse( 44 | "admin:emailtemplates_massemailmessage_change", 45 | args=(self.get_mass_email_message().pk,), 46 | ), 47 | ) 48 | 49 | def get(self, request, *args, **kwargs): 50 | mass_email_message = self.get_mass_email_message() 51 | if mass_email_message.sent: 52 | messages.success( 53 | request, 54 | _( 55 | "Mass email was already sent. " 56 | "Create new mail message or force sending from shell." 57 | ), 58 | ) 59 | return self.redirect_back() 60 | sent = mass_email_message.send() 61 | if sent: 62 | messages.success(request, _("Mass email sent successfully")) 63 | else: 64 | messages.warning( 65 | request, _("Error occurred when trying to send mass email message.") 66 | ) 67 | return self.redirect_back() 68 | 69 | 70 | send_mass_email_view = SendMassEmailView.as_view() 71 | -------------------------------------------------------------------------------- /emailtemplates/helpers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from importlib import import_module 3 | 4 | from django.template.loader import get_template 5 | 6 | try: 7 | from string import lower 8 | except ImportError: 9 | lower = str.lower 10 | 11 | from django.template.loaders import app_directories 12 | from django.contrib.auth import get_user_model 13 | from django.conf import settings 14 | 15 | 16 | class SubstringMatcher(object): 17 | """ 18 | Class to be used with Mock() in order not to supply full content 19 | of the argument (e.g. for logger). 20 | Based on: http://www.michaelpollmeier.com/python-mock-how-to-assert-a-substring-of-logger-output/ 21 | 22 | Usage with this class aliased to substr: 23 | email_logger.warning.assert_called_with(substr("Can't find EmailTemplate object in database")) 24 | 25 | This would do the same, but requires exactly the same argument content: 26 | email_logger.warning.assert_called_with("Can't find EmailTemplate object in database, using default file template.") 27 | """ 28 | 29 | def __init__(self, containing): 30 | self.containing = lower(containing) 31 | 32 | def __eq__(self, other): 33 | return lower(other).find(self.containing) > -1 34 | 35 | def __unicode__(self): 36 | return 'a string containing "%s"' % self.containing 37 | 38 | def __str__(self): 39 | return unicode(self).encode("utf-8") 40 | 41 | __repr__ = __unicode__ 42 | 43 | 44 | substr = SubstringMatcher 45 | 46 | 47 | class TemplateSourceLoader: 48 | def get_source(self, template_name): 49 | return get_template(template_name).template.source 50 | 51 | 52 | def mass_mailing_recipients(): 53 | """ 54 | Returns iterable of all mass email recipients. 55 | Default behavior will be to return list of all active users' emails. 56 | This can be changed by providing callback in settings return some other list of users, 57 | when user emails are stored in many, non default models. 58 | To accomplish that add constant MASS_EMAIL_RECIPIENTS to settings. It should contain path to function, e.g. 59 | >>> MASS_EMAIL_RECIPIENTS = 'emailtemplates.helpers.mass_mailing_recipients' 60 | 61 | :rtype iterable 62 | """ 63 | if hasattr(settings, "MASS_EMAIL_RECIPIENTS"): 64 | callback_name = settings.MASS_EMAIL_RECIPIENTS.split(".") 65 | module_name = ".".join(callback_name[:-1]) 66 | func_name = callback_name[-1] 67 | module = import_module(module_name) 68 | func = getattr(module, func_name, lambda: []) 69 | return func() 70 | User = get_user_model() 71 | if hasattr(User, "is_active") and hasattr(User, "email"): 72 | filtered_users = ( 73 | User.objects.filter(is_active=True) 74 | .exclude(email__isnull=True) 75 | .exclude(email__exact="") 76 | ) 77 | return filtered_users.values_list("email", flat=True).distinct() 78 | return [] 79 | -------------------------------------------------------------------------------- /emailtemplates/tests/test_models.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | import mock 7 | from django.core import mail 8 | from django.core.files import File 9 | from django.test import TestCase 10 | 11 | from emailtemplates.helpers import TemplateSourceLoader 12 | from emailtemplates.models import EmailTemplate, MassEmailMessage, MassEmailAttachment 13 | from emailtemplates.registry import email_templates, NotRegistered 14 | 15 | 16 | class EmailTemplateTest(TestCase): 17 | def setUp(self): 18 | self.default_content = "

    TEST DEFAULT CONTENT

    " 19 | self.subject = "Subject" 20 | self.email_template = EmailTemplate.objects.create(title="template-1.html") 21 | 22 | @mock.patch.object(TemplateSourceLoader, "get_source") 23 | def test_get_default_content(self, mock_source): 24 | mock_source.return_value = self.default_content 25 | self.assertEqual( 26 | self.email_template.get_default_content(), self.default_content 27 | ) 28 | 29 | @mock.patch.object(email_templates, "get_subject") 30 | def test_get_default_subject(self, mock_subject): 31 | mock_subject.return_value = self.subject 32 | self.assertEqual(self.email_template.get_default_subject(), self.subject) 33 | 34 | @mock.patch.object( 35 | TemplateSourceLoader, "get_source", mock.Mock(side_effect=Exception("error...")) 36 | ) 37 | def test_get_empty_default_content_if_error(self): 38 | self.assertEqual(self.email_template.get_default_content(), "") 39 | 40 | @mock.patch.object( 41 | email_templates, "get_subject", mock.Mock(side_effect=NotRegistered("error...")) 42 | ) 43 | def test_get_empty_default_subject_if_error(self): 44 | self.assertEqual(self.email_template.get_default_subject(), "") 45 | 46 | @mock.patch.object(TemplateSourceLoader, "get_source") 47 | def test_save_default_content(self, mock_source): 48 | mock_source.return_value = self.default_content 49 | email_template = EmailTemplate.objects.create(title="template-2.html") 50 | self.assertEqual(email_template.content, self.default_content) 51 | 52 | @mock.patch.object(email_templates, "get_subject") 53 | def test_save_default_subject(self, mock_subject): 54 | mock_subject.return_value = self.subject 55 | email_template = EmailTemplate.objects.create(title="template-2.html") 56 | self.assertEqual(email_template.subject, self.subject) 57 | 58 | @mock.patch.object(TemplateSourceLoader, "get_source") 59 | def test_do_not_override_existing_content(self, mock_source): 60 | mock_source.return_value = self.default_content 61 | email_template = EmailTemplate.objects.create( 62 | title="template-2.html", content="

    New content

    " 63 | ) 64 | self.assertEqual(email_template.content, "

    New content

    ") 65 | 66 | 67 | class MassEmailMessageTest(TestCase): 68 | def setUp(self): 69 | self.mass_email_message = MassEmailMessage.objects.create( 70 | subject="Temat maila", content="

    Treść emaila

    " 71 | ) 72 | self.attachment_filepath = os.path.join( 73 | os.path.dirname(__file__), "data", "example_file.txt" 74 | ) 75 | mail.outbox = [] 76 | 77 | def test_send(self): 78 | recipients = ["person@example.com"] 79 | sent = self.mass_email_message.send(recipients) 80 | self.assertTrue(sent) 81 | self.assertTrue(self.mass_email_message.sent) 82 | self.assertEqual(mail.outbox[0].to, recipients) 83 | self.assertEqual(mail.outbox[0].subject, "Temat maila") 84 | self.assertEqual(mail.outbox[0].body, "

    Treść emaila

    ") 85 | 86 | def test_send_with_attachments(self): 87 | attachment = MassEmailAttachment.objects.create( 88 | attachment_file=File( 89 | open(self.attachment_filepath, "r"), "example_file.txt" 90 | ), 91 | mass_email_message=self.mass_email_message, 92 | ) 93 | recipients = ["person@example.com"] 94 | sent = self.mass_email_message.send(recipients) 95 | self.assertTrue(sent) 96 | attachments = mail.outbox[0].attachments 97 | self.assertEqual(len(attachments), 1) 98 | self.assertTrue(attachments[0][0].startswith("example_file")) 99 | self.assertTrue(attachments[0][0].endswith(".txt")) 100 | self.assertEqual(attachments[0][1], "Some content of example file.") 101 | self.assertEqual(attachments[0][2], "text/plain") 102 | -------------------------------------------------------------------------------- /emailtemplates/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-08-09 13:50+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" 20 | "%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" 21 | "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" 22 | 23 | #: emailtemplates/admin.py:19 emailtemplates/models.py:83 24 | msgid "Attachment" 25 | msgstr "Anlage" 26 | 27 | #: emailtemplates/admin.py:20 emailtemplates/models.py:84 28 | msgid "Attachments" 29 | msgstr "Anhänge" 30 | 31 | #: emailtemplates/admin.py:50 32 | msgid "Show email preview" 33 | msgstr "E-Mail-Vorschau anzeigen" 34 | 35 | #: emailtemplates/admin.py:54 36 | msgid "Actions" 37 | msgstr "Aktionen" 38 | 39 | #: emailtemplates/apps.py:8 40 | msgid "E-MAIL TEMPLATES" 41 | msgstr "E-Mail Templates" 42 | 43 | #: emailtemplates/forms.py:20 emailtemplates/models.py:29 44 | msgid "template" 45 | msgstr "Vorlage" 46 | 47 | #: emailtemplates/models.py:27 emailtemplates/models.py:68 48 | #: emailtemplates/models.py:101 49 | msgid "ID" 50 | msgstr "" 51 | 52 | #: emailtemplates/models.py:30 emailtemplates/models.py:103 53 | msgid "subject" 54 | msgstr "Thema" 55 | 56 | #: emailtemplates/models.py:31 emailtemplates/models.py:104 57 | msgid "content" 58 | msgstr "Inhalt" 59 | 60 | #: emailtemplates/models.py:33 61 | msgid "language" 62 | msgstr "Sprache" 63 | 64 | #: emailtemplates/models.py:39 65 | msgid "attachments" 66 | msgstr "anhänge" 67 | 68 | #: emailtemplates/models.py:41 69 | msgid "created" 70 | msgstr "erstellt" 71 | 72 | #: emailtemplates/models.py:42 73 | msgid "modified" 74 | msgstr "geändert" 75 | 76 | #: emailtemplates/models.py:46 77 | msgid "Email template" 78 | msgstr "E-Mail Template" 79 | 80 | #: emailtemplates/models.py:47 81 | msgid "Email templates" 82 | msgstr "E-Mail Templates" 83 | 84 | #: emailtemplates/models.py:70 85 | msgid "name" 86 | msgstr "Name" 87 | 88 | #: emailtemplates/models.py:72 89 | msgid "Attachment file" 90 | msgstr "Datei anhängen" 91 | 92 | #: emailtemplates/models.py:75 93 | msgid "Comment" 94 | msgstr "Kommentar" 95 | 96 | #: emailtemplates/models.py:75 97 | msgid "visible only in admin" 98 | msgstr "nur im Administrationsbereich sichtbar" 99 | 100 | #: emailtemplates/models.py:77 101 | msgid "Ordering" 102 | msgstr "Bestellung" 103 | 104 | #: emailtemplates/models.py:78 105 | msgid "Send as link" 106 | msgstr "Als Link versenden" 107 | 108 | #: emailtemplates/models.py:87 109 | #, python-format 110 | msgid "Attachment: %s" 111 | msgstr "Anlage: %s" 112 | 113 | #: emailtemplates/models.py:105 114 | msgid "sent" 115 | msgstr "gesendet" 116 | 117 | #: emailtemplates/models.py:108 118 | msgid "Mass email message" 119 | msgstr "Massen E-Mail" 120 | 121 | #: emailtemplates/models.py:109 122 | msgid "Mass email messages" 123 | msgstr "Massen E-Mails" 124 | 125 | #: emailtemplates/registry.py:80 126 | #, python-format 127 | msgid "USAGE: %s" 128 | msgstr "ANWENDUNG: %s" 129 | 130 | #: emailtemplates/registry.py:83 131 | #, python-format 132 | msgid "CONTEXT:
    %s" 133 | msgstr "KONTEXT:
    %s" 134 | 135 | #: emailtemplates/templates/admin/emailtemplates/massemailmessage/change_form.html:5 136 | msgid "Send to all users" 137 | msgstr "An alle Benutzer senden" 138 | 139 | #: emailtemplates/tests/test_template_registry.py:60 140 | msgid "USAGE" 141 | msgstr "ANWENDUNG" 142 | 143 | #: emailtemplates/tests/test_template_registry.py:61 144 | msgid "CONTEXT" 145 | msgstr "KONTEXT" 146 | 147 | #: emailtemplates/views.py:55 148 | msgid "" 149 | "Mass email was already sent. Create new mail message or force sending from " 150 | "shell." 151 | msgstr "" 152 | "Die Massen-E-Mail wurde bereits gesendet. Neue E-Mail-Nachricht erstellen " 153 | "oder Senden aus der Shell erzwingen" 154 | 155 | #: emailtemplates/views.py:62 156 | msgid "Mass email sent successfully" 157 | msgstr "Massen-E-Mail erfolgreich gesendet" 158 | 159 | #: emailtemplates/views.py:65 160 | msgid "Error occurred when trying to send mass email message." 161 | msgstr "" 162 | "Beim Versuch, eine Massen-E-Mail-Nachricht zu senden, ist ein Fehler " 163 | "aufgetreten" 164 | 165 | #~ msgid "email template" 166 | #~ msgstr "E-Mail-Vorlage" 167 | -------------------------------------------------------------------------------- /emailtemplates/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-10-26 12:24+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && " 20 | "(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && " 21 | "n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" 22 | 23 | #: emailtemplates/admin.py:19 emailtemplates/models.py:98 24 | msgid "Attachment" 25 | msgstr "Załącznik" 26 | 27 | #: emailtemplates/admin.py:20 emailtemplates/models.py:99 28 | msgid "Attachments" 29 | msgstr "Załączniki" 30 | 31 | #: emailtemplates/admin.py:50 32 | msgid "Show email preview" 33 | msgstr "Pokaż podgląd wiadomości" 34 | 35 | #: emailtemplates/admin.py:54 36 | msgid "Actions" 37 | msgstr "Akcje" 38 | 39 | #: emailtemplates/apps.py:8 40 | msgid "E-MAIL TEMPLATES" 41 | msgstr "Szblony wiadomości E-mail" 42 | 43 | #: emailtemplates/forms.py:20 emailtemplates/models.py:31 44 | msgid "template" 45 | msgstr "szablon" 46 | 47 | #: emailtemplates/models.py:29 emailtemplates/models.py:83 48 | #: emailtemplates/models.py:116 49 | msgid "ID" 50 | msgstr "" 51 | 52 | #: emailtemplates/models.py:33 emailtemplates/models.py:118 53 | msgid "subject" 54 | msgstr "tytuł" 55 | 56 | #: emailtemplates/models.py:36 57 | msgid "you can use variables from table" 58 | msgstr "możesz użyć zmiennych kontekstowych z tabeli" 59 | 60 | #: emailtemplates/models.py:38 emailtemplates/models.py:119 61 | msgid "content" 62 | msgstr "zawartość" 63 | 64 | #: emailtemplates/models.py:40 65 | msgid "language" 66 | msgstr "język" 67 | 68 | #: emailtemplates/models.py:45 69 | #| msgid "Ordering" 70 | msgid "ordering" 71 | msgstr "kolejność" 72 | 73 | #: emailtemplates/models.py:47 74 | msgid "attachments" 75 | msgstr "załączniki" 76 | 77 | #: emailtemplates/models.py:49 78 | msgid "created" 79 | msgstr "utworzono" 80 | 81 | #: emailtemplates/models.py:50 82 | msgid "modified" 83 | msgstr "zmodyfikowano" 84 | 85 | #: emailtemplates/models.py:54 86 | msgid "Email template" 87 | msgstr "Szablon wiadomości" 88 | 89 | #: emailtemplates/models.py:55 90 | msgid "Email templates" 91 | msgstr "Szablony wiadomości" 92 | 93 | #: emailtemplates/models.py:85 94 | msgid "name" 95 | msgstr "nazwa" 96 | 97 | #: emailtemplates/models.py:87 98 | msgid "Attachment file" 99 | msgstr "Plik załącznika" 100 | 101 | #: emailtemplates/models.py:90 102 | msgid "Comment" 103 | msgstr "Komentarz" 104 | 105 | #: emailtemplates/models.py:90 106 | msgid "visible only in admin" 107 | msgstr "widoczne tylko w panelu administracyjnym" 108 | 109 | #: emailtemplates/models.py:92 110 | msgid "Ordering" 111 | msgstr "Kolejność" 112 | 113 | #: emailtemplates/models.py:93 114 | msgid "Send as link" 115 | msgstr "Wyślij jako link" 116 | 117 | #: emailtemplates/models.py:102 118 | #, python-format 119 | msgid "Attachment: %s" 120 | msgstr "Załącznik: %s" 121 | 122 | #: emailtemplates/models.py:120 123 | msgid "sent" 124 | msgstr "wysłano" 125 | 126 | #: emailtemplates/models.py:123 127 | msgid "Mass email message" 128 | msgstr "Wiadomość grupowa" 129 | 130 | #: emailtemplates/models.py:124 131 | msgid "Mass email messages" 132 | msgstr "Wiadomości grupowe" 133 | 134 | #: emailtemplates/registry.py:81 135 | #, python-format 136 | msgid "USAGE: %s" 137 | msgstr "UŻYCIE: %s" 138 | 139 | #: emailtemplates/registry.py:84 140 | #, python-format 141 | msgid "CONTEXT:
    %s" 142 | msgstr "KONTEKST:
    %s" 143 | 144 | #: emailtemplates/templates/admin/emailtemplates/massemailmessage/change_form.html:5 145 | msgid "Send to all users" 146 | msgstr "Wyślij do wszystkich użytkowników" 147 | 148 | #: emailtemplates/tests/test_template_registry.py:60 149 | msgid "USAGE" 150 | msgstr "UŻYCIE" 151 | 152 | #: emailtemplates/tests/test_template_registry.py:61 153 | msgid "CONTEXT" 154 | msgstr "KONTEKST" 155 | 156 | #: emailtemplates/views.py:55 157 | msgid "" 158 | "Mass email was already sent. Create new mail message or force sending from " 159 | "shell." 160 | msgstr "" 161 | "Wiadomość grupowa została już wysłana. Utwórz nową lub wymuś wysłanie z " 162 | "konsoli shell." 163 | 164 | #: emailtemplates/views.py:62 165 | msgid "Mass email sent successfully" 166 | msgstr "Wiadomość grupowa wysłana pomyślnie" 167 | 168 | #: emailtemplates/views.py:65 169 | msgid "Error occurred when trying to send mass email message." 170 | msgstr "Wystąpił błąd podczas próby wysłania wiadomości grupowej." 171 | 172 | #~ msgid "email template" 173 | #~ msgstr "szablon email" 174 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-emailtemplates 2 | ********************* 3 | 4 | About 5 | ===== 6 | 7 | Django app that enables developers to create default templates for emails 8 | and Site Admins to easily override the templates via Admin Panel. 9 | 10 | Assumptions 11 | =========== 12 | 13 | * Site Admins should know context for each template. 14 | * Site Admins should be familiar with Django Template System. 15 | 16 | Changelog 17 | ========= 18 | 19 | 1.1.17 20 | ------ 21 | * Add support for django 4 - https://github.com/deployed/django-emailtemplates/pull/39 22 | 23 | 1.1.16 24 | ------ 25 | * change max_length from 100 to 255 in email attachments - https://github.com/deployed/django-emailtemplates/pull/38 26 | 27 | 1.1.15 28 | ------ 29 | * ordering in email template & default subject from registry - https://github.com/deployed/django-emailtemplates/pull/37 30 | 31 | 1.1.14 32 | ------ 33 | * Additional fields for email attachment - https://github.com/deployed/django-emailtemplates/pull/36 34 | 35 | 1.1.13 36 | ------ 37 | * Change default auto field to BigAutoField - https://github.com/deployed/django-emailtemplates/pull/35 38 | 39 | 1.1.12 40 | ------- 41 | * 23614 german translations - https://github.com/deployed/django-emailtemplates/pull/34 42 | 43 | 1.1.11 44 | ------- 45 | * subject improvement - Now it is possible to use django templates as email subject 46 | 47 | 1.1.10 48 | ------- 49 | * Fixed problem with duplicated kwargs in send method 50 | 51 | 1.1.9 52 | ----- 53 | * absolute url of the attached files 54 | * use attachment filename in the email context 55 | 56 | 1.1.8 57 | ----- 58 | * send related attachments together with email templates 59 | 60 | 1.1.7 61 | ----- 62 | * added missing translations [pl] 63 | * added support for naming of the templates 64 | 65 | 1.1.6 66 | ----- 67 | * Template loader fix 68 | * Added missing migration - fixed language choices 69 | 70 | 1.1.5 71 | ----- 72 | * Add default_app_config 73 | 74 | 1.1.4 75 | ----- 76 | * Add verbose name, replace __unicode__ into __str__ 77 | 78 | 1.1.3 79 | ----- 80 | * Adding support for DEFAULT_REPLY_TO_EMAIL in django settings. 81 | 82 | 1.1.2 83 | ----- 84 | 85 | * EmailFromTemplate.send_email - added new param: fail_silently 86 | - When it’s False, msg.send() will raise an smtplib.SMTPException if an error occurs. 87 | 88 | 1.1.1 89 | ----- 90 | 91 | * Fix confusing logger stating that email was sent even though an error had occured 92 | * cosmetic changes - logging messages possible to be aggregated by tools like sentry 93 | 94 | 95 | 1.1.0 96 | ----- 97 | 98 | * Basic mass mailing feature. Just go to admin, create new MassEmailMessage object and fill its subject, HTML content and attachments. 99 | Click admin button to send it or use Django shell. Emails are be default sent to all active users of user model (it must have is_active and email fields). 100 | In case of many application users sending emails using admin button may require to implement sending from queue rather than synchronously. 101 | You can create custom recipients function returning list and specify reference to it in MASS_EMAIL_RECIPIENTS setting. 102 | * `EmailFromTemplate.send()` now receives `attachments_paths` parameter of paths that can be used by `EmailMessage.attach_file()` django core method. 103 | 104 | 1.0.4 105 | ----- 106 | 107 | * Django 1.11 compatibility fix 108 | 109 | 1.0.3 110 | ----- 111 | 112 | * Django 1.11 compatibility 113 | 114 | 1.0.2 115 | ----- 116 | 117 | * `help_context` parameter of `EmailRegistry.register()` may now contain tuple of description and example value shown in preview 118 | * Changed EmailTemplateAdminForm title to use ChoiceField choices as lazy function. This way all registered templates are printed in admin form, independent of order Python loads application modules. 119 | 120 | 1.0.1 121 | ----- 122 | 123 | * better admin panel 124 | * show email preview action 125 | * set default email content from related template 126 | 127 | 1.0.0 128 | ----- 129 | 130 | * This version introduced **backward incompatible** EmailTemplateRegistry. 131 | * All EmailTemplates must be registered using email_templates.register(path). Not registered email templates will raise NotRegistered exception. Registry validation can be avoid by creating email template with flag registry_validation set to False. 132 | * Removed prefix from EmailFromTemplate. All templates must be located in {{templates}}/emailtemplates. 133 | 134 | 0.8.7.3 135 | ------- 136 | 137 | * Set default email title if is not defined in the database. 138 | 139 | 0.8.7.1 140 | ------- 141 | 142 | * Added missing migration 143 | 144 | 0.8.7 145 | ----- 146 | 147 | * Check syntax errors in EmailTemplate's content (admin form) 148 | 149 | 0.8.6.2 150 | ------- 151 | 152 | * Added missing migrations 153 | 154 | 0.8.6.1 155 | ------- 156 | 157 | * Migrations dir fix 158 | 159 | 0.8.6 160 | ----- 161 | 162 | * Compatibility with Django 1.10 163 | 164 | 0.8.5 165 | ----- 166 | 167 | * Fixed template loader error - added default Engine 168 | 169 | 0.8.4 170 | ----- 171 | 172 | * Django 1.8.8 required 173 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0002_auto_20170428_1442.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-28 14:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("emailtemplates", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="emailtemplate", 17 | name="title", 18 | field=models.CharField( 19 | choices=[ 20 | (b"accounts/activation.html", b"accounts/activation.html"), 21 | ( 22 | b"project/introduction_project_mail_1d.html", 23 | b"project/introduction_project_mail_1d.html", 24 | ), 25 | ( 26 | b"project/notice_original_deletion_3days.html", 27 | b"project/notice_original_deletion_3days.html", 28 | ), 29 | (b"supports/feedback_email.html", b"supports/feedback_email.html"), 30 | ( 31 | b"accounts/email_verification.html", 32 | b"accounts/email_verification.html", 33 | ), 34 | (b"share/invitation.html", b"share/invitation.html"), 35 | ( 36 | b"orders/cancelled_payment.html", 37 | b"orders/cancelled_payment.html", 38 | ), 39 | ( 40 | b"project/new_template_available.html", 41 | b"project/new_template_available.html", 42 | ), 43 | ( 44 | b"invitations/invite_friend.html", 45 | b"invitations/invite_friend.html", 46 | ), 47 | ( 48 | b"project/introduction_project_mail_2d.html", 49 | b"project/introduction_project_mail_2d.html", 50 | ), 51 | ( 52 | b"share/invitation_subject.html", 53 | b"share/invitation_subject.html", 54 | ), 55 | (b"orders/delivered.html", b"orders/delivered.html"), 56 | ( 57 | b"subscriptions/subscription_email.html", 58 | b"subscriptions/subscription_email.html", 59 | ), 60 | ( 61 | b"project/notice_original_deletion_1week.html", 62 | b"project/notice_original_deletion_1week.html", 63 | ), 64 | (b"accounts/welcome.html", b"accounts/welcome.html"), 65 | (b"project/download_zip.html", b"project/download_zip.html"), 66 | (b"orders/in_production.html", b"orders/in_production.html"), 67 | ( 68 | b"project/notice_original_deletion_1month.html", 69 | b"project/notice_original_deletion_1month.html", 70 | ), 71 | ( 72 | b"accounts/introducing_email_4d.html", 73 | b"accounts/introducing_email_4d.html", 74 | ), 75 | ( 76 | b"orders/reorder_incentive_mail.html", 77 | b"orders/reorder_incentive_mail.html", 78 | ), 79 | ( 80 | b"project/project_action_like_comment_notification.html", 81 | b"project/project_action_like_comment_notification.html", 82 | ), 83 | ( 84 | b"accounts/introducing_email_2d.html", 85 | b"accounts/introducing_email_2d.html", 86 | ), 87 | ( 88 | b"accounts/password_reset_email.html", 89 | b"accounts/password_reset_email.html", 90 | ), 91 | (b"project/package_expired.html", b"project/package_expired.html"), 92 | (b"project/package_upgrade.html", b"project/package_upgrade.html"), 93 | ( 94 | b"project/introduction_project_mail_7d.html", 95 | b"project/introduction_project_mail_7d.html", 96 | ), 97 | ( 98 | b"project/project_action_notification.html", 99 | b"project/project_action_notification.html", 100 | ), 101 | ( 102 | b"accounts/introducing_email_3d.html", 103 | b"accounts/introducing_email_3d.html", 104 | ), 105 | ( 106 | b"accounts/introducing_email_1d.html", 107 | b"accounts/introducing_email_1d.html", 108 | ), 109 | ( 110 | b"supports/support_request.html", 111 | b"supports/support_request.html", 112 | ), 113 | ( 114 | b"supports/support_confirm.html", 115 | b"supports/support_confirm.html", 116 | ), 117 | ], 118 | max_length=255, 119 | verbose_name="template", 120 | ), 121 | ), 122 | ] 123 | -------------------------------------------------------------------------------- /emailtemplates/models.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import os 4 | 5 | from django.conf import settings 6 | from django.db import models 7 | from django.utils import translation 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from emailtemplates.helpers import TemplateSourceLoader, mass_mailing_recipients 11 | from emailtemplates.registry import email_templates, NotRegistered 12 | 13 | try: 14 | from django.utils.timezone import now 15 | except ImportError: 16 | from datetime import datetime 17 | 18 | now = datetime.now 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class EmailTemplate(models.Model): 24 | """ 25 | Model to store email template. 26 | """ 27 | 28 | id = models.BigAutoField( 29 | auto_created=True, primary_key=True, serialize=False, verbose_name=_("ID") 30 | ) 31 | title = models.CharField(_("template"), max_length=255) 32 | subject = models.CharField( 33 | _("subject"), 34 | max_length=255, 35 | blank=True, 36 | help_text=_("you can use variables from table"), 37 | ) 38 | content = models.TextField(_("content")) 39 | language = models.CharField( 40 | _("language"), 41 | max_length=10, 42 | choices=settings.LANGUAGES, 43 | default=settings.LANGUAGE_CODE, 44 | ) 45 | ordering = models.PositiveIntegerField(verbose_name=_("ordering"), default=1) 46 | attachments = models.ManyToManyField( 47 | "EmailAttachment", blank=True, verbose_name=_("attachments") 48 | ) 49 | created = models.DateTimeField(default=now, verbose_name=_("created")) 50 | modified = models.DateTimeField(default=now, verbose_name=_("modified")) 51 | 52 | class Meta: 53 | unique_together = (("title", "language"),) 54 | verbose_name = _("Email template") 55 | verbose_name_plural = _("Email templates") 56 | ordering = ("ordering",) 57 | 58 | def __str__(self): 59 | return "%s -> %s" % (self.title, self.language) 60 | 61 | def get_default_content(self): 62 | loader = TemplateSourceLoader() 63 | try: 64 | return loader.get_source(self.title) 65 | except Exception as e: 66 | logger.error("Error loading template %s. Details: %s ", self.title, e) 67 | return "" 68 | 69 | def get_default_subject(self): 70 | translation.activate(self.language) 71 | try: 72 | return email_templates.get_subject(self.title) 73 | except NotRegistered: 74 | return "" 75 | 76 | def save(self, *args, **kwargs): 77 | if not self.content: 78 | self.content = self.get_default_content() 79 | if not self.subject: 80 | self.subject = self.get_default_subject() 81 | super(EmailTemplate, self).save(*args, **kwargs) 82 | 83 | 84 | class BaseEmailAttachment(models.Model): 85 | id = models.BigAutoField( 86 | auto_created=True, primary_key=True, serialize=False, verbose_name=_("ID") 87 | ) 88 | name = models.CharField(_("name"), blank=True, max_length=50) 89 | attachment_file = models.FileField( 90 | _("Attachment file"), upload_to="emails/attachments/", max_length=255 91 | ) 92 | comment = models.TextField( 93 | verbose_name=_("Comment"), blank=True, help_text=_("visible only in admin") 94 | ) 95 | ordering = models.PositiveIntegerField(verbose_name=_("Ordering"), default=0) 96 | send_as_link = models.BooleanField(verbose_name=_("Send as link"), default=True) 97 | 98 | class Meta: 99 | abstract = True 100 | ordering = ["ordering"] 101 | verbose_name = _("Attachment") 102 | verbose_name_plural = _("Attachments") 103 | 104 | def __str__(self): 105 | return _("Attachment: %s") % self.get_name() 106 | 107 | def get_name(self): 108 | return self.name or os.path.basename(self.attachment_file.name) 109 | 110 | 111 | class EmailAttachment(BaseEmailAttachment): 112 | pass 113 | 114 | # email_template = models.ForeignKey(EmailTemplate, verbose_name=_('email template'), on_delete=models.CASCADE) 115 | 116 | 117 | class MassEmailMessage(models.Model): 118 | id = models.BigAutoField( 119 | auto_created=True, primary_key=True, serialize=False, verbose_name=_("ID") 120 | ) 121 | subject = models.CharField(_("subject"), max_length=255) 122 | content = models.TextField(_("content")) 123 | date_sent = models.DateTimeField(_("sent"), null=True, blank=True) 124 | 125 | class Meta: 126 | verbose_name = _("Mass email message") 127 | verbose_name_plural = _("Mass email messages") 128 | 129 | def __str__(self): 130 | return self.subject 131 | 132 | @property 133 | def sent(self): 134 | return bool(self.date_sent) 135 | 136 | def send(self, recipients=None, force=False): 137 | from emailtemplates.email import EmailFromTemplate 138 | 139 | recipients = recipients or mass_mailing_recipients() 140 | if self.sent and not force: 141 | return False 142 | eft = EmailFromTemplate( 143 | name="emailtemplates/mass_email.html", 144 | subject=self.subject, 145 | template_object=self, 146 | registry_validation=False, 147 | ) 148 | attachment_paths = [ 149 | attachment.attachment_file.path for attachment in self.attachments.all() 150 | ] 151 | sent_count = 0 152 | for recipient in recipients: 153 | sent = eft.send(to=[recipient], attachment_paths=attachment_paths) 154 | if sent: 155 | sent_count += 1 156 | logger.info( 157 | "Successfully sent mass email message to user %s", recipient 158 | ) 159 | else: 160 | logger.warning("Error sending mass email message to user %s", recipient) 161 | self.date_sent = now() 162 | self.save() 163 | return sent_count == len(recipients) 164 | 165 | 166 | class MassEmailAttachment(BaseEmailAttachment): 167 | mass_email_message = models.ForeignKey( 168 | MassEmailMessage, related_name="attachments", on_delete=models.CASCADE 169 | ) 170 | -------------------------------------------------------------------------------- /emailtemplates/registry.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from django.template.loader import render_to_string 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class AlreadyRegistered(Exception): 11 | pass 12 | 13 | 14 | class NotRegistered(Exception): 15 | pass 16 | 17 | 18 | class HelpContext(object): 19 | """ 20 | Provides helpers methods for displaying help context keys (descriptions) and values (examples). 21 | """ 22 | 23 | def __init__(self, help_context): 24 | self.help_context = help_context or {} 25 | 26 | def get_help_keys(self): 27 | """ 28 | Returns dict of help_context keys (description texts used in `EmailRegistry.register()` method). 29 | """ 30 | help_keys = {} 31 | for k, v in self.help_context.items(): 32 | if isinstance(v, tuple): 33 | help_keys[k] = v[0] 34 | else: 35 | help_keys[k] = v 36 | return help_keys 37 | 38 | def get_help_values(self): 39 | """ 40 | Returns dict of help_context values (example values submitted in `EmailRegistry.register()` method). 41 | """ 42 | help_values = {} 43 | for k, v in self.help_context.items(): 44 | if isinstance(v, tuple) and len(v) == 2: 45 | help_values[k] = v[1] 46 | else: 47 | help_values[k] = "<%s>" % k 48 | return help_values 49 | 50 | 51 | class RegistrationItem(object): 52 | def __init__(self, path, help_text="", help_context=None, name="", subject=""): 53 | self.name = name or path 54 | self.path = path 55 | self.help_text = help_text 56 | self.subject = subject 57 | self.help_context_obj = HelpContext(help_context) 58 | 59 | @property 60 | def help_context(self): 61 | return self.help_context_obj.get_help_keys() 62 | 63 | def _context_key(self, key): 64 | return "{{ %s }}" % key 65 | 66 | def context_description(self): 67 | help_text_item = ( 68 | lambda k, v: "%s - %s" % (self._context_key(k), v) 69 | if v 70 | else "%s" % self._context_key(k) 71 | ) 72 | return "
    ".join( 73 | [ 74 | help_text_item(k, v) 75 | for (k, v) in sorted(self.help_context_obj.get_help_keys().items()) 76 | ] 77 | ) 78 | 79 | def as_form_help_text(self): 80 | item_help_text = ( 81 | _("USAGE: %s") % self.help_text if self.help_text else "" 82 | ) 83 | item_help_context = ( 84 | _("CONTEXT:
    %s") % self.context_description() 85 | if self.help_context_obj.get_help_keys() 86 | else "" 87 | ) 88 | return "
    ".join((item_help_text, item_help_context)) 89 | 90 | def as_form_choice(self): 91 | return self.path, self.name 92 | 93 | def get_help_content(self): 94 | return self.help_context_obj.get_help_values() 95 | 96 | 97 | class EmailTemplateRegistry(object): 98 | def __init__(self): 99 | self._registry = {} 100 | 101 | def register(self, path, name="", help_text=None, help_context=None, subject=""): 102 | """ 103 | Registers email template. 104 | 105 | Example usage: 106 | email_templates.register('hello_template.html', help_text='Hello template', 107 | help_context={'username': 'Name of user in hello expression'}) 108 | 109 | :param name: Template name [optional] 110 | :param path: Template file path. It will become immutable registry lookup key. 111 | :param help_text: Help text to describe template in admin site 112 | :param help_context: Dictionary of possible keys used in the context and description of their content 113 | :param subject: Default subject of email [optional] 114 | 115 | `help_context` items values may be strings or tuples of two strings. If strings, then email template preview 116 | will use variable names to fill context, otherwise the second tuple element will become example value. 117 | 118 | If an email template is already registered, this will raise AlreadyRegistered. 119 | """ 120 | if path in self._registry: 121 | raise AlreadyRegistered("The template %s is already registered" % path) 122 | self._registry[path] = RegistrationItem( 123 | path, help_text, help_context, name=name, subject=subject 124 | ) 125 | logger.debug("Registered email template %s", path) 126 | 127 | def is_registered(self, path): 128 | return path in self._registry 129 | 130 | def get_registration(self, path): 131 | """ 132 | Returns registration item for specified path. 133 | 134 | If an email template is not registered, this will raise NotRegistered. 135 | """ 136 | if not self.is_registered(path): 137 | raise NotRegistered("Email template not registered") 138 | return self._registry[path] 139 | 140 | def get_name(self, path): 141 | return self.get_registration(path).name 142 | 143 | def get_help_text(self, path): 144 | return self.get_registration(path).help_text 145 | 146 | def get_help_context(self, path): 147 | return self.get_registration(path).help_context 148 | 149 | def get_help_content(self, path): 150 | return self.get_registration(path).get_help_content() 151 | 152 | def get_subject(self, path): 153 | return self.get_registration(path).subject 154 | 155 | def registration_items(self): 156 | return self._registry.values() 157 | 158 | def email_template_choices(self): 159 | """ 160 | Returns list of choices that can be used in email template form field choices. 161 | """ 162 | return [item.as_form_choice() for item in self.registration_items()] 163 | 164 | def get_form_help_text(self, path): 165 | """ 166 | Returns text that can be used as form help text for creating email templates. 167 | """ 168 | try: 169 | form_help_text = render_to_string( 170 | "admin/emailtemplates/_helptext.html", 171 | context={"registration_item": self.get_registration(path)}, 172 | ) 173 | except NotRegistered: 174 | form_help_text = "" 175 | return form_help_text 176 | 177 | 178 | # Global object for singleton registry of email templates 179 | email_templates = EmailTemplateRegistry() 180 | -------------------------------------------------------------------------------- /emailtemplates/tests/test_template_registry.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from django.test import TestCase 3 | from django.utils.translation import gettext as _ 4 | 5 | from ..registry import EmailTemplateRegistry, RegistrationItem, HelpContext 6 | 7 | 8 | class HelpContextTest(TestCase): 9 | def test_get_help_keys(self): 10 | help_context = HelpContext( 11 | { 12 | "username": ("Name of user in hello expression", "superman_90"), 13 | "full_name": ("Full user name", "John Smith"), 14 | "property": "Some other property", 15 | } 16 | ) 17 | self.assertDictEqual( 18 | help_context.get_help_keys(), 19 | { 20 | "username": "Name of user in hello expression", 21 | "full_name": "Full user name", 22 | "property": "Some other property", 23 | }, 24 | ) 25 | 26 | def test_get_help_values(self): 27 | help_context = HelpContext( 28 | { 29 | "username": ("Name of user in hello expression", "superman_90"), 30 | "full_name": ("Full user name", "John Smith"), 31 | "property": "Some other property", 32 | } 33 | ) 34 | self.assertDictEqual( 35 | help_context.get_help_values(), 36 | { 37 | "username": "superman_90", 38 | "full_name": "John Smith", 39 | "property": "", 40 | }, 41 | ) 42 | 43 | 44 | class RegistrationItemTest(TestCase): 45 | def test_context_description(self): 46 | item = RegistrationItem( 47 | "hello_template.html", 48 | help_text="Hello template", 49 | help_context={"username": "Name of user in hello expression"}, 50 | ) 51 | self.assertIn("{{ username }}", item.context_description()) 52 | 53 | def test_as_form_help_text(self): 54 | item = RegistrationItem( 55 | "hello_template.html", 56 | help_text="Hello template", 57 | help_context={"username": "Name of user in hello expression"}, 58 | ) 59 | self.assertEqual(str, type(item.as_form_help_text())) 60 | self.assertIn(_("USAGE"), item.as_form_help_text()) 61 | self.assertIn(_("CONTEXT"), item.as_form_help_text()) 62 | 63 | def test_as_form_choice(self): 64 | item = RegistrationItem( 65 | "hello_template.html", 66 | help_text="Hello template", 67 | help_context={"username": "Name of user in hello expression"}, 68 | ) 69 | self.assertEqual(tuple, type(item.as_form_choice())) 70 | 71 | def test_safe_defaults(self): 72 | item = RegistrationItem("hello_template.html") 73 | self.assertEqual(str, type(item.help_text)) 74 | self.assertEqual(dict, type(item.help_context)) 75 | self.assertEqual(tuple, type(item.as_form_choice())) 76 | 77 | 78 | class EmailTemplateRegistryTest(TestCase): 79 | def test_is_registered(self): 80 | registry = EmailTemplateRegistry() 81 | registry.register("hello_template.html") 82 | self.assertTrue(registry.is_registered("hello_template.html")) 83 | 84 | def test_get_subject(self): 85 | template_registry = EmailTemplateRegistry() 86 | template_registry.register("hello_template.html", subject="subject") 87 | self.assertEqual( 88 | template_registry.get_subject("hello_template.html"), "subject" 89 | ) 90 | 91 | def test_get_help_text(self): 92 | template_registry = EmailTemplateRegistry() 93 | template_registry.register( 94 | "hello_template.html", 95 | help_text="Hello template", 96 | help_context={"username": "Name of user in hello expression"}, 97 | ) 98 | help_text = template_registry.get_help_text("hello_template.html") 99 | self.assertEqual(help_text, "Hello template") 100 | 101 | def test_get_help_context(self): 102 | template_registry = EmailTemplateRegistry() 103 | template_registry.register( 104 | "hello_template.html", 105 | help_text="Hello template", 106 | help_context={"username": "Name of user in hello expression"}, 107 | ) 108 | help_context = template_registry.get_help_context("hello_template.html") 109 | self.assertIn("username", help_context) 110 | 111 | def test_get_help_content(self): 112 | template_registry = EmailTemplateRegistry() 113 | template_registry.register( 114 | "hello_template.html", 115 | help_text="Hello template", 116 | help_context={ 117 | "username": ("Name of user in hello expression", "superman_90"), 118 | "full_name": ("Full user name", "John Smith"), 119 | "property": "Some other property", 120 | }, 121 | ) 122 | help_content = template_registry.get_help_content("hello_template.html") 123 | self.assertDictEqual( 124 | help_content, 125 | { 126 | "username": "superman_90", 127 | "full_name": "John Smith", 128 | "property": "", 129 | }, 130 | ) 131 | 132 | def test_get_email_templates(self): 133 | template_registry = EmailTemplateRegistry() 134 | template_registry.register( 135 | "hello_template.html", 136 | help_text="Hello template", 137 | help_context={"username": "Name of user in hello expression"}, 138 | ) 139 | template_registry.register("simple_template.html", help_text="Simple template") 140 | self.assertEqual(2, len(template_registry.email_template_choices())) 141 | 142 | def test_email_template_choices(self): 143 | template_registry = EmailTemplateRegistry() 144 | template_registry.register( 145 | "hello_template.html", 146 | help_text="Hello template", 147 | help_context={"username": "Name of user in hello expression"}, 148 | ) 149 | self.assertEqual(1, len(template_registry.email_template_choices())) 150 | template, _ = template_registry.email_template_choices()[0] 151 | self.assertEqual("hello_template.html", template) 152 | 153 | def test_registration_items(self): 154 | template_registry = EmailTemplateRegistry() 155 | template_registry.register( 156 | "hello_template.html", 157 | help_text="Hello template", 158 | help_context={"username": "Name of user in hello expression"}, 159 | ) 160 | items = list(template_registry.registration_items()) 161 | self.assertEqual(1, len(items)) 162 | self.assertEqual("hello_template.html", items[0].path) 163 | -------------------------------------------------------------------------------- /emailtemplates/tests/test_email.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | 4 | import mock 5 | from django.conf import settings 6 | from django.core import mail 7 | from django.test import TestCase, override_settings 8 | from django.utils.html import escape 9 | from mock import Mock 10 | 11 | from ..email import EmailFromTemplate 12 | from ..email import logger as email_logger 13 | from ..models import EmailTemplate, EmailAttachment 14 | from ..registry import email_templates, NotRegistered, EmailTemplateRegistry 15 | 16 | 17 | class CheckEmail(TestCase): 18 | def check_email_was_sent(self, eft, to, reply_to=None): 19 | if reply_to is None: 20 | reply_to = [] 21 | 22 | self.assertTrue(len(mail.outbox) > 0) 23 | msg = mail.outbox[0] 24 | self.assertTrue(settings.DEFAULT_FROM_EMAIL in msg.from_email) 25 | self.assertEqual(msg.content_subtype, "html") 26 | self.assertEqual(msg.subject, eft.subject) 27 | self.assertEqual(msg.body, eft.message) 28 | self.assertTrue("Message-Id" in msg.message()) 29 | self.assertEqual(msg.to, to) 30 | self.assertEqual(msg.reply_to, reply_to) 31 | 32 | 33 | class EmailFromTemplateTest(CheckEmail): 34 | def setUp(self): 35 | mail.outbox = [] 36 | email_templates = EmailTemplateRegistry() 37 | self.attachment_filepath = os.path.join( 38 | os.path.dirname(__file__), "data", "example_file.txt" 39 | ) 40 | 41 | @override_settings(DEFAULT_REPLY_TO_EMAIL="hello@hello.pl") 42 | def test_empty_object(self): 43 | eft = EmailFromTemplate(registry_validation=False) 44 | self.assertTrue(isinstance(eft, object)) 45 | eft.render_message() 46 | to = ["to@example.com"] 47 | eft.send_email(to) 48 | self.check_email_was_sent(eft, to, reply_to=["hello@hello.pl"]) 49 | 50 | @override_settings(DEFAULT_REPLY_TO_EMAIL=None) 51 | def test_empty_object_with_empty_reply_to(self): 52 | eft = EmailFromTemplate(registry_validation=False) 53 | self.assertTrue(isinstance(eft, object)) 54 | eft.render_message() 55 | to = ["to@example.com"] 56 | eft.send_email(to) 57 | self.check_email_was_sent(eft, to) 58 | 59 | @mock.patch("emailtemplates.email.logger") 60 | def test_with_empty_db_object(self, mock_logger): 61 | eft = EmailFromTemplate(registry_validation=False, name="template.html") 62 | eft.get_object() 63 | mock_logger.warning.assert_called_with( 64 | "Can't find %s template in the filesystem, will use very default one.", 65 | "template.html", 66 | ) 67 | eft.render_message() 68 | to = ["to@example.com"] 69 | eft.send_email(to) 70 | self.check_email_was_sent(eft, to) 71 | 72 | def test_get_default_attachments_ordering(self): 73 | email_template = EmailTemplate.objects.create(title="template.html") 74 | attachment1 = EmailAttachment.objects.create( 75 | name="file1", 76 | attachment_file="test/file1.pdf", 77 | ordering=10, 78 | send_as_link=True, 79 | ) 80 | attachment2 = EmailAttachment.objects.create( 81 | name="file2", 82 | attachment_file="test/file2.pdf", 83 | ordering=1, 84 | send_as_link=True, 85 | ) 86 | email_template.attachments.add(attachment1, attachment2) 87 | eft = EmailFromTemplate(registry_validation=False, name="template.html") 88 | result = eft.get_default_attachments(as_links=True) 89 | self.assertEqual(result[0][0], attachment2.name) 90 | self.assertEqual(result[1][0], attachment1.name) 91 | 92 | def test_init_check_email_templates_registry(self): 93 | with self.assertRaises(NotRegistered): 94 | email_template = EmailFromTemplate("some_template.html") 95 | email_templates.register("some_template.html") 96 | email_template = EmailFromTemplate("some_template.html") 97 | self.assertTrue(email_templates.is_registered("some_template.html")) 98 | 99 | def test_send_attachment_paths(self): 100 | eft = EmailFromTemplate(registry_validation=False) 101 | to = ["to@example.com"] 102 | eft.send(to, attachment_paths=[self.attachment_filepath]) 103 | self.check_email_was_sent(eft, to) 104 | self.assertEqual( 105 | mail.outbox[0].attachments, 106 | [("example_file.txt", "Some content of example file.", "text/plain")], 107 | ) 108 | 109 | 110 | class EmailFromTemplateWithFixturesTest(CheckEmail): 111 | def setUp(self): 112 | self.language = "pl" 113 | email_templates = EmailTemplateRegistry() 114 | self.support_template = EmailTemplate.objects.create( 115 | language=self.language, 116 | title="support_respond.html", 117 | subject="Hi {{ user_name }}", 118 | content="Support: {{ user_name }}", 119 | ) 120 | mail.outbox = [] 121 | email_logger.debug = Mock() 122 | 123 | def test_support_database_template(self): 124 | template_name = "support_respond.html" 125 | eft = EmailFromTemplate( 126 | name=template_name, language=self.language, registry_validation=False 127 | ) 128 | eft.context = {"user_name": "Lucas"} 129 | eft.get_object() 130 | email_logger.debug.assert_called_with( 131 | "Got template %s from database", template_name 132 | ) 133 | self.assertEqual(eft.template_source, "database") 134 | eft.render_message() 135 | to = ["tester1@example.com", "tester2@example.com"] 136 | eft.send_email(to) 137 | self.check_email_was_sent(eft, to) 138 | 139 | def test_support_database_template_without_title(self): 140 | self.support_template.subject = "" 141 | self.support_template.save(update_fields=["subject"]) 142 | eft = EmailFromTemplate( 143 | name="support_respond.html", 144 | subject="default email title - hi {{ user_name }}", 145 | language=self.language, 146 | registry_validation=False, 147 | ) 148 | eft.context = {"user_name": "Lucas"} 149 | eft.get_object() 150 | self.assertEqual(eft.subject, "default email title - hi Lucas") 151 | 152 | def test_friends_invitation_no_database_or_filesystem_template(self): 153 | eft = EmailFromTemplate(registry_validation=False) 154 | eft.context = { 155 | "user_name": "Alibaba", 156 | "personal_message": "I'd like you te be site member!", 157 | "landing_url": "http://example.com/followers/612/", 158 | } 159 | eft.template = "{{ user_name }}, {{ personal_message }} {{ landing_url }}" 160 | eft.render_message() 161 | self.assertEqual( 162 | eft.message, 163 | escape( 164 | "Alibaba, I'd like you te be site member! http://example.com/followers/612/" 165 | ), 166 | ) 167 | to = ["tester@example.com"] 168 | self.assertEqual(eft.template_source, "default") 169 | eft.send_email(to) 170 | self.check_email_was_sent(eft, to) 171 | -------------------------------------------------------------------------------- /emailtemplates/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-03-31 14:23 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="EmailTemplate", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ( 29 | "title", 30 | models.CharField( 31 | choices=[ 32 | (b"accounts/activation.html", b"accounts/activation.html"), 33 | ( 34 | b"project/introduction_project_mail_1d.html", 35 | b"project/introduction_project_mail_1d.html", 36 | ), 37 | ( 38 | b"project/notice_original_deletion_3days.html", 39 | b"project/notice_original_deletion_3days.html", 40 | ), 41 | ( 42 | b"accounts/password_reset_email.html", 43 | b"accounts/password_reset_email.html", 44 | ), 45 | ( 46 | b"accounts/email_verification.html", 47 | b"accounts/email_verification.html", 48 | ), 49 | (b"share/invitation.html", b"share/invitation.html"), 50 | ( 51 | b"orders/cancelled_payment.html", 52 | b"orders/cancelled_payment.html", 53 | ), 54 | ( 55 | b"project/new_template_available.html", 56 | b"project/new_template_available.html", 57 | ), 58 | ( 59 | b"supports/feedback_email.html", 60 | b"supports/feedback_email.html", 61 | ), 62 | ( 63 | b"invitations/invite_friend.html", 64 | b"invitations/invite_friend.html", 65 | ), 66 | ( 67 | b"project/introduction_project_mail_2d.html", 68 | b"project/introduction_project_mail_2d.html", 69 | ), 70 | ( 71 | b"share/invitation_subject.html", 72 | b"share/invitation_subject.html", 73 | ), 74 | (b"orders/delivered.html", b"orders/delivered.html"), 75 | ( 76 | b"project/project_action_notification.html", 77 | b"project/project_action_notification.html", 78 | ), 79 | ( 80 | b"project/notice_original_deletion_1week.html", 81 | b"project/notice_original_deletion_1week.html", 82 | ), 83 | (b"accounts/welcome.html", b"accounts/welcome.html"), 84 | ( 85 | b"project/download_zip.html", 86 | b"project/download_zip.html", 87 | ), 88 | ( 89 | b"orders/in_production.html", 90 | b"orders/in_production.html", 91 | ), 92 | ( 93 | b"project/notice_original_deletion_1month.html", 94 | b"project/notice_original_deletion_1month.html", 95 | ), 96 | ( 97 | b"accounts/introducing_email_4d.html", 98 | b"accounts/introducing_email_4d.html", 99 | ), 100 | ( 101 | b"orders/reorder_incentive_mail.html", 102 | b"orders/reorder_incentive_mail.html", 103 | ), 104 | ( 105 | b"project/project_action_like_comment_notification.html", 106 | b"project/project_action_like_comment_notification.html", 107 | ), 108 | ( 109 | b"accounts/introducing_email_2d.html", 110 | b"accounts/introducing_email_2d.html", 111 | ), 112 | ( 113 | b"project/package_expired.html", 114 | b"project/package_expired.html", 115 | ), 116 | ( 117 | b"project/package_upgrade.html", 118 | b"project/package_upgrade.html", 119 | ), 120 | ( 121 | b"project/introduction_project_mail_7d.html", 122 | b"project/introduction_project_mail_7d.html", 123 | ), 124 | ( 125 | b"subscriptions/subscription_email.html", 126 | b"subscriptions/subscription_email.html", 127 | ), 128 | ( 129 | b"accounts/introducing_email_3d.html", 130 | b"accounts/introducing_email_3d.html", 131 | ), 132 | ( 133 | b"accounts/introducing_email_1d.html", 134 | b"accounts/introducing_email_1d.html", 135 | ), 136 | ( 137 | b"supports/support_request.html", 138 | b"supports/support_request.html", 139 | ), 140 | ( 141 | b"supports/support_confirm.html", 142 | b"supports/support_confirm.html", 143 | ), 144 | ], 145 | max_length=255, 146 | verbose_name="template", 147 | ), 148 | ), 149 | ( 150 | "subject", 151 | models.CharField( 152 | blank=True, max_length=255, verbose_name="subject" 153 | ), 154 | ), 155 | ("content", models.TextField(verbose_name="content")), 156 | ( 157 | "language", 158 | models.CharField( 159 | choices=[(b"de", b"German")], 160 | default=b"de", 161 | max_length=10, 162 | verbose_name="language", 163 | ), 164 | ), 165 | ("created", models.DateTimeField(default=django.utils.timezone.now)), 166 | ("modified", models.DateTimeField(default=django.utils.timezone.now)), 167 | ], 168 | ), 169 | migrations.AlterUniqueTogether( 170 | name="emailtemplate", 171 | unique_together=set([("title", "language")]), 172 | ), 173 | ] 174 | -------------------------------------------------------------------------------- /emailtemplates/email.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | import os 4 | import re 5 | from smtplib import SMTPException 6 | from urllib.parse import urljoin 7 | 8 | from django.conf import settings 9 | from django.core.exceptions import ObjectDoesNotExist 10 | from django.core.mail import EmailMessage 11 | from django.template import Template, Context, TemplateDoesNotExist 12 | from django.template.loader import get_template 13 | 14 | from .models import now, EmailTemplate 15 | from .registry import email_templates 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class EmailFromTemplate(object): 21 | """ 22 | EmailFromTemplate class tries to load template from database. If template object is not found, 23 | it tries to load template from templates/emailtemplates directory. 24 | This enables developers to create default templates for emails that are sent, 25 | and Site Admins to easily override the templates and provide email translations. 26 | 27 | Site Admins should know given template context. 28 | Site Admins should be familiar with Django Template System. 29 | """ 30 | 31 | def __init__( 32 | self, 33 | name="", 34 | from_email=settings.DEFAULT_FROM_EMAIL, 35 | base_url="", 36 | language=settings.LANGUAGE_CODE, 37 | subject="", 38 | template_class=EmailTemplate, 39 | registry_validation=True, 40 | template_object=None, 41 | ): 42 | """ 43 | Class constructor 44 | 45 | @param name: template name 46 | @param from_email: sender email address, by default settings.DEFAULT_FROM_EMAIL 47 | @param language: email language, by default settings.LANGUAGE_CODE 48 | @param subject: subject of the email 49 | @param template_class: class, template objects will be retrieved from 50 | @param registry_validation: if True template must be registered prior to instantiating EmailFromTemplate 51 | 52 | By default 'date' context variable is filled in. 53 | """ 54 | if registry_validation: 55 | email_templates.get_registration(name) 56 | self.from_email = from_email 57 | self.template_class = template_class 58 | self.template_object = template_object 59 | self.subject = subject 60 | self.language = language 61 | self.name = name 62 | self.base_url = base_url or getattr(settings, "BASE_URL", "") 63 | 64 | self.template = None 65 | self.compiled_template = None # for storing compiled template 66 | self.context = {"date": now()} # default context 67 | self.sent = 0 # number of messages sent 68 | self.message = "" 69 | self.content_subtype = "html" 70 | self._template_source = "default" 71 | 72 | @property 73 | def template_source(self): 74 | """Source of the template. One of the following: 75 | * default 76 | * filesystem 77 | * database 78 | """ 79 | return self._template_source 80 | 81 | def __get_path(self): 82 | return self.name 83 | 84 | def __get_template_from_file(self): 85 | path = self.__get_path() 86 | try: 87 | self.compiled_template = get_template(path) 88 | except (TemplateDoesNotExist, IOError): 89 | logger.warning( 90 | "Can't find %s template in the filesystem, will use very default one.", 91 | path, 92 | ) 93 | else: 94 | self._template_source = "filesystem" 95 | 96 | def build_absolute_uri(self, url: str): 97 | """ 98 | Builds an absolute URI. 99 | """ 100 | absolute_http_url_re = re.compile(r"^https?://", re.I) 101 | if absolute_http_url_re.match(url): 102 | return url 103 | return urljoin(self.base_url, url) 104 | 105 | def get_template_object(self): 106 | if self.template_object: 107 | return self.template_object 108 | return self.template_class.objects.get(title=self.name, language=self.language) 109 | 110 | def get_subject(self, template): 111 | subject_template = str(template.subject) or self.subject 112 | subject = Template(subject_template).render(Context(self.get_context())) 113 | return subject 114 | 115 | def get_object(self): 116 | while True: 117 | try: 118 | tmp = self.get_template_object() 119 | except ObjectDoesNotExist: 120 | logger.warning( 121 | "Can't find EmailTemplate object in database, using default file template." 122 | ) 123 | break 124 | except UnicodeError: 125 | logger.warning( 126 | "Can't convert to unicode EmailTemplate object from database, using default file template." 127 | ) 128 | break 129 | else: 130 | self.template = str(tmp.content) 131 | self.subject = self.get_subject(tmp) 132 | self._template_source = "database" 133 | logger.debug("Got template %s from database", self.name) 134 | return 135 | # fallback 136 | self.__get_template_from_file() 137 | 138 | def __compile_template(self): 139 | if not self.compiled_template: 140 | self.compiled_template = Template(self.template) 141 | 142 | def get_context(self): 143 | self.context.update( 144 | {"default_attachments": self.get_default_attachments(as_links=True)} 145 | ) 146 | return self.context 147 | 148 | def render_message(self): 149 | self.__compile_template() 150 | try: 151 | message = self.compiled_template.render(self.get_context()) # 152 | except AttributeError: 153 | # NOTE: for template from string Context() is still required! 154 | message = self.compiled_template.render(Context(self.get_context())) 155 | self.message = message 156 | 157 | def get_message_object(self, send_to, attachment_paths, *args, **kwargs): 158 | if kwargs.get("reply_to") is None: 159 | defaut_reply_to_email = getattr(settings, "DEFAULT_REPLY_TO_EMAIL", None) 160 | if defaut_reply_to_email: 161 | kwargs["reply_to"] = [defaut_reply_to_email] 162 | 163 | msg = EmailMessage( 164 | self.subject, self.message, self.from_email, send_to, *args, **kwargs 165 | ) 166 | if attachment_paths: 167 | for path in attachment_paths: 168 | msg.attach_file(path) 169 | return msg 170 | 171 | def send_email( 172 | self, send_to, attachment_paths=None, fail_silently=True, *args, **kwargs 173 | ): 174 | """ 175 | Sends email to recipient based on self object parameters. 176 | 177 | @param fail_silently: When it’s False, msg.send() will raise an smtplib.SMTPException if an error occurs. 178 | @param send_to: recipient email 179 | @param args: additional args passed to EmailMessage 180 | @param kwargs: kwargs passed to EmailMessage 181 | @param attachment_paths: paths to attachments as received by django EmailMessage.attach_file(path) method 182 | @return: number of sent messages 183 | """ 184 | msg = self.get_message_object(send_to, attachment_paths, *args, **kwargs) 185 | msg.content_subtype = self.content_subtype 186 | 187 | try: 188 | self.sent = msg.send() 189 | except SMTPException as e: 190 | if not fail_silently: 191 | raise 192 | logger.error("Problem sending email to %s: %s", send_to, e) 193 | 194 | return self.sent 195 | 196 | def get_default_attachments(self, as_links=False): 197 | """ 198 | Prepare default attachments data (files will be include into email as attachments) 199 | """ 200 | attachments = [] 201 | try: 202 | tmp = self.get_template_object() 203 | except ObjectDoesNotExist: 204 | return attachments 205 | 206 | for attachment in tmp.attachments.filter(send_as_link=as_links): 207 | if as_links: 208 | attachments.append( 209 | ( 210 | attachment.get_name(), 211 | self.build_absolute_uri(attachment.attachment_file.url), 212 | ) 213 | ) 214 | else: 215 | attachments.append( 216 | ( 217 | os.path.basename(attachment.attachment_file.name), 218 | attachment.attachment_file.read(), 219 | ) 220 | ) 221 | return attachments 222 | 223 | def send(self, to, attachment_paths=None, *args, **kwargs): 224 | """This function does all the operations on eft object, that are necessary to send email. 225 | Usually one would use eft object like this: 226 | eft = EmailFromTemplate(name='sth/sth.html') 227 | eft.get_object() 228 | eft.render_message() 229 | eft.send_email(['email@example.com']) 230 | return eft.sent 231 | """ 232 | attachments = self.get_default_attachments(as_links=False) 233 | attachments.extend(kwargs.pop("attachments", [])) 234 | 235 | self.get_object() 236 | self.render_message() 237 | self.send_email(to, attachment_paths, attachments=attachments, *args, **kwargs) 238 | if self.sent: 239 | logger.info("Mail has been sent to: %s ", to) 240 | return self.sent 241 | --------------------------------------------------------------------------------