It's a good idea to define a custom user model in Django. In this tutorial we will add a number of fields such as date of birth, address, phone number to the model. We will also make all these fields editable in the Django and Wagtail admin.
Let's have a look at admin first. To do that, we need a superuser, so create one, if you haven't done so already:
python3 manage.py createsuperuser
Create a username and a password and provide an email address when prompted.
In a Wagtail project the traditional Django admin is at http://127.0.0.1:8000/django-admin (without Wagtail it is at …/admin). If you log in there, our custom user model is not there. To identify it in your admin, put the following in the admin.py
file of your project:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = UserCreationForm
form = UserChangeForm
model = CustomUser
list_display = ['pk', 'email', 'username', 'first_name', 'last_name']
add_fieldsets = UserAdmin.add_fieldsets + (
(None, {'fields': ('email', 'first_name', 'last_name',)}),
)
fieldsets = UserAdmin.fieldsets
admin.site.register(CustomUser, CustomUserAdmin)
UserCreationForm
and UserChangeForm
allow us to create and edit users in our admin. The attribute list_display
controls which fields are displayed in the admin user overview and fieldsets
which fields can be created or edited.
Now check http://127.0.0.1:8000/django-admin: the custom user is visible in admin and users can be created and edited.
Up until now we still have nothing more than the standard Django user fields. It's time to add some extra fields in models.py
of one of our apps (called userauth
). I'll add all the fields at once and explain them below.
from django.contrib.auth.models import AbstractUser
from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
class CustomUser(AbstractUser):
display_name = models.CharField(verbose_name=_("Display name"), max_length=30, help_text=_("Will be shown e.g. when commenting"))
date_of_birth = models.DateField(verbose_name=_("Date of birth"), blank=True, null=True)
address1 = models.CharField(verbose_name=_("Address line 1"), max_length=1024, blank=True, null=True)
address2 = models.CharField(verbose_name=_("Address line 2"), max_length=1024, blank=True, null=True)
zip_code = models.CharField(verbose_name=_("Postal Code"), max_length=12, blank=True, null=True)
city = models.CharField(verbose_name=_("City"), max_length=1024, blank=True, null=True)
country = CountryField(blank=True, null=True)
phone_regex = RegexValidator(regex=r"^\+(?:[0-9]●?){6,14}[0-9]$", message=_("Enter a valid international mobile phone number starting with +(country code)"))
mobile_phone = models.CharField(validators=[phone_regex], verbose_name=_("Mobile phone"), max_length=17, blank=True, null=True)
additional_information = models.CharField(verbose_name=_("Additional information"), max_length=4096, blank=True, null=True)
photo = models.ImageField(verbose_name=_("Photo"), upload_to='photos/', default='photos/default-user-avatar.png')
class Meta:
ordering = ['last_name']
def __str__(self):
return f"{self.username}: {self.first_name} {self.last_name}"
The display name speaks for itself. We have added the possibility of translating the verbose name, by using gettext_lazy
. When you are sure you will only use one language, you can of course omit this. In this tutorial we will consistently add the translation functionality for all 'human readable' strings. The date of birth field is not mandatory, as is indicated by the parameters blank=True, null=True
. Address fields speak for themselves, except the CountryField
. This is not standard Django, it is imported from a package called django-countries
. This allows us to choose a country from a list of all countries in the world, instead of just typing the country in a textfield. We install the package with
pip3 install django-countries
Add django_countries
to the list of INSTALLED_APPS
in settings/base.py
and add it to requirements.txt
as well.
The field phone_regex
is a RegexValidator
with a pattern that fits international phone numbers. It will of course not guarantee that the phone number is valid, but it does provide some protection against erroneous input. The regex field is fed into the mobile phone field.
The standard Django ImageField
puts uploaded images in the directory indicated by the upload_to
field, prefixed by the MEDIA_ROOT
parameter in settings. Since our media root is our base directory plus media
, uploaded images will be placed in /media/photos/
. We have also added a default image, in the directory photos/
. Because of the way Django handles files, it will prefix the MEDIA_ROOT
to the path for the default image. That means we have to place our default image in the same directory /media/photos/
. Putting files manually in MEDIA_ROOT
is an exception, since it is normally only used for uploading files; there are other ways of providing a default image if you want to avoid that.
We have also defined an ordering of our users (by last name) and a string representation, useful when viewing users in admin. The fields username
, last_name
and first_name
are available to us through the model AbstractUser
.
Our user model is complete, now we need to add the extra fields to our admin. First, in the CustomUserAdmin
in admin.py
add the extra fields to add_fieldsets
and fieldsets
:
add_fieldsets = UserAdmin.add_fieldsets + (
(None, {'fields': ('email', 'first_name', 'last_name', 'display_name', 'date_of_birth', 'address1', 'address2', 'zip_code', 'city', 'country', 'mobile_phone', 'additional_information', 'photo',)}),
)
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('display_name', 'date_of_birth', 'address1', 'address2', 'zip_code', 'city', 'country', 'mobile_phone', 'additional_information', 'photo',)}),
)
We have to migrate our database to incorporate all new fields:
python3 manage.py makemigrations
python3 manage.py migrate
When we now run the server and go to Django admin, we see that all the fields are there!
Time to look at the Wagtail admin at http://127.0.0.1:8000/admin. Only the standard fields are there unfortunately, so we have to add the extra fields. Wagtail also has a UserCreationForm
and a UserEditForm
(slightly different name here), so we add the following to forms.py
:
from .models import CustomUser
from wagtail.users.forms import UserCreationForm, UserEditForm
class WagtailUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = CustomUser
widgets = {'date_of_birth': forms.DateInput(attrs={'type':'date'})}
class WagtailUserEditForm(UserEditForm):
class Meta(UserEditForm.Meta):
model = CustomUser
widgets = {'date_of_birth': forms.DateInput(attrs={'type':'date'})}
Note that we have added a widget DateInput
from the forms
library of Django, which allows us to pick dates from a calendar.
We are not done yet. Wagtail needs templates create.html
and edit.html
to use these forms and expects them in a certain place. We will follow Django's customary template directory structure and put all templates for the app userauth
in the directory userauth/templates/userauth
.
To allow Django to find these templates, add the following path to the TEMPLATES
variable in settings/base.py
:
os.path.join(BASE_DIR, 'userauth/templates/userauth/'),
In that directory add two more subdirectories wagtailusers/users
, and then add the following two templates:
{% extends "wagtailusers/users/create.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.display_name %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.date_of_birth %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.address1 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.address2 %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.zip_code %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.city %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.mobile_phone %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.additional_information %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.photo %}
{% endblock extra_fields %}
Same in edit.html
, except for the first line:
{% extends "wagtailusers/users/edit.html" %}
Finally we have to add the following parameters in our settings/base.py
:
WAGTAIL_USER_CREATION_FORM = 'userauth.forms.WagtailUserCreationForm'
WAGTAIL_USER_EDIT_FORM = 'userauth.forms.WagtailUserEditForm'
WAGTAIL_USER_CUSTOM_FIELDS = ['display_name', 'date_of_birth', 'address1', 'address2', 'zip_code', 'city', 'country', 'mobile_phone', 'additional_information', 'photo',]
Now run the server and go to http://127.0.0.1:8000/admin to check that all new fields are there!
Read more about logging in and signing up custom users and other authentication processes.
Comment on this article (sign in first or confirm by name and email below)