Django from first principles, part 14
MP 107: Building a registration page for new users.
Note: This is the 14th post in a series about building a full Django project, starting with a single file. This series will be free to everyone, as soon as each post comes out.
In the last post we built a login page, and implemented a logout button. That's helpful for users who already have accounts, but it doesn't do anything for people who want to create a new account.
In this post we'll build a registration page, where new users can create their own accounts. This will involve processing a user creation form that Django provides. We'll be writing our own forms shortly to allow users to create blogs and write posts, so this work will be good practice for the work that follows.
Building a registration page
Django provides a form that lets new users register an account, but it's up to us to specify a URL, write a view to process the form, and create a template for displaying the form. Let's do each of these in turn.
The register URL
The URL for the registration page should be similar to the one for the login page:
http://localhost:8000/accounts/register/
We'll define this URL pattern in accounts/urls.py:
from django.urls import path, include from django.contrib.auth import urls as auth_urls from . import views app_name = "accounts" urlpatterns = [ path("", include(auth_urls)), path("register/", views.register, name="register"), ]
We'll need to write a view that processes the user creation form, so we import the views module we're about to create. We point the register
URL pattern to the view function we're about to write, which we'll call register()
.
The register()
view function
Make a new file called views.py in the accounts/ directory. Here's what that file should look like:
from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render, redirect from django.contrib.auth import login def register(request): if request.method != "POST": form = UserCreationForm() else: form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() login(request, new_user) return redirect("blogs:index") context = {"form": form} return render(request, "registration/register.html", context)
We import the UserCreationForm
that Django provides for registering new users. We also import the login
function, which allows you to programmatically log a user in.
Users will end up viewing this page either through a GET request, or a POST request. If you're not familiar with those terms, a browser issues a GET request when it only wants to receive information from a server. Browsers issue POST requests when they need to send information to the server. When working with a form in a view, a GET request indicates the browser needs an empty form, that the user will then fill in. A POST request indicates the user has already filled in the form, and is submitting the information they entered.
Returning a blank form
With that in mind, it's helpful to look at the register()
view function in two ways. First, let's consider a user who just loaded the registration page. They need a blank copy of the registration form.
Here's the part of the view function that's relevant in this situation:
def register(request): if request.method != "POST": form = UserCreationForm() else: ... context = {"form": form} return render(request, "registration/register.html", context)
We actually detect GET requests by catching any request that's not a POST
request. There are a few other request types, and anything that's not a POST request should receive the same response as a GET request. (Nobody should be trying to send a DELETE request to our registration page; if they do, we'll just send them a blank version of the form.)
When a GET request is received, we create an instance of the UserCreationForm
class inside the if
block. If you call UserCreationForm()
with no data, Django returns a blank instance of the form.1
Notice the last two lines of this function are outside of the entire if-else
block. These two lines run for all requests. The blank form that's created for a GET request is packed into the context
dictionary, and passed to the register.html template to render the page. The user will see a blank form, where they can enter their data.
Processing a completed form
Now consider a user who has filled out the form, and clicked the submit button. Their browser submits a POST request, which includes the information they entered in the form.
Here's the execution path that's followed in that case:
def register(request): if request.method != "POST": ... else: form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() login(request, new_user) return redirect("blogs:index") context = {"form": form} return render(request, "registration/register.html", context)
There's more code here, because there's more work to do when the user has submitted some data. We create an instance of UserCreationForm
again, but this time we pass it the POST data from the request. Django returns a form object, but this time the form object has extra attributes that include the data submitted by the user.
The form has three fields: a username, a password, and the same password a second time. The form.is_valid()
method verifies that all this data is correct. Django verifies that the username meets appropriate criteria for a valid username, and that the passwords match and have an appropriate level of complexity. If the form is valid, we continue processing it. We save the data from the form, which stores the new user's account information in the database. We then log them in, and redirect them to the home page. They should see the authenticated version of the home page, which shows them their registration was successful.
If the form is not valid, the is_valid()
method adds an error message to the form. This includes suggestions for how the user can correct their submission. Execution then drops to the last two lines of the function. The form is packed into the context
dictionary, and the user sees a refreshed version of the registration page with a helpful error message.
The register template
Here's the template, which should be saved as register.html, in the same directory as login.html:
{% extends "blogs/base.html" %} {% block content %} <form action="{% url 'accounts:register' %}" method='post'> {% csrf_token %} {{ form.as_div }} <button name="submit" class="button primary">Register</button> </form> {% endblock content %}
This template is similar to the login template. It displays a form provided by Django, within the content
block of the base template. The form's action
field sends the form to the register()
view function when it's submitted, and the method
field ensures that it's submitted as a POST request.
Linking to the registration page
Finally, we need to add a link to the registration page. This should only be visible to users who aren't currently authenticated. To make sure that happens, it's placed alongside the login link in the {% else %}
block of the navigation bar, in base.html:
<div class="nav-right"> {% if user.is_authenticated %} ... {% else %} <a href="{% url 'accounts:login' %}">Log in</a> <a href="{% url 'accounts:register' %}">Register</a> {% endif %} </div>
Now users who aren't authenticated can click the login link if they already have an account, or the register link if they haven't made an account yet.
Using the registration page
If you load the site and log out of any existing user sessions, you should see the following:
If you click the registration link, you should see a form where you can enter information for a new account:
If you enter the required information and click Register, you should be brought back to the home page as an authenticated user:
Now new users can create accounts, and everyone can log in and log out.
Looking at an invalid form
There are a number of reasons a submitted registration form can be considered invalid. The username may already have been claimed, the passwords might not match or have the appropriate level of complexity, or the user may have left one of the fields blank.
Let's make one of these mistakes, and see what the resulting page looks like:
I submitted the same username that I used to make an account earlier. The is_valid()
method recognized this, and returned False
. But it also added the error message A user with that username already exists. That message is included as part of the form, and we don't have to do anything special to display it.
Looking at the POST data
Things like request.POST
can seem like a bit of magic that Django performs to give you access to the information you need on the backend of a web site. Behind all that magic is a set of Python structures that you probably learned a while ago. Let's take a look at request.POST
, and see how the data from the form is actually stored.
If you don't already have it running, open a terminal tab and start the development server with the runserver
command. Click a few links in the BlogMaker Lite site, and watch the terminal output. You should see something like this:
$ python manage.py runserver ... [09/Jul/2024 10:08:55] "GET / HTTP/1.1" 200 1318 [09/Jul/2024 10:08:57] "GET /blogs/ HTTP/1.1" 200 3320 [09/Jul/2024 10:08:58] "GET /blogs/1/ HTTP/1.1" 200 3949 [09/Jul/2024 10:08:59] "GET / HTTP/1.1" 200 1318
This is the output after clicking on the home page, the blogs page, a specific blog page, and back to the home page. Notice that these are all GET requests. You can see the timestamp for each request, and you can see that they all returned a 200 status code.
We can add a print()
call anywhere we want inside a Django project, and the output will show up in the development server's output. You shouldn't leave print()
calls in your production code, but you should feel free to learn more about Django by examining anything in the request-response cycle that you're curious about.
Let's look at the request.POST
object when a new user submits a registration form:
def register(request): if request.method != "POST": ... else: form = UserCreationForm(data=request.POST) print("POST data:", request.POST) if form.is_valid(): ... context = {"form": form} return render(request, "registration/register.html", context)
Add this print()
call to the else
block of the register()
function, and submit a valid user registration form. You should see something like this in the terminal tab where runserver
is active:
[09/Jul/2024 11:25:04] "GET /accounts/register/ HTTP/1.1" 200 1897 POST data: <QueryDict: { 'csrfmiddlewaretoken': ['qnl8G0...'], 'username': ['fake_user'], 'password1': ['fake_pw'], 'password2': ['fake_pw'], 'submit': [''] }> [09/Jul/2024 11:25:24] "POST /accounts/register/ HTTP/1.1" 302 0 [09/Jul/2024 11:25:24] "GET / HTTP/1.1" 200 1318
We can see the initial GET request for the registration page. We then see the request.POST
data. It's a QueryDict
object, which is a Python dictionary that's been customized for working with form-based data. We can see one key-value pair for each of the fields in the form, along with the data that was submitted: fake_user, fake_pw, and fake_pw. You'll also see the value of the CSRF token, and an empty field for the submit button. Finally, we can see that the POST request resulted in a redirect (302
) to the home page, which was then processed as a GET request.
Django, just like any modern web framework, can seem like it's doing a bunch of mysterious work behind the scenes. But it almost always comes back to the request-response cycle discussed earlier in this series. Most of Django's work centers around receiving information, unpacking it, processing it, repackaging it, and returning an appropriate response. Processing a form is one way way this cycle can play out.
Conclusions
Building a registration page follows the same process as building any other page in a standard Django project: define a URL pattern for the page, write a view, and write a template. The main difference between this page and the pages we made earlier in this series is the form-processing logic in the view. We'll use that same kind of logic in the next phase of work, where we let users create blogs and write their own posts.
Resources
You can find the code files from this post in the django-first-principles GitHub repository. The commits from this post are on the part_14 branch. There's only one commit for this branch.
We're not going to implement more account-focused pages in this project. If you're interested in building out more of these pages, see the documentation page Using the Django authentication system, and specifically the section Authentication Views.
A form is a block of HTML code that renders input fields on a web page, allowing users to enter data. Because of the name, it can seem like UserCreationForm
is a form. The UserCreationForm
we import is actually a class written in Python. How you make an instance of the class affects how the class is used. It can be used to generate the HTML necessary to render a blank form, or a form that shows the data that's already been entered. It can also be used to process the data that's been submitted.
Remember, all Django source code is public, and it can be helpful to take a look at the source code even if you don't fully understand it. Here's the source for UserCreationForm
. It gets most of its functionality from the BaseUserCreationForm
class, which in turn relies on the SetPasswordMixin
class, and a hierarchy of more general form classes.