Skip to content

Commit 32aa167

Browse files
committed
Added personalization and i18n features
1 parent 65e5a34 commit 32aa167

File tree

18 files changed

+352
-24
lines changed

18 files changed

+352
-24
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Distribution
88
build/
99
dist/
10+
django_templated_mail.egg-info
1011

1112
# Byte-compiled / optimized / DLL files
1213
__pycache__/
@@ -36,4 +37,7 @@ ENV/
3637
*.DS_Store
3738

3839
# logs
39-
*.log
40+
*.log
41+
42+
# vim
43+
*.swp

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Welcome to django-templated-mail documentation!
1313
settings
1414
templates_syntax
1515
sample_usage
16+
personalization
1617

1718
Indices and tables
1819
==================

docs/source/personalization.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Personalizing e-mails
2+
========================
3+
4+
5+
6+
Interpolating user data
7+
------------------------------
8+
9+
If you want to put some personal data of your user in the e-mail, you may use
10+
the ``user`` variable in your context. To use this variable you either need to
11+
pass the ``request`` object when creating the ``BaseEmailMessage``:
12+
13+
.. code-block:: python
14+
email_message = BaseEmailMessage(
15+
request=request,
16+
template_name='personalized_mail.html'
17+
)
18+
19+
or you need to specify actual user objects in the `to` field:
20+
21+
.. code-block:: python
22+
email_message.send(to=[user1, user2], single_email=False)
23+
24+
i18n
25+
------
26+
27+
Django features some pretty nice i18n mechanism, but it can be insufficient
28+
when emailing your users. Your e-mails will ofter be sent from inside a celery
29+
task, where the ``request`` object is not accessible. If you still want to
30+
translate the e-mail for your userbase, you need to specify the `locale_field`
31+
value in your settings. This value should point to a field (or a property)
32+
on your user models that contains their locale (it is up to you,
33+
how do you populate this field). If you then send personalized emails, as
34+
described above, they will be translated to your users' locales.

docs/source/settings.rst

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ You may optionally provide following settings:
55

66
.. code-block:: python
77
8-
'DOMAIN': 'example.com'
9-
'SITE_NAME': 'Foo Website'
8+
'TEMPLATED_MAIL': {
9+
'DOMAIN': 'example.com',
10+
'SITE_NAME': 'Foo Website',
11+
LOCALE_FIELD: 'locale',
12+
}
13+
1014

1115
DOMAIN
1216
------
@@ -25,4 +29,12 @@ Used in email template context. Usually it will contain the desired title of you
2529
app. If not provided the current site's name will be used.
2630

2731

28-
**Default**: ``False``
32+
**Required**: ``False``
33+
34+
LOCALE_FIELD
35+
------------------
36+
37+
The field on a user model that contains the locale name to be used. If not
38+
specified, the per-user i18n is disabled.
39+
40+
**Required**: ``False``

manage.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
import sys
3+
4+
if __name__ == "__main__":
5+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
6+
from django.core.management import execute_from_command_line
7+
args = sys.argv
8+
execute_from_command_line(args)

requirements/base

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
django>=1.8
2+
six==1.12.0

requirements/test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ flake8==3.4.1
33
pytest==3.2.2
44
pytest-cov==2.5.1
55
pytest-django==3.1.2
6-
pytest-pythonpath==0.7.1
6+
pytest-pythonpath==0.7.1
7+
six==1.12.0

templated_mail/mail.py

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
1+
import copy
2+
import contextlib
3+
import six
4+
15
from django.conf import settings
6+
from django.contrib.auth.models import AbstractBaseUser
27
from django.contrib.sites.shortcuts import get_current_site
38
from django.core import mail
9+
from django.utils import translation
410
from django.template.context import make_context
511
from django.template.loader import get_template
612
from django.template.loader_tags import BlockNode, ExtendsNode
713
from django.views.generic.base import ContextMixin
814

915

16+
LOCALE_FIELD = getattr(
17+
settings, 'TEMPLATED_MAIL', {}
18+
).get('locale_field', None)
19+
20+
21+
@contextlib.contextmanager
22+
def translation_context(language):
23+
prev_language = translation.get_language()
24+
try:
25+
translation.activate(language)
26+
yield
27+
finally:
28+
translation.activate(prev_language)
29+
30+
31+
@contextlib.contextmanager
32+
def nullcontext():
33+
yield
34+
35+
1036
class BaseEmailMessage(mail.EmailMultiAlternatives, ContextMixin):
1137
_node_map = {
1238
'subject': 'subject',
@@ -25,8 +51,9 @@ def __init__(self, request=None, context=None, template_name=None,
2551

2652
if template_name is not None:
2753
self.template_name = template_name
54+
self.is_rendered = False
2855

29-
def get_context_data(self, **kwargs):
56+
def get_context_data(self, user=None, **kwargs):
3057
ctx = super(BaseEmailMessage, self).get_context_data(**kwargs)
3158
context = dict(ctx, **self.context)
3259
if self.request:
@@ -40,14 +67,14 @@ def get_context_data(self, **kwargs):
4067
site_name = context.get('site_name') or (
4168
getattr(settings, 'SITE_NAME', '') or site.name
4269
)
43-
user = context.get('user') or self.request.user
70+
user = user or context.get('user') or self.request.user
4471
else:
4572
domain = context.get('domain') or getattr(settings, 'DOMAIN', '')
4673
protocol = context.get('protocol') or 'http'
4774
site_name = context.get('site_name') or getattr(
4875
settings, 'SITE_NAME', ''
4976
)
50-
user = context.get('user')
77+
user = user or context.get('user')
5178

5279
context.update({
5380
'domain': domain,
@@ -57,27 +84,62 @@ def get_context_data(self, **kwargs):
5784
})
5885
return context
5986

60-
def render(self):
61-
context = make_context(self.get_context_data(), request=self.request)
87+
def render(self, user=None):
88+
if self.is_rendered:
89+
return
90+
context = make_context(
91+
self.get_context_data(user),
92+
request=self.request,
93+
)
94+
if user is None or LOCALE_FIELD is None:
95+
language_context = nullcontext()
96+
else:
97+
language_context = translation_context(
98+
getattr(user, LOCALE_FIELD)
99+
)
62100
template = get_template(self.template_name)
63-
with context.bind_template(template.template):
101+
with language_context, context.bind_template(template.template):
64102
blocks = self._get_blocks(template.template.nodelist, context)
65103
for block_node in blocks.values():
66104
self._process_block(block_node, context)
67105
self._attach_body()
106+
self.is_rendered = True
68107

69-
def send(self, to, *args, **kwargs):
70-
self.render()
71-
72-
self.to = to
73-
self.cc = kwargs.pop('cc', [])
74-
self.bcc = kwargs.pop('bcc', [])
75-
self.reply_to = kwargs.pop('reply_to', [])
76-
self.from_email = kwargs.pop(
77-
'from_email', settings.DEFAULT_FROM_EMAIL
78-
)
79-
80-
super(BaseEmailMessage, self).send(*args, **kwargs)
108+
def send(self, to, single_email=True, *args, **kwargs):
109+
if single_email:
110+
self.render()
111+
to_emails = []
112+
for recipient in to:
113+
if isinstance(recipient, AbstractBaseUser):
114+
to_emails.append(recipient.email)
115+
elif isinstance(recipient, six.string_types):
116+
to_emails.append(recipient)
117+
else:
118+
raise TypeError(
119+
'The `to` argument should contain strings or users'
120+
)
121+
self.to = to_emails
122+
self.cc = kwargs.pop('cc', [])
123+
self.bcc = kwargs.pop('bcc', [])
124+
self.reply_to = kwargs.pop('reply_to', [])
125+
self.from_email = kwargs.pop(
126+
'from_email', settings.DEFAULT_FROM_EMAIL
127+
)
128+
super(BaseEmailMessage, self).send(*args, **kwargs)
129+
else:
130+
for recipient in to:
131+
email = copy.copy(self)
132+
if isinstance(recipient, AbstractBaseUser):
133+
email_to = [recipient.email]
134+
email.render(user=recipient)
135+
elif isinstance(recipient, six.string_types):
136+
email_to = [recipient]
137+
email.render()
138+
else:
139+
raise TypeError(
140+
'The `to` argument should contain strings or users'
141+
)
142+
email.send(to=email_to, *args, **kwargs)
81143

82144
def _process_block(self, block_node, context):
83145
attr = self._node_map.get(block_node.name)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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 <EMAIL@ADDRESS>, 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: 2019-03-08 15:35+0000\n"
12+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14+
"Language-Team: LANGUAGE <[email protected]>\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+
20+
#: tests/templates/personalized_mail.html:4
21+
#: tests/templates/personalized_mail.html:5
22+
msgid "Hello"
23+
msgstr "გამარჯობა"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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 <EMAIL@ADDRESS>, 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: 2019-04-01 09:54+0000\n"
12+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14+
"Language-Team: LANGUAGE <[email protected]>\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+
20+
#: tests/templates/personalized_mail.html:4
21+
#: tests/templates/personalized_mail.html:5
22+
msgid "Hello"
23+
msgstr "Cześć"

0 commit comments

Comments
 (0)