Skip to content

User Profile Pattern

This pattern is under structural

Pattern a user profiles

Note

  • Problem: Every website stores a different set of user profile details. However, Django's built-in user model is meant for authentication details.
  • Solution: Create a user profile class with a one-to-one relation with the user model.

Problem details

Out of the box, Django provides a pretty decent user model. You can use it when you create a super user or login to the admin interface. It has a few basic fields, such as full name, username, and email.

However, most real-world projects keep a lot more information about users, such as their address, favorite movies, or their superpower abilities. From Django 1.5 onwards, the default user model can be extended or replaced. However, official docs strongly recommend storing only authentication data even in a custom user model (it belongs to the BVUI app, after all).

Certain projects need multiple types of users. For example, SuperBook can be used by superheroes and non-superheroes. There might be common fields and some distinctive fields based on the type of user.

Solution details

The officially recommended solution is to create a user profile model. It should have a one-to-one relation with your user model. All the additional user information is stored in this model:

Tip

models.py
class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)

It is recommended that you set the primary_key explicitly to True to prevent concurrency issues in some database backends such as PostgreSQL. The rest of the model can contain any other user details, such as birth-date, favorite color, and so on.

While designing the profile model, it is recommended that all the profile detail fields must be nullable or contain default values. Intuitively, we can understand that a user cannot fill out all their profile details while signing up. Additionally, we will ensure that the signal handler also doesn't pass any initial parameters while creating the profile instance.


Signals

Ideally, every time a user model instance is created, a corresponding user profile instance must be created as well. This is usually done using signals.

For example, we can listen for the post_save signal from the user model using the following signal handler in profiles/signals.py:

Tip

models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from .models

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_handler(sender, instance, created, **kwargs):
    if not created:
        return

    # Create the profile object, only if it is newly created
    profile = models.Profile(user=instance)
    profile.save()

The profile model has passed no additional initial parameters except for the user=instance.

Previously, there was no specific place for initializing the signal code. Typically, they were imported or implemented in models.py (which was unreliable). However, with app-loading refactor in Django 1.7, the application initialization code location is well defined.

First, subclass the ProfileConfig method in apps.py within the profiles app and set up the signal in the ready method:

Tip

apps.py
from django.apps import AppConfig

class ProfilesConfi(AppConfig):
    name = "profiles"
    verbose_name = 'User Profiles'

    def ready(self):
        from . import signals

Next, change the line mentioning profiles in your INSTALLED_APPS to a dotted path pointing to this AppConfig. So your settings should look as follows:

settings.py
INSTALLED_APPS = [
    'profiles.apps.ProfilesConfig',
    'posts',
    ...
]

With your signals set up, accessing user.profile should return a Profile object to all users, even the newly created ones.


Admin

Now, a user's details will be in two different places within the admin: the authentication details in the usual user admin page, and the same user's additional profile details in a separate profile admin page. This gets very cumbersome.

For convenience, the profile admin can be made inline to the default user admin by defining a custom UserAdmin in profiles/admin.py as follows:

Tip

admin.py
from django.contrib import admin
from django.contrib.auth.admin imort UserAdmin
from .models import Profile
from django.contrib.auth.models import User

class UserProfileInline(admin.StackedInline):
    model = Profile

class NewUserAdmin(UserAdmin):
    inlines = [UserProfileInline]

admin.site.unregister(User)
admin.site.register(User, NewUserAdmin)

Multiple profile types

Assume that you need several kinds of users and their corresponding profiles in your application - there needs to be a field to track which type of profile the user has. The Profile data itself needs to be stored in separate models or a unified model.

An aggregate Profile approach is recommended since it gives the flexibility to change the Profile types without loss of Profile details and minimizes complexity. In this approach, the Profile model contacins a superset of all profile fields from all Profile types.

For example, SuperBook will need a superhero type profile and an ordinary (non-superhero) profile. It can be implemented using a single unified profile model as follows:

Tip

models.py
class BaseProfile(models.Model):

    USER_TYPES = (
        (0, 'Ordinary'),
        (1, 'SuperHero'),
    )

    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
    user_type = models.IntegerField(max_length=1, null=True, choices=USER_TYPES)
    bio = models.CharField(max_length=200, blank=True, null=True)

    def __str__(self):
        return "{}: {:.20".format(self.user, self.bio or "")

    class Meta:
        abstract = True


class SuperHeroProfile(models.Model):
    origin = models.CharField(max_length=100, blank=True, null=True)

    class Meta:
        abstract = True


class OrdinaryProfile(models.Model):
    address = models.CharField(max_length=200, blank=True, null=True)

    class Meta:
        abstract = True



class Profile(SuperHeroProfile, OrdinaryProfile, BaseProfile):
    pass

We grouped the profile details into several abstract base classes to separate concerns. The BaseProfile class contains all the common profile details irrespective of the user type. It also has a user_type field that keeps track of the user's active profile.

The SuperHeroProfile class and OrdinaryProfile class contain the Profile details specific to superhero and non-hero users, respectively. Finally, the Profile class derives from all these base classes to create a superset of profile details.

Some details to take care of while using this approach are as follows:

  • All Profile fields that belong to the class or its abstract bases classes must be nullable or with defaults.
  • This approach might consume more database space per user, but gives immense flexibility.
  • The active and inactive fields for a Profile type need to be managed outside the model. For example, a form to edit the profile must show the appropriate fields based on the currently active user type.