Adding extra fields to a Django custom user model

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.

June 30, 2020, 11:27 a.m.
Themes: Custom User Model

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 createsuperuser

Create a username and a password and provide an email address when prompted.

In a Wagtail project the traditional Django admin is at (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 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, 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 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 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/ 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 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 makemigrations
python3 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 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

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/

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 %}
    {% include "wagtailadmin/shared/field_as_li.html" with %}
    {% 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 %}
{% 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/

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 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)