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
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
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
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:
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
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
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.