From e2b6e22f298eb986d74d28b8d9906f37f5ff8eb8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 23 Sep 2012 17:53:05 +0800 Subject: [PATCH] Modifications to the handling and docs for auth forms. --- .../contrib/admin/templates/admin/login.html | 2 +- django/contrib/auth/fixtures/custom_user.json | 14 +++++ django/contrib/auth/forms.py | 15 +++-- django/contrib/auth/tests/custom_user.py | 5 +- django/contrib/auth/tests/views.py | 23 +++++++ django/contrib/auth/views.py | 8 +-- docs/topics/auth.txt | 61 ++++++++++++++++--- 7 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 django/contrib/auth/fixtures/custom_user.json diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 06fe4c8160..4690363891 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -30,7 +30,7 @@
{% csrf_token %}
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} - {{ form.username }} + {{ form.username }}
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} diff --git a/django/contrib/auth/fixtures/custom_user.json b/django/contrib/auth/fixtures/custom_user.json new file mode 100644 index 0000000000..770bea6541 --- /dev/null +++ b/django/contrib/auth/fixtures/custom_user.json @@ -0,0 +1,14 @@ +[ + { + "pk": "1", + "model": "auth.customuser", + "fields": { + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "last_login": "2006-12-17 07:03:31", + "email": "staffmember@example.com", + "is_active": true, + "is_admin": false, + "date_of_birth": "1976-11-08" + } + } +] \ No newline at end of file diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 08488237c7..a430f042e9 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict from django.utils.html import format_html, format_html_join from django.utils.http import int_to_base36 from django.utils.safestring import mark_safe +from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.models import User from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.tokens import default_token_generator @@ -135,7 +136,7 @@ class AuthenticationForm(forms.Form): Base class for authenticating users. Extend this to get a form that accepts username/password logins. """ - username = forms.CharField(label=_("Username"), max_length=30) + username = forms.CharField(max_length=30) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) error_messages = { @@ -157,6 +158,11 @@ class AuthenticationForm(forms.Form): self.user_cache = None super(AuthenticationForm, self).__init__(*args, **kwargs) + # Set the label for the "username" field. + UserModel = get_user_model() + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + self.fields['username'].label = capfirst(username_field.verbose_name) + def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') @@ -198,9 +204,10 @@ class PasswordResetForm(forms.Form): """ Validates that an active user exists with the given email address. """ + UserModel = get_user_model() email = self.cleaned_data["email"] - self.users_cache = User.objects.filter(email__iexact=email, - is_active=True) + self.users_cache = UserModel.objects.filter(email__iexact=email, + is_active=True) if not len(self.users_cache): raise forms.ValidationError(self.error_messages['unknown']) if any((user.password == UNUSABLE_PASSWORD) diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 775a4fed24..3e7fa097b5 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -32,6 +32,7 @@ class CustomUserManager(BaseUserManager): class CustomUser(AbstractBaseUser): email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) date_of_birth = models.DateField() @@ -72,7 +73,3 @@ class CustomUser(AbstractBaseUser): @property def is_staff(self): return self.is_admin - - @property - def is_active(self): - return True diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index d7d3e441a9..5727dc289f 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -175,6 +175,29 @@ class PasswordResetTest(AuthViewsTestCase): self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserPasswordResetTest(AuthViewsTestCase): + fixtures = ['custom_user.json'] + + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertTrue(urlmatch is not None, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid_custom_user(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + @skipIfCustomUser class ChangePasswordTest(AuthViewsTestCase): diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f75f0ef7e4..747b5c0991 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect # Avoid shadowing the login() and logout() views below. -from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout +from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm -from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -201,13 +200,14 @@ def password_reset_confirm(request, uidb36=None, token=None, View that checks the hash in a password reset link and presents a form for entering a new password. """ + UserModel = get_user_model() assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') try: uid_int = base36_to_int(uidb36) - user = User.objects.get(id=uid_int) - except (ValueError, OverflowError, User.DoesNotExist): + user = UserModel.objects.get(id=uid_int) + except (ValueError, OverflowError, UserModel.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 1f95358b1f..a767b5a93f 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1357,6 +1357,9 @@ Helper functions URL to redirect to after log out. Overrides ``next`` if the given ``GET`` parameter is passed. + +.. _built-in-auth-forms: + Built-in forms -------------- @@ -1915,6 +1918,51 @@ model and you just want to add some additional profile information, you can simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your custom profile fields. +Custom users and the built-in auth forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you may expect, built-in Django's :ref:`forms <_built-in-auth-forms>` +and :ref:`views ` make certain assumptions about +the user model that they are working with. + +If your user model doesn't follow the same assumptions, it may be necessary to define +a replacement form, and pass that form in as part of the configuration of the +auth views. + +* :class:`~django.contrib.auth.forms.UserCreationForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.UserChangeForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.AuthenticationForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, + and will adapt to use the field defined in `USERNAME_FIELD`. + +* :class:`~django.contrib.auth.forms.PasswordResetForm` + + Assumes that the user model has an integer primary key, has a field named + `email` that can be used to identify the user, and a boolean field + named `is_active` to prevent password resets for inactive users. + +* :class:`~django.contrib.auth.forms.SetPasswordForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.PasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + + Custom users and django.contrib.admin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1960,7 +2008,11 @@ A full example Here is an example of a full models.py for an admin-compliant custom user app. This user model uses an email address as the username, and has a required date of birth; it provides no permission checking, beyond a simple -`admin` flag on the user account:: +`admin` flag on the user account. This model would be compatible with all +the built-in auth forms and views, except for the User creation forms. + +This code would all live in a ``models.py`` file for a custom +authentication app:: from django.db import models from django.contrib.auth.models import ( @@ -2006,6 +2058,7 @@ required date of birth; it provides no permission checking, beyond a simple max_length=255 ) date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) objects = MyUserManager() @@ -2040,12 +2093,6 @@ required date of birth; it provides no permission checking, beyond a simple # Simplest possible answer: All admins are staff return self.is_admin - @property - def is_active(self): - "Is the user account currently active?" - # Simplest possible answer: User is always active - return True - .. _authentication-backends: