Extending Django's User Model with OneToOneField
This post is the second in a two-part series on my experience with adding a user registration system to a simple demo app built in Django. In my first post, I talk about how Django's built-in authentication system can do some of the heavy lifting for your registration setup. In this post, I'll walk you through how we tied our data models and authentication together by extending Django's User
model.
You may recall from the first post that there wasn't a pure out-of-the-box solution for user account registration, even in Django's robust built-in authentication. This is especially true if your app users don't look or behave exactly like the User
model in Django. In our case, I want to store custom attributes that don't come with the User
model by default. I also want our players to provide those custom values upon registration. So I started to dig a bit deeper into customizing authentication.
Extending the User
model
For customizing authentication, we have three options:
- Create a proxy model based on the Django
User
model. - Use a
OneToOneField
that links theUser
model to another model that contains additional fields (this can also be referred to as a User Profile). - Create a custom
User
model.
You can say that the first two options "extend" the User
model because they do still use the built-in model. User data is still stored in the same database tables. On the other hand, creating a custom User
model replaces the provided model entirely, and it also means you'd create new database tables to store user data.
As to whether we should completely replace the Person
model: for players (Person
s) to log in to the app, they should be User
s as well, but the third option seemed like more than we needed. The proxy model is suitable for when you need to customize only the behavior of the User
model—for instance, if you want to create a new method. Since our requirement is for the app to also be able to save some additional fields for players, I went with the User Profile option.
OneToOneField
or ForeignKey
?
The first thing we need to do at this point is change our data model. My colleague Steve Pousty describes in his blog post how we went from designing the database to setting up our Django models. This time, we completely threw out our database (which was fine since it was pretty early on in development), and then exported our model again to PostgreSQL and used the inspectdb
command to update models.py
.
We removed the username, password, and email fields. Since we're extending the existing User
model, this means we're letting Django handle these "standard" fields, while we use the Person
model to store the extra attributes that don't already come with User
. I should note that at this point we still had an autogenerated primary key for the underlying person table, and I'll talk about what we ended up doing on the database side.
How do you actually implement the Profile model? This primarily involves using the OneToOneField
option for the model that links back to User
.
Now, Django also has the ForeignKey
option to represent a relationship between two models. If I were to use this option with our Person
model, I should theoretically also expect to be able to query the User
attributes from Person
. However, you'll note in the docs that Django specifically states that it is used for many-to-one relationships. I know that I don't want a many-to-one relationship from Person
to User
, so this was a pretty easy decision to make.
With that said, I did find some examples of ForeignKey
being used with the User
model. They appear to be discussed within the context of using an entirely custom User
model. As a disclaimer, the official Django docs do also mention that it's recommended to set up a custom user model for a new project, even if the default User
model might suffice. This is not the route I had gone, so perhaps we didn't even need to worry about the ForeignKey
option anyway, but I wanted to walk through what our thought process was in case this is helpful for anyone else.
I do think this was one area in which it wasn't immediately clear what the better practice was for our case. The same page of the docs on custom user models goes on to say in a later section: "When you start your project with a custom user model, stop to consider if this is the right choice for your project." To me that sounds pretty contradictory to the earlier recommendation and so it's confusing.
Syncing our database with Django models
I attempted to replace the existing primary key, an AutoField
called person_id
, with a new OneToOneField
for our autogenerated Person
model:
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True,)
However, when I tried to run the migration, it errored out because of the existing person_id primary key in the database, instead of just deleting that primary key and creating a new one with user like I had thought would happen. After some trial and error I decided to just remove person_id
directly in Postgres:
DROP SEQUENCE person_person_id_seq;
ALTER TABLE person DROP COLUMN person_id;
Our Person
model definition in models.py
that finally got migrations to work and the database synced now looked like this:
class Person(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
primary_key=True,
)
discord_id = models.TextField(blank=True, null=True)
zoom_id = models.TextField(blank=True, null=True)
birthdate = models.DateField(blank=True, null=True)
class Meta:
managed = False
db_table = 'person'
Again, you may have previously read about our experience with the managed Meta
attribute in Steve's post. It would have probably saved us some time if we had tried that first. In any case, this is what the person table in our database model ended up looking like:
We don't need to create a User
model so I left that table out in the design.
So, as a heads up, you might run into this same back-and-forth of trying to get migrations in Django to work, and having to fiddle around on the database level as well to get everything to look the way it should. Once we got our database to a state that we wanted, we could move on to figuring out how the registration form should update both User
s as well as the corresponding Person
s.
Using a signal to create a Person
for the new User
I found two blog posts that use signals with the Profile
model. The way I understand signals is that they allow a part of a Django app or project to know when some event or action has taken place elsewhere in the app that it isn't necessarily directly "listening" to. When a signal is sent from a sender to a receiver, the receiver can then carry out a particular action depending on what kind of signal it is.
In our case, I want the User
model to be the sender - specifically, when a new user registers for the first time - and the Person
model to be the receiver. This should result in a new Person object also being created with the creation of a new User
. In our models.py
, I added this code directly below our Person
definition:
@receiver(post_save, sender=User)
def update_profile_signal(sender, instance, created, **kwargs):
if created:
Person.objects.create(user=instance)
instance.person.save()
Because I use the post_save signal, this should also work if the action is just an update on an existing User
. I haven't actually built a way for our D&D players to update their profiles in the app yet, so we have yet to see whether this exactly handles that scenario, but for now it works great for user registration.
Wrapping up with the view
In my views.py
, following the blog examples linked above, I could finally put together a register view to process our form data:
def register(response):
if response.method == 'POST':
form = RegisterForm(response.POST)
if form.is_valid():
user = form.save()
user.refresh_from_db()
user.person.birthdate = form.cleaned_data.get('birthdate')
user.person.discord_id = form.cleaned_data.get('discord_id')
user.person.zoom_id = form.cleaned_data.get('zoom_id')
user.save()
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=password)
login(response, user)
return redirect('/')
else:
form = RegisterForm()
return render(response, 'manager/register.html', {'form': form})
The refresh_from_db()
method is absolutely handy - before I attempt to set the Person
fields with values from the form, I have to make sure to refresh the User
object so we can grab the related Person
instance.
We had already set up our registration template (this would be the manager/register.html
argument in the return statement above) from Part 1, and I didn't need to make any changes there.
...And we're off and running!
Our app now has a registration form that sets our new DnD player signups as users, authenticates them, and also allows them to save some other personal info as well. To recap, here are some of the things we learned:
- Someone who has to log in to and authenticate in a Django-powered app is a
User
. - If you want to store additional attributes with each user, you have the option of extending the
User
model. - A built-in view or form like
UserCreationForm
can save you at least some work. Django has so many of these since it's a framework that's been around for a while. You may still have some further tweaking to do, but it's probably worth digging to see if a common pattern or use case has already been addressed by Django. OneToOneField
establishes the relationship betweenUser
and your "profile" model, allowing you to traverse, query, and work with data on either side of the relationship.
Being new to Django, it took some trial and error to get our models working with our database in the way we wanted. That said, this was a fun little learning exercise, and I hope my experience also helps some of you out there.