DEV Community

Cover image for Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part 2): User registration
AVLESSI Matchoudi
AVLESSI Matchoudi

Posted on • Originally published at Medium

Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part 2): User registration

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.
Image description
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 !!

            ##########
Enter fullscreen mode Exit fullscreen mode

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
  • email
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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': 'test@gmail.com',
      '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='tester1@gmail.com')
    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='superuser@gmail.com', password='super12345', bio='User admin')
    self.assertTrue(super_user.is_superuser)

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the above code, we:

  • Created a new model called CustomUser that subclasses AbstractBaseUser 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"
Enter fullscreen mode Exit fullscreen mode

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/'
Enter fullscreen mode Exit fullscreen mode
  • 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., http://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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We should see something similar to the following output:

Migrations for 'users':
  users/migrations/0001_initial.py
    + Create model CustomUser
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Image description
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',
            },
        ),
    ]
Enter fullscreen mode Exit fullscreen mode

We can now, safely run the migration to create our database

# Linux/MacOs
(.venv)$ python3 manage.py migrate

# windows
(.venv)$ python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'...
Enter fullscreen mode Exit fullscreen mode

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',
]
Enter fullscreen mode Exit fullscreen mode

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),
    ...
]
...
Enter fullscreen mode Exit fullscreen mode

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 = 'test@gmail.com'
    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())
Enter fullscreen mode Exit fullscreen mode

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',)
Enter fullscreen mode Exit fullscreen mode

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'...
Enter fullscreen mode Exit fullscreen mode

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 = 'testuser@email.com'
    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)
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

_ 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'),
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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/bootstrap@5.3.3/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/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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'...
Enter fullscreen mode Exit fullscreen mode

Let's run the server to check what our sign-up form looks like.

(.venv)$ python3 manager.py runserver
Enter fullscreen mode Exit fullscreen mode

Now, navigate to the http://127.0.0.1:8000/users/sign_up/
Image description

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.

READ NEXT PART

Top comments (0)