Every website needs a way to handle user authentication and logic about users. Django comes with a built-in User model as well as views and URLs for login, log out, password change, and password reset. You can see how to implement it all in this tutorial.
But after over a decade of experience working with Django, I've come around to the idea that there is, in fact, one ideal way to start 99% of all new Django projects, and it is as follows:
- include a custom user model but don't add any fields to it
- add a UserProfile model
In this tutorial, I'll demonstrate how to implement these two key features, which will set you up on the right path for your next Django project.
Django Set Up
Let's quickly go through the steps to create a new Django project. To begin, create a new virtual environment and activate it.
# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $
# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $
Then, install Django and use runserver
to confirm that it works properly.
(.venv) $ python -m pip install django~=5.1.0
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py runserver
Navigate to http://127.0.0.1:8000
in your web browser to see the Django welcome screen.
Note that we did not run migrate
to configure our database. It's important to wait until after we've created our new custom user model before doing so.
Custom User Model
I'm giving a talk at this year's DjangoCon US on the topic of User
, but the short version is that if we could start over today, there are several changes to make. The obvious issues are that the default fields are now hopelessly out of date:
- username is required but rarely used these days
- email isn't unique or required
- first name/last name is Western-centric and not global
- no ability to add additional fields
There was an extensive discussion back in 2012 around improving contrib.auth that led to the addition of an AUTH_USER_MODEL
configuration in settings.py
.
Today, you'll find conflicting advice on what to do if you search the Internet. The Django docs highly recommend using a custom user model, but there are plenty of people who favor using UserProfile
instead to separate authentication logic from general user information.
I think the answer is to use both: add a custom user model for maximum flexibility in the future and use a UserProfile
model for any additional logic.
So, how do you add a custom user model? A complete discussion and tutorial is available here, but the bullet notes are as follows.
Create a new app, typically called users
or accounts
.
(.venv) $ python manage.py startapp users
Update the settings.py
file so that Django knows about this new app and set AUTH_USER_MODEL
to the name of our not-yet-created custom user model, which will be called CustomUser
.
# django_project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"users", # new
]
...
AUTH_USER_MODEL = "users.CustomUser" # new
In the users/models.py
file extend AbstractUser to create a new user model called CustomUser
.
# users/models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
pass
def __str__(self):
return self.email
There are two forms closely tied to User
whenever it needs to be rewritten or extended: UserCreationForm and UserChangeForm.
Create a new users/forms.py
file with the following code:
# users/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ("username", "email")
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ("username", "email")
The final step is to update the admin since it is tightly coupled to the default User model. We can update users/admin.py
so that the admin uses our new forms and the CustomUser
model.
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = [
"email",
"username",
]
admin.site.register(CustomUser, CustomUserAdmin)
Only now should we run migrate
for the first time to initialize the local database and apply our work. Create a new migration file for users
and then apply all changes using migrate
.
(.venv) $ python manage.py makemigrations users
Migrations for 'users':
users/migrations/0001_initial.py
+ Create model CustomUser
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, users
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 users.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
User Profile
Adding a User Profile model takes just a few steps. First, we'll import models
at the top of the users/models.py
file and create a UserProfile
model with an OneToOne
relationship with CustomUser
. To control how it is displayed in the admin, add a __str__
method that returns the user and updates the verbose name to display either "User Profile" or "User Profiles."
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models # new
class CustomUser(AbstractUser):
pass
def __str__(self):
return self.email
class UserProfile(models.Model): # new
user = models.OneToOneField(
"users.CustomUser",
on_delete=models.CASCADE,
)
# add additional fields for UserProfile
def __str__(self):
return self.user
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
We need a way to automatically create a UserProfile
whenever a new CustomUser
is created. For this task, we'll use signals, a way to decouple when an action that occurs somewhere else in the application triggers an action somewhere else.
At the top of the file, import the post_save
signal, sent after a model's save()
method is called. Also, import receiver
, a decorator that can be used to register a function as a receiver for the post_save
signal. We'll add a function, create_or_update_user_profile
that does three things:
- It's connected to the
post_save
signal of theCustomUser
model - When a new
CustomUser
is created, this function creates a newUserProfile
for that user - It also saves the
UserProfile
every time theCustomUser
is saved, which allows updates
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.signals import post_save # new
from django.dispatch import receiver # new
class CustomUser(AbstractUser):
pass
def __str__(self):
return self.email
class UserProfile(models.Model):
user = models.OneToOneField(
"users.CustomUser",
on_delete=models.CASCADE,
)
# add additional fields for UserProfile
def __str__(self):
return f"Profile for {self.user.email}"
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
@receiver(post_save, sender=CustomUser) # new
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
instance.userprofile.save()
The nice thing about this signal is that it works retroactively for any users who don't already have a user profile. The next time we save a user, for example, by updating the admin, a UserProfile
model is created if it doesn't already exist.
Our logic is now complete! We have a custom user model that provides immense flexibility in our project going forward and a user profile that can be used right away to store information about the user unrelated to authentication, such as payment information, access to products, and so on.
Let's update users/admin.py
so that the admin properly displays the UserProfile
model. At the top, import the UserProfile
model and create a new class called UserProfileInline
.
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser, UserProfile # new
class UserProfileInline(admin.StackedInline): # new
model = UserProfile
can_delete = False
verbose_name_plural = "User Profile"
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ["email", "username"]
inlines = [UserProfileInline] # new
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(UserProfile) # new
Create a new migrations file to store the UserProfile
changes and then run migrate
to apply them to the database.
(.venv) $ python manage.py makemigrations users
Migrations for 'users':
users/migrations/0002_userprofile.py
+ Create model UserProfile
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
Applying users.0002_userprofile... OK
Signals are a powerful feature in Django, but they're best used sparingly. Signals create implicit connections between different parts of your code: when a signal is triggered, it can be difficult to trace exactly what code will run as a result, especially in large projects. This means that debugging and testing signals can become a challenge. That said, signals have their place, especially when it is necessary to decouple applications and trigger actions in one part of the code based on changes in another.
Admin
Now let's go into the admin to see how our CustomUser
and UserProfile
models are displayed. Create a superuser account and start up the server again with runserver
.
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver
Log into the admin at 127.0.0.1:8000/admin
and you'll be redirected to the admin homepage.
If you click on "Users," you'll see that there is currently a single user.
Then click on "User Profiles" to see that model.
Let's add a field to UserProfile
to see this all in action. Update users/models.py
with an age
field now included.
# users/models.py
...
class UserProfile(models.Model):
user = models.OneToOneField(
"users.CustomUser",
on_delete=models.CASCADE,
)
age = models.PositiveIntegerField(null=True, blank=True) # new
Add a new migrations file and apply it to the database. Then start up the server again with runserver
.
(.venv) $ python manage.py makemigrations users
Migrations for 'users':
users/migrations/0003_userprofile_age.py
+ Add field age to userprofile
(.venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
Applying users.0003_userprofile_age... OK
(.venv) $ python manage.py runserver
The new' age' field will be visible if you refresh the "Change User Profile" page.
Conclusion
If you want more handholding on setting up a new Django project properly, I recommend one of the Courses on this site, which will go through all the steps in more detail.
Top comments (0)