Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refs #88 Fix contrib app for django 2.1 #89

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ matrix:
- python: 3.7
env: TOX_ENV=py37-django21

- env: TOX_ENV=flake8
- env: TOX_ENV=lint

script:
- tox -e $TOX_ENV
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Changelog
0.22 (unreleased)
-----------------

- Nothing changed yet.
- Fix mail_factory.contrib app for django >= 2.1
- Isort the code
- Display python warnings while running tox targets
- Remove some Django < 1.11 compatibility code


0.21 (2018-09-20)
Expand Down
4 changes: 1 addition & 3 deletions demo/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
},
]

MIDDLEWARE_CLASSES = (
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
Expand All @@ -106,8 +106,6 @@
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

MIDDLEWARE = MIDDLEWARE_CLASSES

ROOT_URLCONF = 'demo.urls'

# Python dotted path to the WSGI application used by Django's runserver.
Expand Down
20 changes: 11 additions & 9 deletions docs/source/django.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Here is an example of how you can use Mail Factory with the
You can first add this pattern in your ``urls.py``:

.. code-block:: python
from mail_factory.contrib.auth.views import password_reset

url(_(r'^password_reset/$'),
'mail_factory.contrib.auth.views.password_reset'),
url(_(r'^password_reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-'
r'[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm')
url(_(r'^password_reset/done/$'),
'django.contrib.auth.views.password_reset_done')

urlpatterns = [
url(_(r'^password_reset/$'), password_reset, name="password_reset"),

...
]


Then you can overload the default templates
Expand All @@ -41,6 +41,7 @@ But you can also register your own ``PasswordResetMail``:

class PasswordResetMail(AppBaseMail, PasswordResetMail):
"""Add the App header + i18n for PasswordResetMail."""
template_name = 'password_reset'


class PasswordResetForm(AppBaseMailForm):
Expand All @@ -60,10 +61,11 @@ But you can also register your own ``PasswordResetMail``:
You can then update your urls.py to use this new form:

.. code-block:: python
from mail_factory.contrib.auth.views import PasswordResetView

url(_(r'^password_reset/$'),
'mail_factory.contrib.auth.views.password_reset',
{'email_template_name': 'password_reset'}),
PasswordResetView.as_view(email_template_name="password_reset"),
name="password_reset"),


The default PasswordResetMail is not registered in the factory so that
Expand Down
3 changes: 1 addition & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Welcome to django-mail-factory's documentation!

Django Mail Factory is a little Django app that let's you manage emails
for your project very easily.
It's compatible with Django >= 1.11.

Features
--------
Expand Down Expand Up @@ -138,8 +139,6 @@ them by sending them to a custom address with a custom context.
to contain 'mail_factory.SimpleMailFactoryConfig' instead of
'mail_factory'.

This is only available in Django 1.7 and above.


Contents
--------
Expand Down
4 changes: 2 additions & 2 deletions docs/source/interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ your mail.
import datetime
import uuid

from django.conf import settings
from django.core.urlresolvers import reverse_lazy as reverse
from django import forms
from django.conf import settings
from django.urls import reverse_lazy as reverse
from mail_factory import factory, MailForm, BaseMail


Expand Down
51 changes: 24 additions & 27 deletions mail_factory/contrib/auth/forms.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.sites.models import get_current_site

from django.contrib.auth.forms import \
PasswordResetForm as DjangoPasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import int_to_base36
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode

from .mails import PasswordResetMail
from mail_factory import factory

from .mails import PasswordResetMail


class PasswordResetForm(PasswordResetForm):
class PasswordResetForm(DjangoPasswordResetForm):
"""MailFactory PasswordReset alternative."""

def save(self, domain_override=None,
subject_template_name=None, # Not used anymore
email_template_name=None, # Mail Factory template name
mail_object=None, # Mail Factory Mail object
use_https=False, token_generator=default_token_generator,
from_email=None, request=None):
def mail_factory_email(
self, domain_override=None, email_template_name=None,
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, extra_email_context=None):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
for user in self.users_cache:
email = self.cleaned_data["email"]
for user in self.get_users(email):
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override

context_params = {
'email': user.email,
context = {
'email': email,
'domain': domain,
'site_name': site_name,
'uid': int_to_base36(user.pk),
'uid': force_text(urlsafe_base64_encode(force_bytes(user.pk))),
'user': user,
'token': token_generator.make_token(user),
'protocol': use_https and 'https' or 'http',
'protocol': 'https' if use_https else 'http',
}

from_email = from_email or settings.DEFAULT_FROM_EMAIL
if extra_email_context is not None:
context.update(extra_email_context)

if email_template_name is not None:
mail = factory.get_mail_object(email_template_name,
context_params)
mail = factory.get_mail_object(email_template_name, context)
else:
if mail_object is None:
mail_object = PasswordResetMail
mail = mail_object(context_params)
mail = PasswordResetMail(context)

mail.send(emails=[user.email],
from_email=from_email)
mail.send(emails=[user.email], from_email=from_email)
29 changes: 22 additions & 7 deletions mail_factory/contrib/auth/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.views import password_reset as django_password_reset

from django.contrib.auth.views import \
PasswordResetView as DjangoPasswordResetView
from django.http import HttpResponseRedirect

from .forms import PasswordResetForm


def password_reset(request, **kwargs):
class PasswordResetView(DjangoPasswordResetView):
"""Substitute with the mail_factory PasswordResetForm."""
if 'password_reset_form' not in kwargs:
kwargs['password_reset_form'] = PasswordResetForm
if 'email_template_name' not in kwargs:
kwargs['email_template_name'] = None
form_class = PasswordResetForm
email_template_name = None

def form_valid(self, form):
opts = {
'use_https': self.request.is_secure(),
'token_generator': self.token_generator,
'from_email': self.from_email,
'email_template_name': self.email_template_name,
'request': self.request,
'extra_email_context': self.extra_email_context,
}
form.mail_factory_email(**opts)
return HttpResponseRedirect(self.get_success_url())


return django_password_reset(request, **kwargs)
password_reset = PasswordResetView.as_view()
1 change: 1 addition & 0 deletions mail_factory/factory.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import base64

from . import exceptions
from .forms import MailForm

Expand Down
8 changes: 2 additions & 6 deletions mail_factory/mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,8 @@ def _render_part(self, part, lang=None):

"""
tpl = select_template(self.get_template_part(part, lang=lang))
cur_lang = translation.get_language()
try:
translation.activate(lang or self.lang)
with translation.override(lang or self.lang):
rendered = tpl.render(self.context)
finally:
translation.activate(cur_lang)
return rendered.strip()

def create_email_msg(self, emails, attachments=None, from_email=None,
Expand All @@ -103,7 +99,7 @@ def create_email_msg(self, emails, attachments=None, from_email=None,
"""Create an email message instance."""

from_email = from_email or settings.DEFAULT_FROM_EMAIL
subject = self._render_part('subject.txt', lang=lang).strip()
subject = self._render_part('subject.txt', lang=lang)
try:
body = self._render_part('body.txt', lang=lang)
except TemplateDoesNotExist:
Expand Down
8 changes: 2 additions & 6 deletions mail_factory/messages.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
from os.path import basename
import re

try:
from email.MIMEBase import MIMEBase # python2
except ImportError:
from email.mime.base import MIMEBase # python3
from email.mime.base import MIMEBase
from os.path import basename

from django.conf import settings
from django.core.mail import EmailMultiAlternatives, SafeMIMEMultipart
Expand Down
2 changes: 1 addition & 1 deletion mail_factory/templates/mails/password_reset/body.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}

Expand Down
80 changes: 80 additions & 0 deletions mail_factory/tests/test_contrib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-

"""Keep in mind throughout those tests that the mails from demo.demo_app.mails
are automatically registered, and serve as fixture."""

from __future__ import unicode_literals

from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.views import (
PasswordResetConfirmView,
PasswordResetDoneView
)
from django.core import mail
from django.test import TestCase, override_settings
from django.urls import reverse

from mail_factory import factory
from mail_factory.contrib.auth.mails import PasswordResetMail
from mail_factory.contrib.auth.views import PasswordResetView, password_reset

urlpatterns = [
url(r'^reset/$', password_reset, name="reset"),
url(r'^reset_template_name/$',
PasswordResetView.as_view(email_template_name="password_reset"),
name="reset_template_name"),

url(r'^password_reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
PasswordResetConfirmView.as_view(), name="password_reset_confirm"),
url(r'^password_reset/done/$',
PasswordResetDoneView.as_view(), name="password_reset_done"),
url(r'^admin/', admin.site.urls),
]


def with_registered_mail_klass(mail_klass):
def wrapper(func):
def wrapped(*args, **kwargs):
factory.register(mail_klass)
result = func(*args, **kwargs)
factory.unregister(mail_klass)
return result
return wrapped
return wrapper


@override_settings(ROOT_URLCONF='mail_factory.tests.test_contrib')
class ContribTestCase(TestCase):
def test_password_reset_default(self):

user = User.objects.create_user(username='user',
email='admin@example.com',
password="password")

response = self.client.post(reverse("reset"), data={
"email": user.email
})
self.assertRedirects(response, reverse("password_reset_done"))

self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertEqual(email.subject, "Password reset on example.com")

@with_registered_mail_klass(PasswordResetMail)
def test_password_reset_with_template_name(self):

user = User.objects.create_user(username='user',
email='admin@example.com',
password="password")

response = self.client.post(reverse("reset_template_name"), data={
"email": user.email
})
self.assertRedirects(response, reverse("password_reset_done"))

self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertEqual(email.subject, "Password reset on example.com")
1 change: 0 additions & 1 deletion mail_factory/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from __future__ import unicode_literals


from django import forms
from django.test import TestCase

Expand Down
Loading