Creating a robust authentication system is often the first step when building a Django application. While Django’s default User model works for many projects, real‑world apps frequently need extra fields, alternative login methods, or stricter validation. That’s where a custom user model shines. In this comprehensive guide we’ll walk you through why you should replace the default model, how to design and implement a custom user model, and the best practices that keep your code clean, secure, and future‑proof.
Why Use a Custom User Model?
- Flexibility: Add fields such as
date_of_birth,profile_picture, oris_premiumwithout creating a separate profile model. - Unified authentication: Use email, phone number, or a third‑party identifier as the primary login credential.
- Scalability: Avoid the pain of migrating data later; switching after the first migration can be complex and risky.
- Compliance: Meet GDPR or industry‑specific requirements by storing only the data you truly need.
When to Create a Custom User Model
Ideally, you should decide on a custom user model before running the first migration. If you’ve already created tables with the default model, you can still migrate, but the process is more involved and may require data export/import.
Step‑by‑Step Guide to Building a Custom User Model
1. Plan Your Fields and Authentication Method
Start by listing the attributes your users need. Common choices include:
email (unique)
first_name
last_name
date_of_birth
is_staff
is_active
is_premium
If you want users to log in with their email instead of a username, you’ll need to set USERNAME_FIELD = 'email' later in the model.
2. Create a New App (Optional but Recommended)
Keeping authentication logic isolated makes maintenance easier.
python manage.py startapp accounts
3. Define the Custom User Model
In accounts/models.py, subclass AbstractBaseUser and PermissionsMixin. The latter provides group and permission handling out of the box.
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils import timezone
from django.core.mail import send_mail
from .managers import CustomUserManager
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField('email address', unique=True)
first_name = models.CharField('first name', max_length=30, blank=True)
last_name = models.CharField('last name', max_length=30, blank=True)
date_of_birth = models.DateField('date of birth', null=True, 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.'
)
date_joined = models.DateTimeField('date joined', default=timezone.now)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] # Email & password are required by default.
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
def __str__(self):
return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
def get_short_name(self):
return self.first_name or self.email
def email_user(self, subject, message, from_email=None, **kwargs):
send_mail(subject, message, from_email, [self.email], **kwargs)
4. Implement a Custom User Manager
The manager handles user creation, especially for the createsuperuser command.
from django.contrib.auth.base_user import BaseUserManager
class CustomUserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError('The Email field 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, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
5. Tell Django to Use the New Model
Add the following line to your project’s settings.py:
AUTH_USER_MODEL = 'accounts.CustomUser'
Make sure the accounts app is listed before django.contrib.auth in INSTALLED_APPS to avoid circular import issues.
6. Create and Apply Migrations
python manage.py makemigrations accounts
python manage.py migrate
If you’re starting a new project, Django will create the auth_user table based on your custom model, and everything will line up automatically.
7. Update Admin Interface
Register the custom user model with the admin site so you can manage users easily.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
from .forms import CustomUserCreationForm, CustomUserChangeForm
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ('email', 'first_name', 'last_name', 'is_staff', 'is_active')
list_filter = ('is_staff', 'is_active')
ordering = ('email',)
search_fields = ('email', 'first_name', 'last_name')
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'date_of_birth')}),
('Permissions', {'fields': ('is_staff', 'is_active', 'is_superuser', 'groups', 'user_permissions')}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')}
),
)
admin.site.register(CustomUser, CustomUserAdmin)
8. Create Custom Forms for Admin and Registration
These forms ensure proper validation for the new fields.
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = ('email', 'first_name', 'last_name', 'date_of_birth')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('email', 'first_name', 'last_name', 'date_of_birth', 'is_active', 'is_staff')
Testing Your Custom User Model
Write a few simple tests to verify that user creation, authentication, and admin integration work as expected.
from django.test import TestCase
from django.contrib.auth import get_user_model
class UserModelTests(TestCase):
def test_create_user_with_email(self):
User = get_user_model()
user = User.objects.create_user(email='test@example.com', password='test123')
self.assertEqual(user.email, 'test@example.com')
self.assertTrue(user.check_password('test123'))
self.assertFalse(user.is_staff)
def test_create_superuser(self):
User = get_user_model()
admin = User.objects.create_superuser(email='admin@example.com', password='admin123')
self.assertTrue(admin.is_staff)
self.assertTrue(admin.is_superuser)
Common Pitfalls & How to Avoid Them
- Changing the model after migrations: If you need to add a field later, use
makemigrationsas usual, but avoid alteringUSERNAME_FIELDafter the initial migration. - Forgot to update
AUTH_USER_MODEL: Django will still reference the defaultauth.User, leading to obscure errors. - Using
django.contrib.auth.forms.AuthenticationFormwithout customization: It expects ausernamefield. Replace it with a custom form that authenticates via email. - Hard‑coding user lookups: Always use
get_user_model()instead of importing the model directly.
Best Practices for a Clean, Maintainable Custom User Model
- Define the model early: Decide on the user schema before any data exists.
- Keep authentication logic separate: Use a dedicated
accountsapp for models, managers, forms, and views. - Leverage Django’s built‑in mixins:
PermissionsMixinadds groups and permission handling without extra code. -
Comments
Leave a Reply