Add ability to override user creation and edit forms to allow custom user model

fields to be edited via Wagtail admin.
This commit is contained in:
nfletton 2016-03-29 12:36:54 -06:00 committed by Matt Westcott
parent 4392680067
commit c4feb6462f
9 changed files with 277 additions and 27 deletions

View file

@ -297,6 +297,113 @@ Case-Insensitive Tags
Tags are case-sensitive by default ('music' and 'Music' are treated as distinct tags). In many cases the reverse behaviour is preferable.
Custom User Edit Forms
----------------------
.. code-block:: python
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'
Allows the default ``UserEditForm`` class to be overridden with a custom form when
a custom user model is being used and extra fields are required in the user edit form.
.. code-block:: python
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'
Allows the default ``UserCreationForm`` class to be overridden with a custom form when
a custom user model is being used and extra fields are required in the user creation form.
.. code-block:: python
WAGTAIL_USER_CUSTOM_FIELDS = ['country']
A list of the extra custom fields to be appended to the default list.
Custom user forms example
^^^^^^^^^^^^^^^^^^^^^^^^^
This example shows how to add a text field and foreign key field to a custom user model
and configure Wagtail user forms to allow the fields values to be updated.
Create a custom user model. In this case we extend the ``AbstractUser`` class and add
two fields. The foreign key references another model (not shown).
.. code-block:: python
class User(AbstractUser):
country = models.CharField(verbose_name='country', max_length=255)
status = models.ForeignKey(MembershipStatus, on_delete=models.SET_NULL, null=True, default=1)
Add the app containing your user model to ``INSTALLED_APPS`` and set AUTH_USER_MODEL_ to reference
your model. In this example the app is called ``users`` and the model is ``User``
.. code-block:: python
AUTH_USER_MODEL = 'users.User'
Create your custom user create and edit forms in your app:
.. code-block:: python
from django import forms
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailusers.forms import UserEditForm, UserCreationForm
from users.models import MembershipStatus
class CustomUserEditForm(UserEditForm):
country = forms.CharField(required=True, label=_("Country"))
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
class CustomUserCreationForm(UserCreationForm):
country = forms.CharField(required=True, label=_("Country"))
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
Extend the Wagtail user create and edit templates. These extended template should be placed in a
template directory ``wagtailusers/users``.
Template create.html:
.. code-block:: python
{% extends "wagtailusers/users/create.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
Template edit.html:
.. code-block:: python
{% extends "wagtailusers/users/edit.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
The ``extra_fields`` block allows fields to be inserted below the last name field
in the default templates. Other block overriding options exist to allow appending
fields to the end or beginning of the existing fields, or to allow all the fields to
be redefined.
Add the wagtail settings to your project to reference the user form additions:
.. code-block:: python
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'
WAGTAIL_USER_CUSTOM_FIELDS = ['country', 'status']
.. _AUTH_USER_MODEL: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
URL Patterns
~~~~~~~~~~~~

View file

@ -34,6 +34,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True)),
('first_name', models.CharField(max_length=50, blank=True)),
('last_name', models.CharField(max_length=50, blank=True)),
('country', models.CharField(max_length=100, blank=True)),
(
'groups',
models.ManyToManyField(

View file

@ -40,6 +40,7 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
is_active = models.BooleanField(default=True)
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
country = models.CharField(max_length=100, blank=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']

View file

@ -160,3 +160,9 @@ if 'ELASTICSEARCH_URL' in os.environ:
WAGTAIL_SITE_NAME = "Test Site"
# Extra user field for custom user edit and create form tests. This setting
# needs to here because it is used at the module level of wagtailusers.forms
# when the module gets loaded. The decorator 'override_settings' does not work
# in this scenario.
WAGTAIL_USER_CUSTOM_FIELDS = ['country']

View file

@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
from itertools import groupby
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.db import transaction
@ -20,13 +21,18 @@ User = get_user_model()
# The standard fields each user model is expected to have, as a minimum.
standard_fields = set(['email', 'first_name', 'last_name', 'is_superuser', 'groups'])
# Custom fields
if hasattr(settings, 'WAGTAIL_USER_CUSTOM_FIELDS'):
custom_fields = set(settings.WAGTAIL_USER_CUSTOM_FIELDS)
else:
custom_fields = set()
class UsernameForm(forms.ModelForm):
"""
Intelligently sets up the username field if it is infact a username. If the
Intelligently sets up the username field if it is in fact a username. If the
User model has been swapped out, and the username field is an email or
something else, dont touch it.
something else, don't touch it.
"""
def __init__(self, *args, **kwargs):
super(UsernameForm, self).__init__(*args, **kwargs)
@ -78,7 +84,7 @@ class UserCreationForm(UsernameForm):
class Meta:
model = User
fields = set([User.USERNAME_FIELD]) | standard_fields
fields = set([User.USERNAME_FIELD]) | standard_fields | custom_fields
widgets = {
'groups': forms.CheckboxSelectMultiple
}
@ -150,7 +156,7 @@ class UserEditForm(UsernameForm):
class Meta:
model = User
fields = set([User.USERNAME_FIELD, "is_active"]) | standard_fields
fields = set([User.USERNAME_FIELD, "is_active"]) | standard_fields | custom_fields
widgets = {
'groups': forms.CheckboxSelectMultiple
}

View file

@ -17,14 +17,17 @@
{% csrf_token %}
<section id="account" class="active nice-padding">
<ul class="fields">
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% endblock fields %}
<li><a href="#roles" class="button lowpriority tab-toggle icon icon-arrow-right-after" />{% trans "Roles" %}</a></li>
</ul>

View file

@ -18,16 +18,18 @@
<section id="account" class="active nice-padding">
<ul class="fields">
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
{% block fields %}
{% if form.separate_username_field %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
{% endif %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
{% block extra_fields %}{% endblock extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
{% endblock fields %}
<li><input type="submit" value="{% trans 'Save' %}" class="button" /></li>
</ul>
</section>

View file

@ -1,16 +1,65 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test import TestCase, override_settings
from django.utils import six
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import (
Collection, GroupCollectionPermission, GroupPagePermission, Page)
from wagtail.wagtailusers.forms import UserCreationForm, UserEditForm
from wagtail.wagtailusers.models import UserProfile
from wagtail.wagtailusers.views.users import get_user_creation_form, get_user_edit_form
class CustomUserCreationForm(UserCreationForm):
country = forms.CharField(required=True, label="Country")
class CustomUserEditForm(UserEditForm):
country = forms.CharField(required=True, label="Country")
class TestUserFormHelpers(TestCase):
def test_get_user_edit_form_with_default_form(self):
user_form = get_user_edit_form()
self.assertIs(user_form, UserEditForm)
def test_get_user_creation_form_with_default_form(self):
user_form = get_user_creation_form()
self.assertIs(user_form, UserCreationForm)
@override_settings(
WAGTAIL_USER_CREATION_FORM='wagtail.wagtailusers.tests.CustomUserCreationForm'
)
def test_get_user_creation_form_with_custom_form(self):
user_form = get_user_creation_form()
self.assertIs(user_form, CustomUserCreationForm)
@override_settings(
WAGTAIL_USER_EDIT_FORM='wagtail.wagtailusers.tests.CustomUserEditForm'
)
def test_get_user_edit_form_with_custom_form(self):
user_form = get_user_edit_form()
self.assertIs(user_form, CustomUserEditForm)
@override_settings(
WAGTAIL_USER_CREATION_FORM='wagtail.wagtailusers.tests.CustomUserCreationFormDoesNotExist'
)
def test_get_user_creation_form_with_invalid_form(self):
self.assertRaises(ImproperlyConfigured, get_user_creation_form)
@override_settings(
WAGTAIL_USER_EDIT_FORM='wagtail.wagtailusers.tests.CustomUserEditFormDoesNotExist'
)
def test_get_user_edit_form_with_invalid_form(self):
self.assertRaises(ImproperlyConfigured, get_user_edit_form)
class TestUserIndexView(TestCase, WagtailTestUtils):
@ -85,6 +134,30 @@ class TestUserCreateView(TestCase, WagtailTestUtils):
self.assertEqual(users.count(), 1)
self.assertEqual(users.first().email, 'test@user.com')
@override_settings(
WAGTAIL_USER_CREATION_FORM='wagtail.wagtailusers.tests.CustomUserCreationForm',
WAGTAIL_USER_CUSTOM_FIELDS=['country'],
)
def test_create_with_custom_form(self):
response = self.post({
'username': "testuser",
'email': "test@user.com",
'first_name': "Test",
'last_name': "User",
'password1': "password",
'password2': "password",
'country': "testcountry",
})
# Should redirect back to index
self.assertRedirects(response, reverse('wagtailusers_users:index'))
# Check that the user was created
users = get_user_model().objects.filter(username='testuser')
self.assertEqual(users.count(), 1)
self.assertEqual(users.first().email, 'test@user.com')
self.assertEqual(users.first().country, 'testcountry')
def test_create_with_password_mismatch(self):
response = self.post({
'username': "testuser",
@ -149,6 +222,28 @@ class TestUserEditView(TestCase, WagtailTestUtils):
user = get_user_model().objects.get(pk=self.test_user.pk)
self.assertEqual(user.first_name, 'Edited')
@override_settings(
WAGTAIL_USER_EDIT_FORM='wagtail.wagtailusers.tests.CustomUserEditForm',
)
def test_edit_with_custom_form(self):
response = self.post({
'username': "testuser",
'email': "test@user.com",
'first_name': "Edited",
'last_name': "User",
'password1': "password",
'password2': "password",
'country': "testcountry",
})
# Should redirect back to index
self.assertRedirects(response, reverse('wagtailusers_users:index'))
# Check that the user was edited
user = get_user_model().objects.get(pk=self.test_user.pk)
self.assertEqual(user.first_name, 'Edited')
self.assertEqual(user.country, 'testcountry')
def test_edit_validation_error(self):
# Leave "username" field blank. This should give a validation error
response = self.post({

View file

@ -1,9 +1,12 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.module_loading import import_string
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
@ -24,6 +27,32 @@ change_user_perm = "{0}.change_{1}".format(AUTH_USER_APP_LABEL, AUTH_USER_MODEL_
delete_user_perm = "{0}.delete_{1}".format(AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME.lower())
def get_custom_user_form(form_setting):
try:
return import_string(getattr(settings, form_setting))
except ImportError:
raise ImproperlyConfigured(
"%s refers to a form '%s' that is not available" %
(form_setting, getattr(settings, form_setting))
)
def get_user_creation_form():
form_setting = 'WAGTAIL_USER_CREATION_FORM'
if hasattr(settings, form_setting):
return get_custom_user_form(form_setting)
else:
return UserCreationForm
def get_user_edit_form():
form_setting = 'WAGTAIL_USER_EDIT_FORM'
if hasattr(settings, form_setting):
return get_custom_user_form(form_setting)
else:
return UserEditForm
@any_permission_required(add_user_perm, change_user_perm, delete_user_perm)
@vary_on_headers('X-Requested-With')
def index(request):
@ -91,7 +120,7 @@ def index(request):
@permission_required(add_user_perm)
def create(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
form = get_user_creation_form()(request.POST)
if form.is_valid():
user = form.save()
messages.success(request, _("User '{0}' created.").format(user), buttons=[
@ -101,7 +130,7 @@ def create(request):
else:
messages.error(request, _("The user could not be created due to errors."))
else:
form = UserCreationForm()
form = get_user_creation_form()()
return render(request, 'wagtailusers/users/create.html', {
'form': form,
@ -112,7 +141,7 @@ def create(request):
def edit(request, user_id):
user = get_object_or_404(User, pk=user_id)
if request.method == 'POST':
form = UserEditForm(request.POST, instance=user)
form = get_user_edit_form()(request.POST, instance=user)
if form.is_valid():
user = form.save()
messages.success(request, _("User '{0}' updated.").format(user), buttons=[
@ -122,7 +151,7 @@ def edit(request, user_id):
else:
messages.error(request, _("The user could not be saved due to errors."))
else:
form = UserEditForm(instance=user)
form = get_user_edit_form()(instance=user)
return render(request, 'wagtailusers/users/edit.html', {
'user': user,