Skip to content

Commit

Permalink
Merge pull request #107 from incuna/use-incuna-pigeon
Browse files Browse the repository at this point in the history
Add `incuna-pigeon` to send notifications
  • Loading branch information
LilyFoote committed Mar 16, 2015
2 parents 6e4d651 + 5dbf92e commit 92500c6
Show file tree
Hide file tree
Showing 14 changed files with 213 additions and 172 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## v8.0.0 (Upcoming)

Use `incuna-pigeon` for notifications.

## v7.0.1

* Fix `UserChangeForm` admin form `fields` to only include fields used in `UserAdmin.fieldsets`.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
install_requires = (
'djangorestframework>=2.4.4,<3',
'incuna_mail>=2.0.0,<4.0.0',
'incuna-pigeon>=0.1.0,<1.0.0',
)

extras_require = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans %}
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans with name=site.name %}
You are receiving this email because your email address has been used to
register an account at {{ site.name }}.
register an account at {{ name }}.

Please click the following link to complete your registration:
{% endblocktrans %}
http://{{ site.domain }}/#/register/verify/{{ uid }}/{{ token }}/
{{ protocol }}://{{ site.domain }}/#/register/verify/{{ uid }}/{{ token }}/

{% endautoescape %}
{% blocktrans with name=site.name %}
The {{ name }} team.
{% endblocktrans %}{% endautoescape %}
{% endspaceless %}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans %}
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans with name=site.name %}
You are receiving this email because your email address has been used to
register an account at {{ site.name }}.
register an account at {{ name }}.

Please click the following link to complete your registration:
{% endblocktrans %}
http://{{ site.domain }}/#/register/verify/{{ uid }}/{{ token }}/
{{ protocol }}://{{ site.domain }}/#/register/verify/{{ uid }}/{{ token }}/

{% endautoescape %}
{% blocktrans with name=site.name %}
The {{ name }} team.
{% endblocktrans %}{% endautoescape %}
{% endspaceless %}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans %}
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans with name=site.name %}
You are receiving this email because you requested a password reset
for your user account at {{ site.name }}.
for your user account at {{ name }}.

Please go to the following page and choose a new password:{% endblocktrans %}
{{ protocol }}://{{ site.domain }}{% url 'user_management_api:password_reset_confirm' uidb64=uid token=token %}
{% blocktrans %}
The {{ site.name }} team.
{% blocktrans with name=site.name %}
The {{ name }} team.
{% endblocktrans %}{% endautoescape %}
{% endspaceless %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% spaceless %}{% load i18n %}{% autoescape off %}{% blocktrans with name=site.name %}
You are receiving this email because you requested a password reset
for your user account at {{ name }}.

Please go to the following page and choose a new password:{% endblocktrans %}
{{ protocol }}://{{ site.domain }}{% url 'user_management_api:password_reset_confirm' uidb64=uid token=token %}
{% blocktrans with name=site.name %}
The {{ name }} team.
{% endblocktrans %}{% endautoescape %}
{% endspaceless %}
45 changes: 31 additions & 14 deletions user_management/api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.core import mail
from django.core.cache import cache
from django.core.urlresolvers import reverse
Expand All @@ -23,6 +24,8 @@

User = get_user_model()
TEST_SERVER = 'http://testserver'
SEND_METHOD = 'user_management.utils.notifications.incuna_mail.send'
EMAIL_CONTEXT = 'user_management.utils.notifications.email_context'


class GetAuthTokenTest(APIRequestTestCase):
Expand Down Expand Up @@ -186,7 +189,7 @@ def test_unauthenticated_user_post(self):
email = mail.outbox[0]
verify_url_regex = re.compile(
r'''
http://example\.com/\#/register/verify/
https://example\.com/\#/register/verify/
[0-9A-Za-z_\-]+/ # uid
[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20}/ # token
''',
Expand Down Expand Up @@ -266,18 +269,29 @@ def tearDown(self):
def test_existent_email(self):
email = 'exists@example.com'
user = UserFactory.create(email=email)
context = {}
site = Site.objects.get_current()

request = self.create_request(
'post',
data={'email': email},
auth=False,
)
view = self.view_class.as_view()
with patch.object(self.view_class, 'send_email') as send_email:
response = view(request)
with patch(EMAIL_CONTEXT) as get_context:
get_context.return_value = context
with patch(SEND_METHOD) as send_email:
response = view(request)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

send_email.assert_called_once_with(user)
expected = {
'to': user.email,
'template_name': 'user_management/password_reset_email.txt',
'html_template_name': 'user_management/password_reset_email.html',
'subject': '{} password reset'.format(site.domain),
'context': context,
}
send_email.assert_called_once_with(**expected)

def test_authenticated(self):
request = self.create_request('post', auth=True)
Expand All @@ -295,7 +309,7 @@ def test_non_existent_email(self):
auth=False,
)
view = self.view_class.as_view()
with patch.object(self.view_class, 'send_email') as send_email:
with patch(SEND_METHOD) as send_email:
response = view(request)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

Expand All @@ -307,15 +321,18 @@ def test_missing_email(self):
response = view(request)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_send_email(self):
email = 'test@example.com'
user = UserFactory.create(
email=email,
# Don't send the verification email
email_verification_required=False,
)
def test_email_content(self):
"""Assert email content is output correctly."""
email = 'exists@example.com'
user = UserFactory.create(email=email)

self.view_class().send_email(user)
request = self.create_request(
'post',
data={'email': email},
auth=False,
)
view = self.view_class.as_view()
view(request)

self.assertEqual(len(mail.outbox), 1)

Expand Down Expand Up @@ -955,7 +972,7 @@ def test_send_email(self):
email = mail.outbox[0]

self.assertIn(user.email, email.to)
expected = 'http://example.com/#/register/verify/'
expected = 'https://example.com/#/register/verify/'
self.assertIn(expected, email.body)

expected = 'example.com account validate'
Expand Down
25 changes: 3 additions & 22 deletions user_management/api/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import ugettext_lazy as _
from incuna_mail import send
from rest_framework import generics, response, status, views
from rest_framework.authentication import get_authorization_header
from rest_framework.authtoken.views import ObtainAuthToken
Expand Down Expand Up @@ -103,14 +101,6 @@ class PasswordResetEmail(generics.GenericAPIView):
throttle_classes = [throttling.PasswordResetRateThrottle]
throttle_scope = 'passwords'

def email_context(self, site, user):
return {
'protocol': 'https',
'site': site,
'token': default_token_generator.make_token(user),
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
}

def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if not serializer.is_valid():
Expand All @@ -125,20 +115,11 @@ def post(self, request, *args, **kwargs):
except User.DoesNotExist:
pass
else:
self.send_email(user)
user.send_password_reset()

msg = _('Password reset request successful. Please check your email.')
return response.Response(msg, status=status.HTTP_204_NO_CONTENT)

def send_email(self, user):
site = Site.objects.get_current()
send(
to=[user.email],
template_name=self.template_name,
subject=_('{domain} password reset').format(domain=site.domain),
context=self.email_context(site, user),
)


class OneTimeUseAPIMixin(object):
def initial(self, request, *args, **kwargs):
Expand Down
66 changes: 23 additions & 43 deletions user_management/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from django.utils.encoding import force_bytes, python_2_unicode_compatible
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _
from incuna_mail import send

from user_management.utils import notifications


class UserManager(BaseUserManager):
Expand Down Expand Up @@ -128,56 +129,35 @@ def create_superuser(self, email, password, **extra_fields):


class EmailVerifyUserMethodsMixin:
EMAIL_SUBJECT = '{domain} account validate'
TEXT_EMAIL_TEMPLATE = 'user_management/account_validation_email.txt'
HTML_EMAIL_TEMPLATE = 'user_management/account_validation_email.html'

def email_context(self, site):
return {
'uid': urlsafe_base64_encode(force_bytes(self.pk)),
'token': default_token_generator.make_token(self),
'site': site,
}

def email_kwargs(self, context, domain):
"""Prepare the kwargs to be passed to incuna_mail.send"""
return {
'to': [self.email],
'template_name': self.TEXT_EMAIL_TEMPLATE,
'html_template_name': self.HTML_EMAIL_TEMPLATE,
'subject': self.get_email_subject(domain),
'context': context,
}

def get_email_subject(self, domain):
return _(self.EMAIL_SUBJECT).format(domain=domain)

def send_validation_email(self):
"""
Send a validation email to the user's email address.
"""
Define how validation and password reset emails are sent.
The email subject can be customised by overriding
VerifyEmailMixin.EMAIL_SUBJECT or VerifyEmailMixin.get_email_subject.
To include your site's domain in the subject, include {domain} in
VerifyEmailMixin.EMAIL_SUBJECT.
`password_reset_notification` and `validation_notification` can be overriden to
provide custom settings to send emails.
"""
password_reset_notification = notifications.PasswordResetNotification
validation_notification = notifications.ValidationNotification

By default send_validation_email sends a multipart email using
VerifyEmailMixin.TEXT_EMAIL_TEMPLATE and
VerifyEmailMixin.HTML_EMAIL_TEMPLATE. To send a text-only email
set VerifyEmailMixin.HTML_EMAIL_TEMPLATE to None.
def generate_token(self):
"""Generate user token for account validation."""
return default_token_generator.make_token(self)

You can also customise the context available in the email templates
by extending VerifyEmailMixin.email_context.
def generate_uid(self):
"""Generate user uid for account validation."""
return urlsafe_base64_encode(force_bytes(self.pk))

If you want more control over the sending of the email you can
extend VerifyEmailMixin.email_kwargs.
"""
def send_validation_email(self):
"""Send a validation email to the user's email address."""
if not self.email_verification_required:
raise ValueError(_('Cannot validate already active user.'))

site = Site.objects.get_current()
context = self.email_context(site)
send(**self.email_kwargs(context, site.domain))
self.validation_notification(user=self, site=site).notify()

def send_password_reset(self):
"""Send a password reset to the user's email address."""
site = Site.objects.get_current()
self.password_reset_notification(user=self, site=site).notify()


class EmailVerifyUserMixin(EmailVerifyUserMethodsMixin, models.Model):
Expand Down
8 changes: 7 additions & 1 deletion user_management/models/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db import models


from .notifications import CustomPasswordResetNotification
from ..mixins import (
AvatarMixin,
BasicUserFieldsMixin,
Expand All @@ -22,10 +23,15 @@ class BasicUser(BasicUserFieldsMixin, AbstractBaseUser):
pass


class VerifyEmailUser(VerifyEmailMixin):
class VerifyEmailUser(VerifyEmailMixin, AbstractBaseUser):
pass


class CustomVerifyEmailUser(VerifyEmailMixin, AbstractBaseUser):
"""Customise the notification class to send a password reset."""
password_reset_notification = CustomPasswordResetNotification


class CustomBasicUserFieldsMixin(
NameUserMethodsMixin, EmailUserMixin, DateJoinedUserMixin,
IsStaffUserMixin):
Expand Down
7 changes: 7 additions & 0 deletions user_management/models/tests/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from user_management.utils.notifications import PasswordResetNotification


class CustomPasswordResetNotification(PasswordResetNotification):
"""Test setting a custom notification to alter how we send the password reset."""
text_email_template = 'my_custom_email.txt'
html_email_template = None

0 comments on commit 92500c6

Please sign in to comment.