
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.
- I have to change as little about the framework as possible.
- All of Django’s parts will still Just Work™.
- 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.