Django UserProfile Model

Will Vincent - Aug 28 - - Dev Community

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:

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) $
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Navigate to http://127.0.0.1:8000 in your web browser to see the Django welcome screen.

Django welcome page

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. It's connected to the post_save signal of the CustomUser model
  2. When a new CustomUser is created, this function creates a new UserProfile for that user
  3. It also saves the UserProfile every time the CustomUser 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Log into the admin at 127.0.0.1:8000/admin and you'll be redirected to the admin homepage.

Admin Home Page

If you click on "Users," you'll see that there is currently a single user.

Admin Users Page

Then click on "User Profiles" to see that model.

Admin UserProfile Page

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The new' age' field will be visible if you refresh the "Change User Profile" page.

Admin Userprofile Page with Age Field

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .