mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-04-17 05:21:00 +00:00
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:
parent
4392680067
commit
c4feb6462f
9 changed files with 277 additions and 27 deletions
|
|
@ -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
|
||||
~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue