Customizing Django Authentication using AbstractBaseUser
Joshua Masiko
Posted on March 20, 2020
Introduction
Django has a builtin app that provides much of the required authentication machinery out of the box.
It also allows you to customize authentication if the defaults don't match your requirements.
There are multiple approaches you can take including:
- Using a Proxy Model based on
User
. - Adding a
OneToOneField
that points toUser
on a Profile model. - Extending
AbstractUser
and adding fields for additional profile information. - Extending
AbstractBaseUser
and implementing the required functionality.
You can choose among the first 3 options if you wan't to add minimal customization to the Django defaults.
If, however, you wan't to use an email address instead of a username as the user identifier, you have to extend AbstractBaseUser
.
This article will explain how to do this using a bare-bones listings application as an example.
Prerequisites
You will need to have working knowledge of Python and the Django Framework.
The Django version used in this tutorial is 3.04.
A recent version of Python 3.
The instructions in this tutorial were performed on Ubuntu 18.04 LTS with Python 3.6.
High Level Description of the App
Unauthenticated users can view all listings.
Users have to register to post listings.
The registration information includes:
- Mandatory fields: email, password, phone.
- Optional fields: date_of_birth, photo.
Superusers can create staff users using the Django admin interface.
Staff can flag listings using the Django admin interface.
The most recent 30 unflagged listings are displayed on the home page.
Users can click on a listing to view its details.
Getting Started
On Ubuntu 18.04 you might need to add the universe repository and install python3-venv.
$ sudo add-apt-repository universe
$ sudo apt install -y python3-venv
Project Setup
Create and activate the virtual environment.
$ mkdir ~/.virtualenvs
$ python3 -m venv ~/.virtualenvs/listings
$ source ~/.virtualenvs/listings/bin/activate
Then install Django and create a project using the django-admin
command.
(listings) $ pip install django==3.0.4
(listings) $ django-admin startproject listings
(listings) $ cd listings
You can now run the dev server and navigate to http://127.0.0.1:8000 in your browser.
You should see the Django Welcome Page.
(listings) $ python manage.py runserver
Ignore the warning about unapplied migrations in the console.
The recommended procedure is to apply migrations after setting up the custom user model.
The accounts app
Let's add an app to host our authentiation customizations.
(listings) $ python manage.py startapp accounts
The model and Manager
# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.contrib.auth import get_user_model
from django.utils import timezone
class AccountManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, name, phone, password, **extra_fields):
values = [email, name, phone]
field_value_map = dict(zip(self.model.REQUIRED_FIELDS, values))
for field_name, value in field_value_map.items():
if not value:
raise ValueError('The {} value must be set'.format(field_name))
email = self.normalize_email(email)
user = self.model(
email=email,
name=name,
phone=phone,
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, name, phone, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, name, phone, password, **extra_fields)
def create_superuser(self, email, name, phone, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, name, phone, password, **extra_fields)
class Account(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
name = models.CharField(max_length=150)
phone = models.CharField(max_length=50)
date_of_birth = models.DateField(blank=True, null=True)
picture = models.ImageField(blank=True, null=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
last_login = models.DateTimeField(null=True)
objects = AccountManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'phone']
def get_full_name(self):
return self.name
def get_short_name(self):
return self.name.split()[0]
USERNAME_FIELD
is the name of the field on the user model that is used as the unique identifier.
REQUIRED_FIELDS
are the mandatory fields other than the unique identifier
The create_user
and create_superuser
functions should accept the username field, plus all required fields as positional arguments.
Django’s provides a PermissionsMixin
which we can include in the class hierarchy for our user model to support Django’s permissions.
Add the accounts app to the list of installed apps in settings.py
# listings/settings.py
...
INSTALLED_APPS = [
...
'accounts.apps.AccountsConfig',
]
Specify the custom model as the default user model using the AUTH_USER_MODEL
setting in settings.py
# listings/settings.py
...
AUTH_USER_MODEL = 'accounts.Account'
Install Pillow which is required by Django's ImageField
(listings) $ pip install pillow
Generate migrations for the model now that the model is setup.
(listings) $ python manage.py makemigrations accounts
And create the database.
(listings) $ python manage.py migrate
The forms
Subclassing AbstractBaseUser
enables us to re-use the Django auth
app built-in forms and views.
The following forms are compatible with any subclass of AbstractBaseUser:
AuthenticationForm
: Uses the username field specified by USERNAME_FIELD
.
SetPasswordForm
: Allows users to change their password without entering the old password.
PasswordChangeForm
: Allows users to change their password by entering the old password and a new password.
AdminPasswordChangeForm
: Allows users to change their password from the Django admin.
PasswordResetForm
: Assumes users to reset their passwords using a reset link sent to the email address.
These forms are by the built-in auth
views to which we delegate authentication and password management.
UserCreationForm
and UserChangeForm
: Can be used to create users and change user details.
These forms are tied to default Django User
model so we will provide our own implementations.
We will also add a registration form for signing up users.
# accounts/forms.py
from django import forms
from django.contrib.auth.models import Group
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from .models import Account
class RegistrationForm(forms.ModelForm):
password = forms.CharField(label='Password', widget=forms.PasswordInput)
class Meta:
model = Account
fields = ('email', 'name', 'phone', 'date_of_birth', 'picture', 'password')
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password"])
if commit:
user.save()
return user
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = Account
fields = ('email', 'name', 'phone', 'date_of_birth', 'picture', 'is_staff', 'is_superuser')
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField()
class Meta:
model = Account
fields = ('email', 'name', 'phone', 'date_of_birth', 'picture', 'password', 'is_active', 'is_superuser')
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
The views
We implement views for user registration and profile editing.
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login as auth_login, logout
from django.contrib import messages
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse
from .models import Account
from .forms import RegistrationForm
class RegistrationView(CreateView):
template_name = 'registration/register.html'
form_class = RegistrationForm
def get_context_data(self, *args, **kwargs):
context = super(RegistrationView, self).get_context_data(*args, **kwargs)
context['next'] = self.request.GET.get('next')
return context
def get_success_url(self):
next_url = self.request.POST.get('next')
success_url = reverse('login')
if next_url:
success_url += '?next={}'.format(next_url)
return success_url
class ProfileView(UpdateView):
model = Account
fields = ['name', 'phone', 'date_of_birth', 'picture']
template_name = 'registration/profile.html'
def get_success_url(self):
return reverse('index')
def get_object(self):
return self.request.user
The admin
Now we register the custom user model with Django’s admin.
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Account
from .forms import UserCreationForm, UserChangeForm
class AccountAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ('email', 'name', 'phone', 'date_of_birth', 'is_staff', 'is_superuser')
list_filter = ('is_superuser',)
fieldsets = (
(None, {'fields': ('email', 'is_staff', 'is_superuser', 'password')}),
('Personal info', {'fields': ('name', 'phone', 'date_of_birth', 'picture')}),
('Groups', {'fields': ('groups',)}),
('Permissions', {'fields': ('user_permissions',)}),
)
add_fieldsets = (
(None, {'fields': ('email', 'is_staff', 'is_superuser', 'password1', 'password2')}),
('Personal info', {'fields': ('name', 'phone', 'date_of_birth', 'picture')}),
('Groups', {'fields': ('groups',)}),
('Permissions', {'fields': ('user_permissions',)}),
)
search_fields = ('email', 'name', 'phone')
ordering = ('email',)
filter_horizontal = ()
admin.site.register(Account, AccountAdmin)
Testing the app so far
We should now able to create users and edit their details.
Let's create a superuser.
(listings) $ python manage.py createsuperuser
Email: admin@example.com
Name: Admin User
Phone: 123456
Password:
Password (again):
Superuser created successfully.
Run the Django dev server then navigate to http://127.0.0.1:8000/admin in your browser and login with the credentials you specified.
(listings) $ python manage runserver
You should be able to add and edit users.
The core app
Now let's implement the core app functionality which includes:
- Anyone can view listings.
- Users can register to create accounts.
- Registered users can post listings
- Logged in users can change their passwords.
- Logged in users can edit their profiles.
- Staff users can flag listings.
We will use Bootstrap 4 and django-crispy-forms for styling the application interface and forms.
Install django-crispy-forms
Install django-crispy-form using pip
(listings) $ pip install django-crispy-forms
Then add crispy_forms the the installed apps list in your settings file and set the default styling to bootstrap4:
# listings/settings.py
...
INSTALLED_APPS = [
...
'crispy_forms',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'
Create the app
(listings) $ python manage.py startapp core
Then add it to the installed apps list in settings.py
# listings/settings.py
INSTALLED_APPS = [
...
'core.apps.CoreConfig',
]
Since the user model has a picture
field let's set up MEDIA_URL
and MEDIA_ROOT
to tell Django where to save uploaded images.
# listings/settings.py
...
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
The auth
app password reset view sends an email to the user containing instructions and a URL.
We will use the console email backend for testing purposes.
# listings/settings.py
...
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
We also want to redirect to the home view on login and logout.
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'
The models
Each listing is associated with the user and category.
We also track the date and time that the listing was posted.
Listings may be flagged. We record the timestamp and user who flagged the listing.
By default listings will be sorted in reverse chronological order.
# core/models.py
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.urls import reverse
from django.template.defaultfilters import slugify
class Category(models.Model):
name = models.CharField(max_length=150)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = "Categories"
class Listing(models.Model):
title = models.CharField(max_length=150)
content = models.TextField()
category = models.ForeignKey(Category, on_delete=models.PROTECT)
expiry_date = models.DateField(null=True, blank=True)
location = models.CharField(max_length=150, null=True, blank=True)
slug = models.SlugField()
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='listings'
)
created_at = models.DateTimeField(default=timezone.now)
flagged = models.BooleanField(default=False)
flagged_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
null=True,
blank=True
)
flagged_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.title
class Meta:
ordering = ['-created_at']
def save(self, *args, **kwargs):
if not self.id:
self.slug = slugify(self.title)
return super(Listing, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('listing',
args=[self.slug])
We can now create migrations for the app and sync the database with the models.
(listings) $ python manage.py makemigrations core
(listings) $ python manage.py migrate core
The admin
We register the Category
and Listing
models with the Django admin.
# core/admin.py
from django.contrib import admin
from django.utils import timezone
from .models import Category, Listing
class ListingAdmin(admin.ModelAdmin):
list_display = ('category', 'title', 'content', 'created_by', 'flagged', 'flagged_by', 'flagged_at')
list_filter = ('flagged',)
exclude = ['slug', 'created_by', 'created_at', 'flagged_at', 'flagged_by']
def save_model(self, request, obj, form, change):
if not obj.pk:
obj.created_by = request.user
if obj.flagged:
obj.flagged_by = request.user
obj.flagged_at = timezone.now()
else:
obj.flagged_by = obj.flagged_at = None
super().save_model(request, obj, form, change)
admin.site.register(Category)
admin.site.register(Listing, ListingAdmin)
Navigate to http://127.0.0.1:8000/admin.
Login as the superuser and you will see new entries for Categories and Listings displayed.
Clicking the Add
button and create some test categories.
The views
The application functionality consists of 3 views:
- The default view displays the most recent 30 listings.
- A view to post a listing, restricted to logged-in users.
- A view to display a listings details.
# core/views.py
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.views.generic import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from django.urls import reverse
from .models import Listing
class HomeView(ListView):
template_name = 'home.html'
queryset = Listing.objects.filter(flagged=False)
context_object_name = 'listings'
paginate_by = 30
class CreateListing(LoginRequiredMixin, CreateView):
model = Listing
fields = ['title', 'content', 'category', 'expiry_date', 'location']
template_name = 'add_listing.html'
def get_success_url(self):
return reverse('home')
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
class ListingView(DetailView):
template_name = 'listing.html'
model = Listing
def get_object(self):
obj = super(ListingView, self).get_object()
if obj.flagged:
raise Http404()
return obj
The URLs
Let's start with th project-level URLs. We will re-use the auth
app views for Login, Logout, Password Change & Password Reset.
# listings/urls.py
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
from accounts import views
from accounts.views import RegistrationView, ProfileView
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
path('register/', RegistrationView.as_view(), name='register'),
path('profile/', ProfileView.as_view(), name='profile'),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
Then the core app URLs
# core/urls.py
from django.urls import path, include
from .views import HomeView, CreateListing, ListingView
urlpatterns = [
path('', HomeView.as_view(), name='home'),
path('add-listing', CreateListing.as_view(), name='add_listing'),
path('listings/<slug:slug>/', ListingView.as_view(), name='listing'),
]
The auth
views render templates from a registration
folder.
We will create a project level templates
folder for all our templates and a registration
subfolder.
(listings) $ mkdir -p templates/registration
Then add the templates
folder to the DIRS
setting in settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
The templates
auth
templates
These templates will be used by the auth
app views.
Auth templates will exend a base template.
<!--templates/registration/auth_base.html-->
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<title>Listings</title>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-4">
<h1 class="text-center">Listings</h1>
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
Login template used by LoginView
.
<!--templates/registration/login.html-->
{% extends "registration/auth_base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="card">
<div class="card-body">
<h4 class="card-title">Log in to your account</h4>
<form method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">{{ form|crispy }}<button type="submit"
class="btn btn-primary btn-block">Log in</button>
</form>
</div>
<div class="card-footer">
Forgot your password? <a href="{% url "password_reset" %}">click here</a><br>
New user? <a href="{% url "register" %}?next={{ next }}">create new account</a>
</div>
</div>
{% endblock %}
Register template used by RegistrationView
.
<!--templates/registration/register.html-->
{% extends "registration/auth_base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="card">
<div class="card-body">
<h4 class="card-title">Signup for an account</h4>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{form|crispy}}
<input type="hidden" name="next" value="{{ next }}">
<button type="submit" class="btn btn-primary btn-block">Register</button>
</form>
</div>
</div>
{% endblock %}
Change password template used by PasswordChangeView
.
<!--templates/registration/password_change_form.html-->
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row justify-content-center">
<div class="col-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Change your password</h4>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary btn-block">Change Password</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Password change done template used by PasswordChangeDoneView
.
<!--templates/registration/password_change_done.html-->
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row justify-content-center">
<div class="col-8">
<div class="card">
<div class="card-body">
<h2>Your password has changed</h2>
</div>
</div>
</div>
</div>
{% endblock %}
Password Reset template used by PasswordResetView
.
<!--templates/registration/password_reset_form.html-->
{% extends "registration/auth_base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="card">
<div class="card-body">
<h4 class="card-title">Reset your password</h4>
<form method="post">
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary btn-block">Reset</button>
</form>
</div>
</div>
{% endblock %}
Password reset email template used by PasswordResetView
.
<!--templates/registration/password_reset_email.html-->
{% autoescape off %}
You're receiving this email because you requested a password reset for your user account at {{ site_name }}.
Please go to the following page and choose a new password:
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
Your username, in case you’ve forgotten: {{ user.get_username }}
Thanks for using our site!
The {{ site_name }} team
{% endautoescape %}
Password reset done template used by PasswordResetDoneView
.
<!--registration/templates/password_reset_done.html-->
{% extends "registration/auth_base.html" %}
{% block content %}
<div class="card">
<div class="card-body">
<p>We’ve emailed you instructions for setting your password, if an account exists with the email you entered.
You should receive them shortly.</p>
<p>If you don’t receive an email, please make sure you’ve entered the address you registered with, and check
your spam folder.</p>
</div>
</div>
{% endblock %}
Password reset confirm template used by PasswordResetConfirmView
.
<!--templates/registration/password_reset_confirm.html-->
{% extends "registration/auth_base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="card">
<div class="card-body">
{% if validlink %}
<p>Please enter your new password twice so we can verify you typed it in correctly.</p>
<form method="post">{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary btn-block">Change my password</button>
</form>
{% else %}
<p>The password reset link was invalid, possibly because it has already been used. Please request a new password
reset.</p>
{% endif %}
</div>
</div>
{% endblock %}
Password reset complete template used by PasswordResetCompleteView
.
<!--templates/registration/password_reset_complete.html-->
{% extends "registration/auth_base.html" %}
{% block content %}
<div class="card">
<div class="card-body">
<p>Your password has been set. You may go ahead and log in now.</p>
<p><a href="{{ login_url }}">Log in</a></p>
</div>
</div>
{% endblock %}
Edit Profile template used by ProfileView
.
<!--templates/registration/profile.html-->
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row justify-content-center">
<div class="col-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Update your profile</h4>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-primary btn-block">Update Profile</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
core
templates
These templates will be used by the core
app and should be saved in the project level templates
folder.
The Base Template
<!--templates/base.html-->
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<title>Listings</title>
<style>
body {
padding-top: 5rem;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a href="{% url "home" %}" class="navbar-brand">Listings</a>
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse">
<span class="navbar-toggler-icon"></span>
</button>
{% if user.is_authenticated %}
<div id="navbarCollapse" class="collapse navbar-collapse">
<ul class="nav navbar-nav ml-auto">
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">{{user.name}}</a>
<div class="dropdown-menu dropdown-menu-right">
<a href="{% url "profile" %}" class="dropdown-item">Profile</a>
<a href="{% url "password_change" %}" class="dropdown-item">Change Password</a>
<div class="dropdown-divider"></div>
<a href="{% url "logout" %}" class="dropdown-item">Logout</a>
</div>
</li>
</ul>
</div>
{% endif %}
</nav>
<main role="main" class="container">
{% block content %}
{% endblock %}
</main>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
The Home template
<!--templates/home.html-->
{% extends 'base.html' %}
{% block content %}
<a href="{% url "add_listing" %}">Add Listing</a>
<h3 class="mt-2">Latest Listings</h3>
<div class="col-md-8 mt-3">
{% for listing in listings %}
<div class="card">
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>{{listing.title}}<small><a href="{{listing.get_absolute_url}}" > View Details →</a></small></p>
<footer class="blockquote-footer"><small>Posted on {{listing.created_at|date:"M d, Y"}} by
<a href="#">{{listing.created_by.name}}</a> in <a href="">{{listing.category}}</a></small></footer>
</blockquote>
</div>
{% endfor %}
</div>
{% endblock %}
Listing detail template
<!--templates/listing.html-->
{% extends 'base.html' %}
{% block content %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{listing.title}}</h5>
<p class="card-text">{{listing.content}}</p>
<p class="card-text"><small class="text-muted">Posted by {{listing.created_by.name}} on {{listing.created_at}}</small></p>
</div>
</div>
{% endblock %}
Add Listing template
<!--templates/add_listing.html-->
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{form |crispy}}
<button type="submit" class="btn btn-primary btn-block">Add Listing</button>
</form>
</div>
{% endblock %}
Testing the app
Navigate to http://127.0.0.1:8000 in your browser.
Clicking Add Listing
should take you to the login form.
Click create new account
and signup for an account
Upon creation of the account you will be redirected to the login form.
Login using the credentials you specified and you will be taken to the Listing creation form.
Fill in some details and creating a listing and the listing. You will be redirected to the home page.
You can test password change and profile edit by clicking using the dropdown menu in the upper right corner
To test password reset, logout then click the forgot password link.
Enter your email and check the console for the email containing the reset link.
Paste the reset URL in your browser and you will be prompted to enter your new password.
Enter your new password and a confirmation is displayed upon successful change.
You can then click the Log in
link and enter your new password to login.
Conclusion
This article has shown how to customize Django authentication to implement the following features:
- Using an email address instead of a username as the identification token.
- Django admin integration for the custom user model.
- User signup, login.
- Password change and reset.
Posted on March 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.