Welcome back, everyone! This article is a continuation of the guide on building a complete blog app with Django. If you haven’t read the first part of this series yet, I recommend checking it out before proceeding. In this part, we’ll dive into implementing authentication functionality, focusing on setting up secure user registration in Django. Stay tuned for the next part, where we’ll continue building out the app.
In this series, we are building a complete blog application, guided by the following Entity-Relationship Diagram (ERD). For this time, our focus will be on setting up secure user registration. If you find this content helpful, please like, comment, and subscribe to stay updated when the next part is released.
In this article, I’m using Linux. For Mac users, the commands will be the same. If you’re using Windows, I’ll be providing both the Linux/Mac and Windows commands where there are differences, so you can follow along easily regardless of your operating system.
Prerequisites
- Have a basic understanding of Django
- Have a basic knowledge of Python
- PostgreSQL installed on your machine
Okay, let’s get started !!
##########
For any authentication system, a User model is essential. That’s why Django’s authentication system includes a default built-in User model. which comes with these primary attributes:
- username
- password
- first_name
- last_name
However, these attributes might not always align with the specific requirements of a project. As in our case, you can notice that they differ from the User attributes in our application’s ERD.
To address this, Django allows us to override the default User model with a customized one that better suits our application’s needs. This can be achieved through inheritance or by creating a new User model. In this tutorial, we’ll explore the latter approach, where we’ll create a new customised User model that includes attributes specific to our blog application.
While it’s possible to customize Django’s default User model, doing so introduces additional complexity to an already robust system. It’s recommended to stick with the default model whenever possible unless you know what you’re doing or project-specific requirements explicitly demand customization.
Now that we understand what needs to be done, let’s create our first app to implement the custom User model.
1. Create an app named Users
within our project
Make sure you’re in the same directory as manage.py
and type this command:
# macOS/Linux
(.venv)$ python3 manage.py startapp users
# Windows
(.venv)$ python manage.py startapp users
This will create a users
directory, which will be structured as follows:
users/
migrations/
__init__.py
admin.py
apps.py
models.py
tests.py
views.py
Next, we will add the newly created Users app to the project’s INSTALLED_APPS
setting located in the blog_app/settings.py
file. This will inform Django that our app is active and ready to be used.
# django_project/settings.py
INSTALLED_APPS = [
# -- other code
"users.apps.UsersConfig", # <== new app
]
2. Create test cases for the User model
Django uses the unit test module’s built-in test discovery mechanism to automatically find and run tests within your project. It will locate tests in any file named with the pattern test*.py
under the current working directory.
Unlike the first part of this tutorial, we’ll create a separate tests
module to house our test code as we promised at the beginning of the series to follow the best practices. This organization help to improve code readability, maintainability, and reusability.
We will now create a tests
module in the users
directory for our test cases, with separate files for models, views, forms, and any other components that need testing. These files typically follow the naming conventiontest_<component_name>.py
. Thus, our test directory structure will look like this:
users/
/tests/
__init__.py
test_models.py
test_forms.py
test_views.py
The __init__.py
file should be an empty file. In Python, an empty __init__.py
file signifies that the directory is a package and can contain modules. Next, we'll remove the users/tests.py
file, as we'll be organizing our tests into separate files in the users
directory.
Now that we have a solid understanding of the setup, let’s begin writing our tests for the User model using the TDD methodology. Open the users/tests/test_models.py
file and add the following test cases.
# users/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
User = get_user_model()
class UserTest(TestCase):
def setUp(self):
self.user_test = {
'full_name': 'Tester',
'email': '[email protected]',
'bio': 'Biography of Tester',
'password': 'user12345',
}
self.saved = self.create_user(self.user_test)
def create_user(self, data):
return User.objects.create_user(**data)
def test_user_creation(self):
"""User creation is successful when all the required field are fill"""
self.assertTrue(isinstance(self.saved, User))
self.assertEqual(str(self.saved), self.user_test['email'])
def test_required_fields(self):
"""Create a user with a empty email or password should raise an Error"""
fake_user = {
'full_name': 'fake',
'email': '',
'bio': 'fake bio',
'password': 'user12345'
}
with self.assertRaises(TypeError):
User.objects.create_user()
with self.assertRaises(TypeError):
User.objects.create_user(email='[email protected]')
with self.assertRaises(ValueError) as context:
self.create_user(fake_user)
self.assertEqual(str(context.exception), "The Email must be set")
def test_password_encripted(self):
"""Saved password in the database should be different from from provided password"""
self.assertNotEqual(self.saved.password, self.user_test['password'])
def test_creation_superuser(self):
super_user = User.objects.create_superuser(email='[email protected]', password='super12345', bio='User admin')
self.assertTrue(super_user.is_superuser)
In the code above, we import the get_user_model
function to retrieve the custom user model that we will define soon. We have also created tests to ensure that a user is created successfully when the correct credentials are provided, check the required fields, verify that the password is encrypted after the user has been saved, and finally, test the creation of a superuser.
DO NOT apply the migrations. Remember: You must create the custom user model before you apply your first migration.
3. Create a custom user model
3.1 Create a Model Manager
To ensure email serves as the unique identifier for user authentication, we’ll create a custom Manager. This can be achieved by subclassing BaseUserManager
from Django. The custom Manager will define the logic for creating and interacting with users in our application.
Let's create a new file named managers.py
within the users
directory and add the following code:
# users/managers.py
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _
class CusmtomUserManager(BaseUserManager):
"""
Use the email field as the unique identifiers for the authentication
"""
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError(_("The Email must be set"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
"""
Create a SuperUser with email and password
"""
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True"))
return self.create_user(email, password, **extra_fields)
In our custom manager model, we have overridden the create_user
and create_superuser
methods. The create_user
method will now accept an email argument instead of a username argument, and the create_superuser
method will also use email instead of a username.
3.2 Create a Custom Model
Now, let’s create our User model in the models.py
file within the users
directory to ensure that our previous tests pass.
# users/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.utils.translation import gettext_lazy as _
from PIL import Image
from .managers import CustomUserManager
class CustomUser(AbstractBaseUser, PermissionsMixin):
full_name = models.CharField(_('full name'), blank=False, null=False, max=200, verbose_name='Full Name')
email = models.EmailField(_('email address'), unique=True, max_length=200)
photo = models.ImageField(upload_to='profiles', blank=True, null=True, verbose_name='Photo')
bio = models.TextField(blank=True, null=True, verbose_name='Biography')
posts_counter = models.PositiveIntegerField(default=0, null=False, verbose_name='Posts Counter')
updated_at = models.DateTimeField(autho_now=True)
created_at = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = CustomUserManager()
def save(self, *args, **kwargs):
"""
Overwrite the default method save() to resize the profile picture
before save using Pillow.
"""
super().save()
if self.photo:
img = Image.open(self.photo.path)
if img.height > 80 or img.width > 80:
img_size = (80, 80)
img.thumbnail(img_size)
img.save(self.photo.path)
class Meta:
"""
Set the table name to follow our ERD
"""
db_table = 'users'
def __str__(self):
return self.email
In the above code, we:
- Created a new model called
CustomUser
that subclassesAbstractBaseUser
which allows us to create our own model attributes - Added the users' table attributes to our model
- Set the
USERNAME_FIELD
-- which defines the unique identifier for the User model to email - Specified that all objects for the class come from the
CustomUserManager
- Resized the profile picture to have 80px as height and 80px as width and saved the pictures in the
media/profiles
directory which will be soon set up in the setting. - Named our table
users
to align with the provided ERD.
3.3 Settings
We’ll add the following line to our settings.py
file to inform Django to use our custom user model for user authentication instead of the default built-in model.
# blog_app/settings.py
AUTH_USER_MODEL = "users.CustomUser"
This line sets users.CustomUser
as the active user model within our project. Additionally, we'll configure Django's media storage by adding these lines to settings.py:
# blog_app/settings.py
import os
...
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
- MEDIA_ROOT specifies the server-side path where uploaded media files in our case our user profile picture will be stored on our computer
- MEDIA_URL defines the URL prefix that the browser will use to access uploaded media files over HTTP (e.g., https://yourdomain.com/media/).
Finally, to ensure that uploaded pictures are only saved to the media directory during development, we’ll add some code to our blog_app/urls.py
file. This configuration allows the development server to serve media files directly while keeping media serving disabled in production for security and performance reasons.
# blog_app/urls.py
# -- other code
from django.conf import settings # new line
from django.conf.urls.static import static # new line
urlpatterns = [
# -- application paths
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # new line
3.4 Run migrations
Now, we can create and apply the migrations, which will create a new database that uses the custom user model.
# Linux/MacOs
(.venv)$ python3 manage.py makemigrations users
# Windows
(.venv)$ python manage.py makemigrations users
We should see something similar to the following output:
Migrations for 'users':
users/migrations/0001_initial.py
+ Create model CustomUser
We can run the migration to create a database with the tables but first, let's see what SQL that migration would run.
# Linux/MacOs
(.venv)$ python3 manage.py sqlmigrate users 0001
# Windows
(.venv)$ python manage.py sqlmigrate users 0001
From the output, we can see that Django will create a users
table with all the attributes in our ERD.
Our migration file looks like this:
# Generated by Django 5.1.1 on 2024-10-08 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('full_name', models.CharField(max_length=200, verbose_name='full name')),
('email', models.EmailField(max_length=200, unique=True, verbose_name='email address')),
('photo', models.ImageField(blank=True, null=True, upload_to='profiles', verbose_name='Photo')),
('bio', models.TextField(blank=True, null=True, verbose_name='Biography')),
('posts_counter', models.PositiveIntegerField(default=0, verbose_name='Posts Counter')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'db_table': 'users',
},
),
]
We can now, safely run the migration to create our database
# Linux/MacOs
(.venv)$ python3 manage.py migrate
# windows
(.venv)$ python manage.py migrate
If you encounter any errors at this stage, please make sure you've followed all the steps in the first part of this series.
Now, let's make sure our tests are passing
# Linux/MacOs
(.venv)$ python3 manage.py test
# Windows
(.venv)$ python manage.py test
The output should be similar to the following:
Found 6 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 2.746s
OK
Destroying test database for alias 'default'...
3.5 Remove Django built-in admin application (optional)
When we create a new project, Django automatically adds django.contrib.admin
to the INSTALLED_APPS
. While the Admin app is very useful for backend administration, it is not a requirement for our project, and we won't be using it. So to adhere to the YAGNI (You Aren't Gonna Need It) principle in software development, I recommend removing it. This will help keep our project lean and focused on the essential features.
First, remove or comment out django.contrib.admin
from the INSTALLED_APPS
setting in the blog_app/settings.py
file.
# blog_app/setting.py
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
# -- other codes
'users.apps.UsersConfig',
]
Next, remove or comment out the URL path in the blog_app/urls.py file
# blog_app/urls.py
# from django.contrib import admin
...
urlpatterns = [
# path('admin/', admin.site.urls),
...
]
...
4. Create a Custom form for our model
4.1 Create test
Following our TDD approach, let's create tests for the custom form we'll use for user registration. In the tests
directory, create a new file named test_forms.py
and add these test cases:
# users/tests/test_forms.py
from django.test import TestCase
from users.forms import CustomUserCreationForm
class CustomUserCreationFormTest(TestCase):
def setUp(self):
self.full_name = 'First Name'
self.password = 'test12345'
self.email = '[email protected]'
self.bio = 'Tester biography'
def test_email_validation(self):
"""Form should raise error when email is not a valid email"""
data = {
'full_name': self.full_name,
'password1': self.password,
'password2': self.password,
'email': 'testegmail.com',
'bio': self.bio,
}
form = CustomUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['email'], ["Enter a valid email address."])
def test_name_validation(self):
"""Form should raise error when the full name is not first and last name"""
data = {
'full_name': 'Fake',
'password1': self.password,
'password2': self.password,
'email': 'testegmail',
'bio': self.bio,
}
form = CustomUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['full_name'], ["Enter a first and last name."])
def test_password_confirmation(self):
"""Form should raise error when password do not match"""
data = {
'full_name': self.full_name,
'password1': self.password,
'password2': 'newowkdkdkd',
'email': self.email,
'bio': self.bio,
}
form = CustomUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['password2'], ["The two password fields didn't match."])
def test_bio_validation(self):
"""Form should raise error when the bio is less than 4 characters"""
data = {
'full_name': self.full_name,
'password1': self.password,
'password2': self.password,
'email': self.email,
'bio': 'th',
}
form = CustomUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors['bio'], ["The bio must be at least 4 characters long. Please provide more details."])
def test_form_validation(self):
"""Form should be valid"""
data = {
'full_name': self.full_name,
'password1': self.password,
'password2': self.password,
'email': self.email,
'bio': self.bio,
}
form = CustomUserCreationForm(data)
self.assertTrue(form.is_valid())
You may notice a few error flags in your text editor; that's perfectly normal since we haven't created the form yet.
4.2 Create forms
Django provides default forms UserCreationForm
and UserChangeForm
, for creating and updating user models. Since we're using a custom CustomUser
model, we need to create custom forms that specifically work with our model. To achieve this, we'll create subclasses of UserCreationForm
and UserChangeForm
in a new file called forms.py
within the users
directory.
# users/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth import get_user_model
from .models import CustomUser
from django import forms
User = get_user_model()
import re
class CustomUserCreationForm(UserCreationForm):
"""
Set the password fields to the default messages
"""
full_name = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'Enter Full name', 'class': 'form-control'})
)
email = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'Enter email', 'class': 'form-control'})
)
bio = forms.CharField(
widget=forms.Textarea(attrs={'placeholder': 'Enter author biography', 'class': 'form-control', 'rows': 5})
)
password1 = forms.CharField(
label='Password',
widget=forms.PasswordInput(attrs={'placeholder': 'Enter Password', 'class': 'form-control'})
)
password2 = forms.CharField(
label='Confirm Password',
widget=forms.PasswordInput(attrs={'placeholder': 'Confirm Password', 'class': 'form-control'})
)
class Meta:
model = CustomUser
fields = ('full_name', 'email', 'bio',)
def clean_full_name(self):
full_name = self.cleaned_data.get('full_name')
regex = r"^[a-zA-Z]{2,}(?:\s[a-zA-Z]{2,}(?:-[a-zA-Z]{2,})*)+$"
if not re.match(regex, full_name):
raise forms.ValidationError("Enter a first and last name.")
return full_name
def clean_email(self):
email = self.cleaned_data.get('email')
user = User.objects.filter(email=email)
if user.exists():
raise forms.ValidationError("Email Already Exist")
return email
def clean_bio(self):
bio = self.cleaned_data.get('bio')
if not len(bio) >= 4:
raise forms.ValidationError("The bio must be at least 4 characters long. Please provide more details.")
return bio
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('full_name', 'photo', 'bio',)
In our custom user creation form, we implement several validation checks to ensure data integrity and prevent invalid user data from being entered.
The clean_full_name
method verifies that a correct full name with both first and last names is entered.
The clean_bio
method ensures that the user's biography is at least 4 characters long.
Finally, the clean_email
method checks if the provided email address is not already present in our database to prevent duplicate email addresses from being registered.
Now, When we run our test we should see this:
(.venv)$ python3 manager.py test
Found 11 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 2.918s
OK
Destroying test database for alias 'default'...
5. Create views for the model
5.1 Create test cases for the views
Navigate to the users/tests
directory and create a new file named test_views.py
. This file will house the tests for our custom user views.
# users/tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
User = get_user_model()
class SignUpPageTests(TestCase):
def setUp(self) -> None:
self.full_name = 'test user'
self.email = '[email protected]'
self.bio = 'test user bio'
self.password = 'fake12345'
def test_signup_page_view(self):
"""The Signup url should render the 'registration/signup.html' template"""
response = self.client.get(reverse('users:signup'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, template_name='registration/signup.html')
def test_signup_correct_data(self):
"""User should be saved when a correct data is provided"""
response = self.client.post(reverse('users:signup'), data={
'name': self.full_name,
'email': self.email,
'bio': self.bio,
'password1': self.password,
'password2': self.password
})
users = User.objects.all()
self.assertEqual(users.count(), 1)
self.assertNotEqual(users[0].password, self.password)
def test_signup_fake_data(self):
"""User shouldn't be save with missing email field"""
response = self.client.post(reverse('users:signup'), data={
'name': self.full_name,
'email': '',
'bio': self.bio,
'password1': self.password,
'password2': self.password
})
self.assertEqual(response.status_code, 200)
users = User.objects.all()
self.assertEqual(users.count(), 0)
All the tests above will fail; we should see that 14 tests are run, with 3 of them failing.
5.2 Create a view for sign-up
In the users
directory open the file named views.py
and add this code:
# users/views.py
from django.views.generic import CreateView
from django.urls import reverse_lazy
from .forms import CustomUserCreationForm
from django.contrib.auth import get_user_model
User = get_user_model()
class SignUpView(CreateView):
form_class = CustomUserCreationForm
model = User
success_url = reverse_lazy('home')
template_name = 'registration/signup.html'
_ Model is to tell our sign-up view to use our custom model
_ Success_url is to redirect after the operation is successful. We set it to the home page for now
_ template_name as you might already guess is to tell which template should be used
6. Create a URL for our view
Create a new file named urls.py in the users
directory to define URLs for user-related functionalities. In this file, add the following code:
# users/urls.py
from django.urls import path
from . import views # Import views from the current users app
app_name = 'users'
urlpatterns = [
path('sign_up/', views.SignUpView.as_view(), name='signup'),
]
This code defines a URL pattern for the signup view (SignUpView). The app_name
is set to 'users' for easier namespacing of URLs within our application.
Then, include the users' URLs in the project's main URLs file:
# blog_app/urls.py
from django.urls import path, include # Import the include function
# ... other imports
urlpatterns = [
# ... other URL patterns
path('users/', include('users.urls')), # Include users app URLs
]
# ... other code
7. Create a Signup template
First, create a directory called templates in your users
directory. Within the templates
directory we have just created, create another directory called registration
, and within that create a file called signup.html
.
In other words, our template should be at users/templates/registration/signup.html
and add this code.
{% extends 'layout.html' %}
{% block page %} Register {% endblock page%}
{% block content %}
<div class="container my-3 p-3">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-5">
<div class="card-header">
<h3 class="font-weight-light my-4 text-center">Create Account</h3>
</div>
<form method="POST" class="row g-3 card-body">
{% csrf_token %}
{% if form.errors %}
{% for field, message in form.errors.items %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{{field}}:</strong> {{message|first}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="col-md-6">
<label class="form-label">Email Address</label>
{{ form.email }}
</div>
<div class="col-md-6">
<label class="form-label">Full Name</label>
{{ form.full_name }}
</div>
<div class="col-12">
<label class="form-label">Biography</label>
{{ form.bio }}
</div>
<div class="form-group col-md-6">
<label for="password1">Password</label>
<div class="input-group mt-2">
{{ form.password1 }}
<div class="input-group-append">
<span class="input-group-text">
<i class="bi bi-eye-slash" id="togglePassword1" style="cursor: pointer;"></i>
</span>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="password2">Confirm Password</label>
<div class="input-group mt-2">
{{ form.password2 }}
<div class="input-group-append">
<span class="input-group-text">
<i class="bi bi-eye-slash" id="togglePassword2" style="cursor: pointer;"></i>
</span>
</div>
</div>
</div>
<div class="form-group mt-4 mb-0">
<button type="submit" class="col-md-12 btn bg-secondary bg-gradient text-white">Sign Up</button><br><br>
</div>
</form>
<div class="card-footer text-center">
<a href="#" class="small">Have an account? Go to Sign in</a>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
Next, let's add Sign up link to our layout in the templates/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block page %}{% endblock %} | Blog App</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">Blog App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarScroll" aria-controls="navbarScroll" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarScroll">
<ul class="navbar-nav me-auto my-2 my-lg-0 navbar-nav-scroll" style="--bs-scroll-height: 100px;">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{% url 'home' %}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'about' %}">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'users:signup' %}">Register</a>
</li>
</ul>
<form class="d-flex">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</nav>
{% block content %}
{% endblock %}
<footer></footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
We include the bootstrap Icon link in the <head></head>
tag of our layout.hml
file to have the eye icon in our sign-up form.
Now, let's run our tests to make sure their passing
(.venv)$ python3 manager.py test
Found 14 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..............
----------------------------------------------------------------------
Ran 14 tests in 3.644s
OK
Destroying test database for alias 'default'...
Let's run the server to check what our sign-up form looks like.
(.venv)$ python3 manager.py runserver
Now, navigate to the https://127.0.0.1:8000/users/sign_up/
This looks pretty good, right!! Now, when we register a new user, we are successfully redirected to the homepage. However, this behaviour is not very practical, as you typically want to either automatically log in to the user upon registration or redirect them to the login page. We will discover this in the next part of this tutorial, where we'll delve into implementing the login functionality, as I feel this tutorial is becoming a bit too long.
I haven't gone into much detail for some code snippets, as I believe the code is self-explanatory. I've added comments to explain each line where necessary. However, if you need further clarification or have any questions, please don't hesitate to comment. I'm happy to provide more detailed explanations. Your feedback is always appreciated! Don't forget to like and leave a comment.
Top comments (0)