Customizing Django Authentication using AbstractBaseUser
Tags: django
Published: 2020-03-20 08:34:35

## 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:

1. Using a Proxy Model  based on ``User``.
2. Adding a ``OneToOneField`` that points to ``User`` on a Profile model.
3. Extending ``AbstractUser`` and adding fields for additional profile information.
4. 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
```
![welcome.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/a9a4079fec-welcome.PNG) 

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
```
![admin.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/046a8dd632-admin.PNG) 
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.
![coreadmin.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/dad4eef975-coreadmin.PNG) 

Clicking the ``Add`` button and create some test categories.
![Categories.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/be176fafe8-Categories.PNG) 

## 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``.
```html
<!--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}}" >&nbsp;View Details &rarr;</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.
![home.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/2427f6598f-home.PNG)

Clicking ``Add Listing`` should take you to the login form.
![login.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/ef96876ba6-login.PNG)

Click ``create new account`` and signup for an account![signup.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/a8a5595f3b-signup.PNG)

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.
![add_listing.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/9b62595329-add_listing.PNG)

Fill in some details and creating a listing and the listing. You will be redirected to the home page.
![listings.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/18d768f043-listings.PNG)

You can test password change and profile edit by clicking using the dropdown menu in the upper right corner
![menu.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/2147aebc4d-menu.PNG)

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.
![reset.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/9f3b8e305b-reset.PNG)

![email.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/19e6d23d35-email.PNG)

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.
![confirm_reset.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/710901c798-confirm_reset.PNG) 
![resetdone.PNG](https://l2earn.nyc3.digitaloceanspaces.com/l2earn/images/uploads/2020/03/19/14a6417a40-resetdone.PNG) 
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.

[The Source code for this article can be found here.](https://github.com/joshwizzy/listings)