├── 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 |
--------------------------------------------------------------------------------