Extending Django’s User Model

Published on:
Sept. 23, 2017

So I’m working on this app, right? This app needs to allow people to do normal stuff like register an account, log in, and create, read, update, and delete (CRUD) stuff. This is all baked into the Django framework as-is. In fact, it’s exactly what Django was made for.

I also want my users to be able to create profiles with profile pictures, links to social media sites, and other things. That is not included, so I need to add it. Unfortunately, directly changing Django’s User model can break some things in Django’s authentication system. Here’s how I solved this problem.

The Problem


Django has a built-in authentication sytem. This sytem comes with a User object that is used to store user’s first and last names, username, password, and email. No more, no less. Normally in object-oriented programming, you can add functionality to an existing class by inheriting from it. Unfurtunately, Django’s User is included in many, many other parts of the framework. Creating a subclass of the build-in User class would create problems that I am just too lazy to deal with.

Normally if something isn’t obvious, I turn to Daniel and Audrey Roy Greenfeld’s amazing book “Two Scoops of Django” to see how the pros deal with it. They list three options for extending the User model. Options 1 and 2 do not preserve the built-in User, and instead suggest creating subclasses of it’s ancestors. These don’t solve the problem of preserving built-in systems as-is, but they are more flexible than Option 3.

Option 3 is to create a new model, and link it to Django’s User via a OneToOne database relationship. I liked this option for 3 reasons.

  1. I have to change as little about the framework as possible.
  2. All of Django’s parts will still Just Work™.
  3. Site visitors’ login info will be stored separately from their profile info.

The catch with Option 3 is that none of Django’s built-in views or forms were made to handle creating multiple objects at the same time. How then, is one supposed to create a registration form that lets users create a profile and an account with one button click?

A Solution


I say “a” solution here instead of “the” solution because there are as many ways to solve a problem as there are people who solve it. I’ve definitely used many apps with accounts and profiles (Facebook, Twitter, the other ones), so I know this problem has been encountered and overcome many times before.

Both models and forms in Django have a .save() method that can be added to or overridden. In order to make two objects with one form I had to override one of them. I chose to override the method on the form because in my mind it makes sense that the form is doing the saving, and should create and save both objects. If you disagree and have a better suggestion I’d love to hear about it. Here’s the code:

models.py

class UserProfile(models.Model):
    """
    ...docstring...
    """

    class Meta:
        verbose_name = 'Profile'
        verbose_name_plural = 'Profiles'

    user = models.OneToOneField(
        User,
        related_name='profile',
        on_delete=models.CASCADE,
    )

    ...other fields and methods...

    # Notice -> DOES NOT CREATE USER
    def save(self, *args, **kwargs):
        """
        Slugifies username automatically when UserProfile is saved
        """
        self.slug = slugify(self.user.username)
        super(UserProfile, self).save(*args, **kwargs)

As you can see, there’s no magic happening there.

All the good stuff is in forms.py:

class RegistrationForm(CrispyBaseModelForm):
    """
    ...docstring...
    """

    # specify widget for password input
    password = forms.CharField(widget=forms.PasswordInput())
    password_confirmation = forms.CharField(widget=forms.PasswordInput())

    # fields for Profile
    website = forms.URLField(required=False)
    picture = forms.ImageField(required=False)

    ...more stuff...

    class Meta:
        model = User
        fields = ('username', 'email',)

    def save(self):
        """
        Here we override the parent class's 'save' method to create a
        UserProfile instance matching the User in the form.
        """
        if self.is_valid():
            user = User.objects.create(
                username=self.cleaned_data['username'],
                email=self.cleaned_data['email'],
            )

            password = self.cleaned_data['password']
            password_confirmation = self.cleaned_data['password_confirmation']

            if password and password == password_confirmation:
                user.set_password(self.cleaned_data['password'])
                user.save()

            else:
                raise forms.ValidationError('Passwords Entered Do Not Match')

            website = self.cleaned_data['website']
            picture = self.cleaned_data['picture']
            profile = UserProfile.objects.create(user=user)

            if website:
                profile.website = website

            if picture:
                profile.picture = picture

            profile.save()

            return self

        else:
            return self.errors

Here we completely customize the .save() method on our form. Let’s break this down.

The Breakdown


First we specify the fields we want added and/or customized for this form. The password entry fields use the PassWordInput widget so that the passwords won’t be visible on screen.

class RegistrationForm(CrispyBaseModelForm):
    """
    ...docstring...
    """

    # specify widget for password input
    password = forms.CharField(widget=forms.PasswordInput())
    password_confirmation = forms.CharField(widget=forms.PasswordInput())

    # fields for Profile
    website = forms.URLField(required=False)
    picture = forms.ImageField(required=False)

Next we use a metaclass to declare what Model our form will relate to, and what fields from that model our form will borrow.

    class Meta:
        model = User
        fields = ('username', 'email',)

Then we’ll begin defining our method by ensuring our form was presented valid data. If it was, we’ll go ahead and create the User object with the data we were given. Notice that if the password and confirmation values don’t match, the form will return an error.

    def save(self):
        """
        Here we override the parent class's 'save' method to create a
        UserProfile instance matching the User in the form.
        """
        if self.is_valid():
            user = User.objects.create(
                username=self.cleaned_data['username'],
                email=self.cleaned_data['email'],
            )

            password = self.cleaned_data['password']
            password_confirmation = self.cleaned_data['password_confirmation']

            if password and password == password_confirmation:
                user.set_password(self.cleaned_data['password'])
                user.save()

            else:
                raise forms.ValidationError('Passwords Entered Do Not Match')

Finally we create a new Profile from the other fields in our form. Note that because the fields weren’t required in the form, we have to ensure they exist before assigning them.

            website = self.cleaned_data['website']
            picture = self.cleaned_data['picture']
            profile = UserProfile.objects.create(user=user)

            if website:
                profile.website = website

            if picture:
                profile.picture = picture

            profile.save()

            return self

And just like that, we have our User and our Profile. One generic class-based view, one form, two objects. Boom.

If you have a suggestion for a better way to implement this feature, please let me know.