April 25, 2018
I wrote last fall about extending Django’s User model. In that blog post, I wrote a solution to allow Users of a website to have Profiles without breaking either Django’s built-in authentication system or any third-part packages. A few months down the road a user tried something I hadn’t foreseen, and I learned something new.
After testing the prototype, a friend suggested I add the ability for users to log in with social media accounts. This is becoming more and more common, and seemed like a good next thing to learn. Luckily there was already an open-source Django package with most of the hard work already done. I was quickly able to incorporate the package and allow people to authenticate with Facebook, Twitter, and GitHub. Hooray!
Several months later a new user created an account with their Facebook credentials, and suddenly every attempt to visit my homepage was causing a Server Error. How can this be? Inspecting the app’s production data showed me that this new User didn’t have a Profile! This is exactly what I tried to avoid with all that work in the last blog post.
When I added the User’s Profiles to my app, I originally created only one way for new User’s to make accounts. I made sure the Registration Form added a Profile for each new User every time a User was saved to the database, like so:
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
While this does exactly what I want every time the form is saved, it doesn’t address a very important question. What if a User gets saved to the database without a Profile? That’s what happened when people tried use Social Media to register for my Bingo App.
The people who maintain Django have obviously encountered a lot of stuff. Django has a built-in “signal dispatcher” that allows different objects to send and receive signals based on events. In fact, a new Object getting written to the database automatically sends both a “pre-save” and “post-save” signal.
I used the User’s post-save signal to trigger a Profile object being created. The following is from my Profile system’s
class UserProfile(models.Model): ... Profile Stuff ... @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): """ Each time a User is created, it sends a signal with a reference to its class (sender), the specific User object (instance), and a boolean telling us if the database save was successful (created). The decorator on this method makes sure this method is called every time a User object sends its "post save" signal. """ if created: UserProfile.objects.create(user=instance)
And just like that every new User will have a profile, no matter how it’s created!
What’s the lesson? What is the takeaway?
Enforce relationships close to the database.
If models are dependant on one another, and there’s no way to decouple them, that relationship needs to be enforced at the model level. By trying to enforce a relationship between Models through a Form, I left a gaping whole through which people could break my app without even trying anything crazy. Oops.
I tested social authentication, but every person who tried it already had an account and was just adding Social Media login. I never tested that a brand new user could register via Social Media. Perfect testing may be unattainable, but I will never stop trying! Unless I’m tired. Or hungry.