Django tips: extending the User model

An entry published by James Bennett on June 6, 2006, Part of the categories Django and Frameworks. 12 comments posted.

One of Django‘s great strengths is its built-in user and authentication system; the bundled application django.contrib.auth includes models for users, user groups and permissions and views for logging users in and out and changing and resetting their passwords. This is enough to cover the needs of a huge number of sites and, after the admin, the auth system is probably the most popular bundled application Django ships (or maybe the other way around, since the admin requires the auth system to be installed).

Because of the auth system’s popularity, though, one of the most common questions people ask about Django is “how do I extend the User model?” By default, users have a small but useful set of fields which take care of most common use cases, but there are plenty of times where it’d be extremely handy to be able to add just a few more to suit a particular application’s needs.

There are a few immediately obvious ways to do this:

  1. Modify the User model in place before installing the auth application. This is easy to do at first, but can lead to maintenance headaches when it comes time to upgrade Django. If you’re tracking trunk through Subversion that can be much more of an annoyance than it’s worth.
  2. Copy the auth application over into your own project and modify it to your needs. This avoids some of the maintenance troubles, but removes the utility of Django bundling an auth system in the first place. It can also cause compatibility problems with other applications which expect the User model to be in django.contrib.auth.
  3. Subclass the User model and add the fields you need. As I’m writing this, the option to subclass is only available in older releases of Django — model subclassing is being refactored in trunk right now — and even then, getting Django to use your subclassed User model instead of the original (via replaces_module, which was ugly and undocumented and was thankfully removed a while back) can involve quite a bit of work.
  4. Create an entirely new model with the extra fields you want, and relate it back to the built-in User model with a OneToOneField. This is by far the easiest method, and Django has a feature which makes this even better; we’ll see how that works in a minute.

Edit: I’ve seen a couple people mentioning that the method I describe below is “the best way until model subclassing is fixed in trunk”. Please be sure to read the comment in item 3 above about how, even when subclassing is working again, trying to do that will be much more difficult than it would seem — the “replaces_module” option has been removed, so there is no longer any way to tell Django to use your subclass in place of the built-in User class.

Let’s extend the User model

For sake of a simple example, let’s assume we want to add three extra fields for users: the URL of their website, their phone number and their home address. We begin by defining our new model; I’m going to call it UserProfile for reasons which will become apparent shortly, but feel free to call it whatever you like — the name of the model really doesn’t matter.

Updated January 30 2007: the Django book now officially recommends using a foreign key with “unique=True”. So do that instead of the OneToOne. I’ve updated the code snippet to reflect this.

Here’s the code:

from django.db import models
from django.contrib.auth.models import User
     
class UserProfile(models.Model):
    url = models.URLField()
    home_address = models.TextField()
    phone_numer = models.PhoneNumberField()
    user = models.ForeignKey(User, unique=True)

Once the model is defined, we can install the app it lives in and it’ll be immediately available. At this point we can take advantage of Django’s object-relational mapping; given a User, we can access their UserProfile, and given a UserProfile we can access the User. But there’s one more thing we can do that’ll get Django to help us out a bit more.

Quick edit because I get asked this a lot: read the next paragraph carefully. The value of the setting is not “appname.models.modelname”, it’s just “appname.modelname”. The reason is that Django is not using this to do a direct import; instead, it’s using an internal model-loading function which only wants the name of the app and the name of the model. Trying to do things like “appname.models.modelname” or “projectname.appname.models.modelname” in the AUTH_PROFILE_MODULE setting will cause Django to blow up with the dreaded “too many values to unpack” error, so make sure you’ve put “appname.modelname”, and nothing else, in the value of AUTH_PROFILE_MODULE.

All we have to do is open up the Django settings file and add a new setting called AUTH_PROFILE_MODULE; this should be of the form “appname.modelname”, where “appname” is the name of your application and “modelname” is the name of your new model. So if our application was called “myapp”, for example, we’d add this:

AUTH_PROFILE_MODULE = 'myapp.UserProfile'

There’s a method on the built-in User model called get_profile(), which keys off this setting; if AUTH_PROFILE_MODULE is defined, then calling get_profile() on any User will return the associated object from your custom class. So, for example, we could do something like this:

from django.contrib.auth.models import User
u = User.objects.get(pk=1) # Get the first user in the system
user_address = u.get_profile().home_address

And that’s why I chose to call the example model “UserProfile”; you don’t have to name it like that, but I think giving it a name that involves “profile” can be useful to help you remember what it’s for.

More than meets the eye

At first it might not seem like this offers any great advantage over Django’s normal API — which would let us do u.userprofile.home_address — but there are a couple important features here that aren’t present in the standard API access for a OneToOneField:

  1. It’s a completely consistent generic interface. Using the standard API in the example above means hard-coding u.userprofile all over the place; what happens if you later change the name of that model, or decide you need to reuse that code somewhere else? Using the AUTH_PROFILE_MODULE setting and get_profile() makes your code more robust and more portable. You can even use introspection of the object returned by get_profile() (which is automatically cached, by the way, to save you from having to hit your database too often) to make your code completely generic.
  2. It makes site-specific user customization insanely easy. If you’re using Django’s bundled ‘sites’ application to manage multiple sites which each have their own settings files, each one can use a different custom model tailored to its needs. And since the interface is consistent, you can write less code because each site only needs to know what to do with these objects, not how to fetch them.

Better code. More portable code. Less code. And it’s incredibly easy to set up. Things like this are why I love Django.

On June 6, 2006, Justin said:

Isn’t use of the OneToOneField currently discouraged?

On June 6, 2006, James Bennett said:

The official docs say that, yes, because whatever solution we end up with for model subclassing is probably going to change/somewhat replace one-to-one relationships.

But this is a situation where you probably wouldn’t want to switch over to subclassing regardless (because then we’re back in the hellish situation of how you tell Django that your subclass should be the “real” User class), so I wouldn’t worry about it. If you are concerned about this issue, though, a ForeignKey, properly secured to ensure that only one profile can ever be created for a given user, would work too.

On June 8, 2006, Dagur said:

This won’t work in the admin site will it?

On June 8, 2006, João Marcus said:

OneToOne didn’t work for me, so I used: models.ForeignKey(User, edit_inline=models.TABULAR, num_in_admin=1,min_num_in_admin=1, max_num_in_admin=1,num_extra_on_change=0)

On June 8, 2006, James Bennett said:

Dagur, if you mean “will this make all of the fields show up on the same page in the admin”, then no. It will work exactly the same as any other OneToOneField in the admin application.

On June 8, 2006, James Bennett said:

João, yeah, doing a ForeignKey will work, but as I said above you have to be careful to ensure that only one profile instance gets created per user (since a foreign key enables many-to-one relationships).

In which case, edit_inline will let you edit both the user and the associated profile in one admin page.

On June 15, 2006, Umbrae said:

Excellent post James,

You mentioned to Dagur that it will not make the fields show up on the same page in the admin.

A followup question to that - is that possible in an easy-to-follow fashion? Or is it pretty complex? I’m fairly sure its do-able, but how exactly to do it right is a different story.

On June 15, 2006, James Bennett said:

Doing it as a ForeignKey instead of a OneToOneField, and using the edit_inline attribute will cause your “extra” fields to show up on the same page in the admin as the main User object, but you will have to be careful to ensure that only one instance of your model ever gets associated with a given user; otherwise things will break.

On June 22, 2006, eas said:

I tried setting the UserProfile to edit_inline=True using the OneToOneField. Now I get a “UserProfile matching query does not exist” if I create a new User and save it without populating anything in the UserProfile section.

Is there something obvious I should be doing that I’m missing (i’m admittedly not particularly clueful as a web developer).

On June 22, 2006, James Bennett said:

OneToOneField imposes the restriction that there must be one object on each side of the relationship, so you can’t create a User without creating a corresponding UserProfile and vice-versa. I believe that when you don’t use edit_inline, it lets you create one side, but will immediately redirect and make you create the other upon saving.

On July 2, 2006, Joshua said:

heehee: ForeignKey(…, unique=True) …Django doesn’t complain and the SQL looks ok, so I guess it works!

Combining with above: models.ForeignKey(User, unique=True, edit_inline=models.TABULAR, num_in_admin=1,min_num_in_admin=1, max_num_in_admin=1,num_extra_on_change=0)

On February 9, 2007, Ran said:

Great tip, thanks ! The only issue, as others commented, is getting to edit these new fields inline in the admin page.

Trying as Joshua did above doesn’t work here. Fields do show up, with the correct values if already set, but django silently fails to update or create them..

Comments for this entry are closed. If you'd like to share your thoughts on this entry with me, please contact me directly.

ponybadge