Skip to content

Commit

Permalink
Merge pull request #83 from fusionbox/release-1.6
Browse files Browse the repository at this point in the history
Cleanups for releasing authtools 1.6
  • Loading branch information
jxcl committed Jun 14, 2017
2 parents be5f6fe + c5c9979 commit fa82068
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 82 deletions.
4 changes: 3 additions & 1 deletion CHANGES.rst
Expand Up @@ -4,7 +4,9 @@ CHANGES
1.6.0 (unreleased)
------------------

- Nothing changed yet.
- Add support for Django 1.9, 1.10, 1.11 (Jared Proffitt #82)
- Remove old conditional imports dating as far back as Django 1.5
- Update readme


1.5.0 (2016-03-26)
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -6,7 +6,7 @@ django-authtools
:alt: Build Status


A custom user model app for Django 1.5+ that features email as username and
A custom user model app for Django 1.8+ that features email as username and
other things. It tries to stay true to the built-in user model for the most
part.

Expand Down
4 changes: 3 additions & 1 deletion authtools/backends.py
Expand Up @@ -42,7 +42,9 @@ def authenticate(self, username=None, password=None, **kwargs):
)


class CaseInsensitiveUsernameFieldModelBackend(CaseInsensitiveUsernameFieldBackendMixin, ModelBackend):
class CaseInsensitiveUsernameFieldModelBackend(
CaseInsensitiveUsernameFieldBackendMixin,
ModelBackend):
pass


Expand Down
53 changes: 24 additions & 29 deletions authtools/forms.py
@@ -1,42 +1,37 @@
from __future__ import unicode_literals

from django import forms, VERSION as DJANGO_VERSION
from django.forms.utils import flatatt
from django.contrib.auth.forms import (
ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget,
PasswordResetForm as OldPasswordResetForm,
UserChangeForm as DjangoUserChangeForm,
AuthenticationForm as DjangoAuthenticationForm,
)
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import identify_hasher
from django.contrib.auth.hashers import identify_hasher, UNUSABLE_PASSWORD_PREFIX
from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.html import format_html

User = get_user_model()


def is_password_usable(pw):
# like Django's is_password_usable, but only checks for unusable
# passwords, not invalidly encoded passwords too.
try:
# 1.5
from django.contrib.auth.hashers import UNUSABLE_PASSWORD
return pw != UNUSABLE_PASSWORD
except ImportError:
# 1.6
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
return not pw.startswith(UNUSABLE_PASSWORD_PREFIX)
"""Decide whether a password is usable only by the unusable password prefix.
We can't use django.contrib.auth.hashers.is_password_usable either, because
it not only checks against the unusable password, but checks for a valid
hasher too. We need different error messages in those cases.
"""

return not pw.startswith(UNUSABLE_PASSWORD_PREFIX)


class BetterReadOnlyPasswordHashWidget(ReadOnlyPasswordHashWidget):
"""
A ReadOnlyPasswordHashWidget that has a less intimidating output.
"""
def render(self, name, value, attrs=None, renderer=None):
try:
from django.forms.utils import flatatt
except ImportError:
from django.forms.util import flatatt # Django < 1.7
final_attrs = flatatt(self.build_attrs(attrs))

if not value or not is_password_usable(value):
Expand Down Expand Up @@ -143,8 +138,7 @@ class UserChangeForm(forms.ModelForm):

class Meta:
model = User
if DJANGO_VERSION >= (1, 6):
fields = '__all__'
fields = '__all__'

def __init__(self, *args, **kwargs):
super(UserChangeForm, self).__init__(*args, **kwargs)
Expand Down Expand Up @@ -174,19 +168,20 @@ class FriendlyPasswordResetForm(OldPasswordResetForm):
"sure you've registered?")

def clean_email(self):
super_clean_email = getattr(
super(FriendlyPasswordResetForm, self), 'clean_email', None)
if callable(super_clean_email): # Django == 1.5
# Django 1.5 sets self.user_cache
return super_clean_email()

# Simulate Django 1.5 behavior in Django >= 1.6.
# This is not as efficient as in Django 1.5, since clean_email() and
# save() will be running the same query twice.
# Whereas Django 1.5 just caches it.
"""Return an error message if the email address being reset is unknown.
This is to revert https://code.djangoproject.com/ticket/19758
The bug #19758 tries not to leak emails through password reset because
only usernames are unique in Django's default user model.
django-authtools leaks email addresses through the registration form.
In the case of django-authtools not warning the user doesn't add any
security, and worsen user experience.
"""

email = self.cleaned_data['email']
qs = User._default_manager.filter(is_active=True, email__iexact=email)
results = [user for user in qs if user.has_usable_password()]
results = list(self.get_users(email))

if not results:
raise forms.ValidationError(self.error_messages['unknown'])
return email
Expand Down
9 changes: 4 additions & 5 deletions authtools/models.py
@@ -1,11 +1,11 @@
from __future__ import unicode_literals

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.core.mail import send_mail
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _


class UserManager(BaseUserManager):
Expand Down Expand Up @@ -51,9 +51,8 @@ def get_short_name(self):
return self.email

def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
"""Sends an email to this User."""

send_mail(subject, message, from_email, [self.email], **kwargs)

@python_2_unicode_compatible
Expand Down
50 changes: 39 additions & 11 deletions authtools/urls.py
Expand Up @@ -4,16 +4,44 @@


urlpatterns = [
url(r'^login/$', views.LoginView.as_view(), name='login'),
url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
url(r'^password_change/$', views.PasswordChangeView.as_view(), name='password_change'),
url(r'^password_change/done/$', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
url(r'^password_reset/$', views.PasswordResetView.as_view(), name='password_reset'),
url(r'^password_reset/done/$', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
url(r'^reset/done/$', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
url(r'^reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.PasswordResetConfirmView.as_view()),
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
url(
r'^login/$',
views.LoginView.as_view(),
name='login'
),
url(
r'^logout/$'
, views.LogoutView.as_view(),
name='logout'
),
url(
r'^password_change/$',
views.PasswordChangeView.as_view(),
name='password_change'
),
url(
r'^password_change/done/$',
views.PasswordChangeDoneView.as_view(),
name='password_change_done'
),
url(
r'^password_reset/$',
views.PasswordResetView.as_view(),
name='password_reset'
),
url(
r'^password_reset/done/$',
views.PasswordResetDoneView.as_view(),
name='password_reset_done'
),
url(
r'^reset/done/$',
views.PasswordResetCompleteView.as_view(),
name='password_reset_complete'
),
url(
r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'),
name='password_reset_confirm'
),
]
34 changes: 12 additions & 22 deletions authtools/views.py
Expand Up @@ -14,10 +14,7 @@
from django.contrib import auth
from django.http import HttpResponseRedirect

try:
from django.contrib.sites.shortcuts import get_current_site
except ImportError:
from django.contrib.sites.models import get_current_site # Django < 1.7
from django.contrib.sites.shortcuts import get_current_site

try:
# django >= 1.10
Expand All @@ -40,16 +37,11 @@ class SuccessURLAllowedHostsMixin(object):
# skip since this was not available before django 1.11
pass

try:
from django.contrib.auth import update_session_auth_hash
except ImportError:
# Django < 1.7
def update_session_auth_hash(request, user):
pass
from django.contrib.auth import update_session_auth_hash

from django.shortcuts import redirect, resolve_url
from django.utils.functional import lazy
from django.utils.http import base36_to_int, is_safe_url
from django.utils.http import base36_to_int, is_safe_url, urlsafe_base64_decode
from django.utils import six
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
Expand Down Expand Up @@ -112,7 +104,12 @@ def get_next_url(self):
except AttributeError:
pass

url_is_safe = is_safe_url(redirect_to, allowed_hosts=allowed_hosts, require_https=self.request.is_secure())
url_is_safe = is_safe_url(
redirect_to,
allowed_hosts=allowed_hosts,
require_https=self.request.is_secure()
)

except TypeError:
# django < 1.11
url_is_safe = is_safe_url(redirect_to, host=host)
Expand Down Expand Up @@ -172,7 +169,8 @@ class AuthDecoratorsMixin(NeverCacheMixin, CsrfProtectMixin, SensitivePostParame
pass


class LoginView(AuthDecoratorsMixin, SuccessURLAllowedHostsMixin, WithCurrentSiteMixin, WithNextUrlMixin, FormView):
class LoginView(AuthDecoratorsMixin, SuccessURLAllowedHostsMixin,
WithCurrentSiteMixin, WithNextUrlMixin, FormView):
form_class = AuthenticationForm
authentication_form = None
template_name = 'registration/login.html'
Expand Down Expand Up @@ -351,17 +349,9 @@ def get_queryset(self):
return User._default_manager.all()

def get_user(self):
# django 1.5 uses uidb36, django 1.6 uses uidb64
uidb36 = self.kwargs.get('uidb36')
uidb64 = self.kwargs.get('uidb64')
assert bool(uidb36) ^ bool(uidb64)
try:
if uidb36:
uid = base36_to_int(uidb36)
else:
# urlsafe_base64_decode is not available in django 1.5
from django.utils.http import urlsafe_base64_decode
uid = urlsafe_base64_decode(uidb64)
uid = urlsafe_base64_decode(uidb64)
return self.get_queryset().get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return None
Expand Down
11 changes: 8 additions & 3 deletions setup.py
@@ -1,7 +1,8 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import os
import io
import os

from setuptools import setup, find_packages

__doc__ = ("Custom user model app for Django featuring email as username and"
" class-based views for authentication.")
Expand Down Expand Up @@ -36,8 +37,12 @@ def read(fname):
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
],
)
5 changes: 0 additions & 5 deletions tests/manage.py
Expand Up @@ -4,11 +4,6 @@
import warnings

import django
if django.VERSION[:2] == (1, 6):
# This is only necessary for Django 1.6
from django.contrib.auth.tests import custom_user
custom_user.AbstractUser._meta.local_many_to_many = []
custom_user.PermissionsMixin._meta.local_many_to_many = []

warnings.simplefilter('error')

Expand Down
10 changes: 6 additions & 4 deletions tox.ini
@@ -1,15 +1,17 @@
[tox]
envlist=
py{27,33}-dj18,
py{27,34}-dj19,
py{27,34}-dj110,
py{27,34}-dj111
py{27,33,35,36}-dj18,
py{27,34,35,36}-dj19,
py{27,34,35,36}-dj110,
py{27,34,35,36}-dj111

[testenv]
basepython=
py27: python2.7
py33: python3.3
py34: python3.4
py35: python3.5
py36: python3.6
commands=
/usr/bin/env
make test
Expand Down

0 comments on commit fa82068

Please sign in to comment.