In this article I will show you how to add a custom user model to your Django Project.

Why do you need a custom user model in your Django project?

Django is a powerful, batteries-included web framework.

It's strong side is built-in authentication, user model, permissions already taken care for you.

The standard user model might not well for your project.

In fact, in almost all projects I've seen there was a need to add some additional pieces of data to the existing user model. Be it social media links, profile picture, some security related fields.

Here comes a few problems:

  • You can't change the standard user model
  • It is close to impossible to change the user model to a custom one mid-project, a.k.a. when the data is already there

If you already have an existing project and you need additional pieces of data, the best option for you is to create an additional model with 1-to-1 relationship to the user model, e.g. UserProfile and add all the fields there. Of course, you need to populate the UserProfile table and make sure a new user profile is created for new users, for example using signals.

But if you haven't published your project yet you need to create a custom user model and set your Django project to use it right away. Then you have freedom to shape it as you want during the lifetime of your project.

In this article I will show you how to create a custom user model and enable it in your Django project.

Requirements

You need to have Docker installed, because we will be using it to run our app.

Also, we use this tutorial as a starting point for this article Django Docker Tutorial with Postgres

Please clone that repository to be able to follow this one step by step.

Source code

The code for this tutorial is available on GitHub: appliku/django-custom-user-model

Create a new app for our custom user model

If you have followed the previous article, you know that we are running commands within a Docker container.

To start a shell within a Docker container run at the root of the project:

docker compose run web bash

Now let's start our app for a custom user model usermodel:

python manage.py startapp usermodel

Go to project/settings.py and add usermodel to INSTALLED_APPS and specify the user model setting.

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "mainapp",
    "usermodel",
]

AUTH_USER_MODEL = "usermodel.User"

Open usermodel/models.py

Let's create our User model:

import uuid

from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.conf import settings
from usermodel.managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(
        db_collation="und-x-icu",
        max_length=255,
        unique=True,
        error_messages={
            'unique': _('A user with such email already exists')
        }
    )

    first_name = models.CharField(_('First Name'), max_length=50, blank=True)
    last_name = models.CharField(_('Last Name'), max_length=50, blank=True)
    is_staff = models.BooleanField(
        _('Staff Status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.')
    )
    is_active = models.BooleanField(
        _('Active'),
        default=True,
        help_text=_('Designates whether this user should be treated as active.')
    )

    is_email_confirmed = models.BooleanField(
        _('Email Confirmed'),
        default=False
    )
    date_joined = models.DateTimeField(
        _('Date Joined'),
        default=timezone.now
    )

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = [
        'first_name',
        'last_name',
    ]

    class Meta:
        verbose_name = _('User')
        verbose_name_plural = _('Users')

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        return f"{self.first_name} {self.last_name}"

    def get_short_name(self):
        return f"{self.first_name}"

    def email_user(self, subject, message, from_email=None, **kwargs):
        send_mail(subject, message, from_email, recipient_list=[self.email], **kwargs)

Let's go over this code:

  • We have defined a User model
  • the id field is a UUID field, not a sequential integer. If you'll need to show ID somewhere, people won't be able to estimate the number of users in your app, nor perform enumeration attacks
  • The email field is a case-insensitive EmailField with collation set to be used for Postgres. Use NOCASE for SQLite and utf8mb4_unicode_ci for MySQL
  • USERNAME_FIELD = 'email' makes the email field a username field. This means we won't have a separate username field anymore. Unless, you are doing some sort of a social network, I think it is more convenient.
  • We also define a custom manager objects = UserManager()

Create usermodel/managers.py and define the custom user model manager:

from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('The email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email=email, password=password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = True
        return self._create_user(email=email, password=password, **extra_fields)

We need a custom manager because Django's standard user creation methods have username as an argument and we don't use that field anymore.

Last step, open usermodel/admin.py file.

Define and register UserAdmin class.

from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from usermodel.models import User
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin


class UserAdmin(DefaultUserAdmin):
    fieldsets = (
        (None, {"fields": ("email", "password")}),
        (
            _("Permissions"),
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                    "user_permissions",
                ),
            },
        ),
        (
            _("Important dates"),
            {
                "fields": (
                    "last_login",
                    "date_joined",
                )
            },
        ),
        (_("User data"), {"fields": (("is_email_confirmed",),)}),
    )
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "password1", "password2"),
            },
        ),
    )
    list_display = ("email", "first_name", "last_name", "is_staff")
    search_fields = ("first_name", "last_name", "email")
    ordering = ("email",)


admin.site.register(User, UserAdmin)

Now create migrations and apply migrations. As always, in the Docker container.

python manage.py makemigrations
python manage.py migrate

The output should look like this:

app@299f5be8f005:/code$ python manage.py makemigrations
Migrations for 'usermodel':
  usermodel/migrations/0001_initial.py
    + Create model User
app@299f5be8f005:/code$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, usermodel
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying usermodel.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK
app@299f5be8f005:/code$

Automatically create a superuser

Using built-in Django user management command

Now we need to add a user.

The first option is to use python manage.py createsuperuser command.

Hint: you can do unattended superuser creation if you set environment variable with the password and a few arguments:

DJANGO_SUPERUSER_PASSWORD=somethingsupersecret123 python manage.py createsuperuser --email=admin@example.com --first_name=Admin --last_name=Super --noinput

Please note, that --first_name and --last_name are required here because of these lines in our model:

    REQUIRED_FIELDS = [
        'first_name',
        'last_name',
    ]

I am a huge fan of automation and that implies usage of idempotent commands, meaning those which you can run as many times as you want and get the same result.

We will make a custom management command for superuser creation.

Continue reading: Create a custom management command: Create super user for a custom user model