Building a User Registration Form with Django's Authentication System

Crunchy Data

This blog post is authored by Kat Batuigas.

If you haven't already read my colleague Steve Pousty's blog post on handling composite primary keys in a simple web application using Django and PostgreSQL, be sure to check it out. This post is going to be the first in a two-part series about adding a user registration system to the same app. In this first post, I'm going to talk specifically about setting up a "self-registration" form for our Dungeon and Dragons (DnD) players, using Django's built-in authentication (which includes built-in forms like UserCreationForm). The next post will talk about extending the default User model in Django so that we can store extra custom attributes about our players. 

To get the most out of this first post, it would be good to be familiar with some Django fundamentals such as models, views, templates, and URLconf. When we started building this app, I was very new to Django, so my goal is for you to be able to follow along this post even with simple knowledge of the basics. Our app uses Django 2.2, but what I cover here isn't dependent on a particular version.

Quick background on our DnD app

For our app, the intended user flow is for an individual to create an account (or log in if they already have one), authenticate, and then do any of the following:

  • create characters 
  • edit characters 
  • view available campaigns 
  • sign up characters for a campaign
  • update their user profile

To work on the above, I needed - you guessed it - some user accounts to test with, so I figured that a good place to start would be to set up account registration.

How to initially proceeded with the registration form

When working with forms, Django generally wants you to create:

  1. In forms.py: a form class to declare the form fields.
  2. In views.py: A view that defines what ought to happen when a user opens the form URL in the browser, and submits the form. 
  3. In the app's templates directory: A template that describes how the form is rendered in the browser. 
  4. In urls.py, a mapping is done between the desired URL path and the corresponding view so that the site knows what code to execute given the page or path the user is requesting. 

I started with setting up our form class in forms.py. 

When I define our form class in forms.py, I'm essentially setting up the form's structure and how the client will interact with it. What are the form fields? What type of input is expected? 

Steve talks a little bit about the database design of our app in his blog post, but what I'll share with you is that initially, our Person model had included fields for username, email, first name, etc. So, for our first pass at defining the form, I just wanted to include all the fields from the model.

The standard way of defining a Form class in Django looks like this:

from django import forms

class RegisterForm(forms.Form):
   first_name = forms.CharField(label='First name', max_length=100)
...

I happily started adding in the fields from our Person model one by one, but as I continued to refer to the documentation to figure out how to map the form data to the model, I saw that there was a ModelForm helper class that I could use. This class seemed to do exactly what I need, which is to have my form just map to a model, with the advantage of needing less code.

While this isn't what I ended up using (more on this later), here's how I initially defined our form class using ModelForm, plus the template (register.html) I set up for the form, as well as a screenshot of the form rendered in the browser. It was relatively simple to at least get it to show up:

forms.py

from django.forms import ModelForm
from manager.models import Person


class RegisterForm(ModelForm):
username = forms.CharField(max_length=100)
password = forms.CharField(widget=PasswordInput)
email = forms.CharField(widget=EmailInput)
discord_id = forms.CharField(max_length=100, label='Discord ID')
zoom_id = forms.CharField(max_length=100, label='Zoom ID')

class Meta:
model = Person
fields = ["username", "password", "birthdate", "email", "discord_id", "zoom_id"]
register.html

{% extends "base_generic.html" %}

{% block title %}Create an Account{% endblock %}

{% block content %}
   <form method="POST" class="form-group">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">Register</button>
   </form>
{% endblock %}
urls.py

from django.urls import path
from . import views


urlpatterns = [
   ...
   path('register/', views.register, name='register'),    
]

User registration form with several fields including username and password

What I wanted to figure out next was how a person could be immediately authenticated upon successfully registering, so that they could create characters and sign up to play in a campaign. That would have to be handled in our "registration" view. I was not at all familiar with authentication in Django, so that was the next thing to learn.

Built-in authentication in Django

Django projects do come with an authentication system out of the box, and it's pretty robust. It allows you to create users (including admin/superusers), assign groups and permissions, limit specific pages only to logged-in users or just users who meet certain criteria, and so much more.

In Django, the people interacting with your site are represented by the User model. Now, when Steve and I initially designed our database, we were still feeling our way around Django and had not considered our players to be Users in the Django sense - hence the initial version of the Person table/model. 

If you need users to authenticate in your app, here's the easiest way to get started with using built-in authentication:

  1. Add out-of-the-box URLconfs for login, logout, and password management in your app's urls.py,
  2. Create templates to handle and render the login, logout, and password management built-in views you want to use - Django does not provide these templates out of the box.

Basically, it doesn't take too many more additional lines of (relatively straightforward) code to allow your app's users to log in to an "end user" interface. But this flow assumes that the users have already been created and set up with valid credentials. You may have also noticed in the list of provided URLconfs that there wasn't one specific for account registration.

So, I still hadn't figured out how exactly a new "person" would be hooked up as a "user" and be able to log in and use the app's features. Is there a way for our registration form to also create a new user? Should I not have a Person model at all and just have our players be represented by the User model?

Django's UserCreationForm

Fortunately, I found the answer to the first question in the built-in UserCreationForm. The doc on authentication in Django mentions that it is "a ModelForm for creating a new user." It comes with three fields: username, password1 and password2, and it also uses a couple of built-in password functions. 

Great! This seemed like a solution to hooking up our players as app users that can authenticate. 

Here's how I ended up rewriting our form class in forms.py:

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User


class RegisterForm(UserCreationForm):
birthdate = forms.DateField()
discord_id = forms.CharField(max_length=100, help_text='Discord ID')
zoom_id = forms.CharField(max_length=100, help_text='Zoom ID')

class Meta:
model = User
fields = ["username", "password1", "password2", "birthdate", "email", "discord_id", "zoom_id"]

Compared to using ModelForm, I was able to take out username, password, and email from the fields definition, and this time I specify model = User in the Meta class. That said, you might be wondering: "Okay, but what about processing the form data? How will the User model save fields like birthdate and Discord ID?" And if you've read through the entire official doc on Django's authentication system, you may have noticed that while there are built-in views and forms, there's also some mention of "custom user model" or "customized authentication."  

Referring to the reference doc for the User model again, I don't see specific instructions for storing extra attributes with User, so I look into customizing authentication and learn about extending the User model - which I talk about in my next post.

What we've done so far

To sum up, so far we've figured out how to define a form class that maps to the User model and creates a new User when instantiated. This allows us to point our DnD players to a form that will set them up as a User in our app with the username and password they provide themselves upon registration.

A lesson learned for me personally was to consider authentication needs early on and be familiar with how your intended framework handles this

In Part 2 of this series, I'll be showing you how we implemented custom authentication and redid our data models. I'll also include how I ended up writing our registration view in views.py. See you there!

Join the Discussion

Newsletter