Adding Users to Your Django Project With A Custom User Models

Django users with contrib auth module
Django users with contrib auth module (License: CC-BY)
  • Published: September 21, 2018

Overview

When your Django project is mature enough to handle users, you start reading about the django.contrib.auth module and how it provides an user authentication system. But you probably need some flexibility and add custom fields to the User model keeping the the default user model behaviour.

That means that you will have:

  • user accounts with your custom fields
  • groups
  • permissions
  • cookie-based user sessions

In this guide we will:

  1. Create a new Django project (called dcu aka.: django-custom-users)
  2. Set up a custom user model (as recommended in Django official documentation) by creating a new users app.
  3. Use shipped modules’ templates/views/urls for login, and user management related tasks.
  4. Have a signup page directly in front-site.

1. Setting up

Create required directories:


$ mkdir -f ~/.virtualenvs
$ mkdir django-custom-users
$ cd django-custom-users
django-custom-users$ 

Create a virtual environment:


django-custom-users$ mkvirtualenv -python=/usr/bin/python3.6 ~/.virtualenvs/dcu
django-custom-users$ workon dcu 

Activate the above virtual environment:


(dcu)django-custom-users$ workon dcu 

Install Django:


(dcu)django-custom-users$ pip install django 

Create the Django project:


(dcu)django-custom-users$ django-admin startproject dcu . 

Now we have the following structure:

.
β”œβ”€β”€ dcu
β”‚Β Β  β”œβ”€β”€ __init__.py
β”‚Β Β  β”œβ”€β”€ settings.py
β”‚Β Β  β”œβ”€β”€ urls.py
β”‚Β Β  └── wsgi.py
└── manage.py

2. Create users app

We are going to create a Django app that will handle our custom users called users.


(dcu)django-custom-users$./manage.py startapp users 
(dcu)django-custom-users$ tree 
.
β”œβ”€β”€ dcu
β”‚Β Β  β”œβ”€β”€ __init__.py
β”‚Β Β  β”œβ”€β”€ __pycache__
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ __init__.cpython-36.pyc
β”‚Β Β  β”‚Β Β  └── settings.cpython-36.pyc
β”‚Β Β  β”œβ”€β”€ settings.py
β”‚Β Β  β”œβ”€β”€ urls.py
β”‚Β Β  └── wsgi.py
β”œβ”€β”€ manage.py
└── users
    β”œβ”€β”€ admin.py
    β”œβ”€β”€ apps.py
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ migrations
    β”‚Β Β  └── __init__.py
    β”œβ”€β”€ models.py
    β”œβ”€β”€ tests.py
    └── views.py

4 directories, 14 files


2.1 Using new app

Let’s follow Django’s recommendation:

it’s highly recommended to set up a custom user model, even if the default User model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises

We start by making Django aware of our new app adding it to settings.py:

In dcu/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
	'users'
]

And defining the default User model as our new CustomUser model instead of the default auth.User using the AUTH_USER_MODEL key:

In dcu/settings.py:

AUTH_USER_MODEL = 'users.CustomUser'

Now every time we need to use our custom model, instead of referring directly to users.CustomUser we should use the function django.contrib.auth.get_user_model that returns the User model that is active in this project like:

from django.contrib.auth import get_user_model

UserModel = get_user_model()

2.2 Extending the User Model

In users.models:

from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    pass

2.3 Use new model in Admin

Admin backend uses two forms that has the old User model hardcoded so we need to subclass them to use our new CustomUser model.

I've raised a ticket to make them use the `get_user_model` function instead, so this step could be avoided in future version (currently 2.1).

In users/forms.py 1:

from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('username', 'email')

class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ('username', 'email')

And tell admin to use these new forms:

In users/admin.py:

from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ['email', 'username',]

admin.site.register(CustomUser, CustomUserAdmin)

Finally, generate and apply migrations:


(dcu)django-custom-users$./manage.py makemigrations 
Migrations for 'users':
  users/migrations/0001_initial.py
    - Create model CustomUser
(dcu)django-custom-users$./manage.py migrate 
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying users.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK

3. Creating pages

We will start creating a base.hml that will contain the HTML skeleton for our site, which all user related templates will extend and a simple home.html template.

3.1 Basic layouts

To create base.html and home.html, we tell Django to include a special /templates directory at the root of our project, so in dcu/settings.py:

TEMPLATES = [
    {
		#...
        'DIRS': ['./templates',],
	}
]

Create the directory templates at project root level:


(dcu)django-custom-users$./manage.py makemigrations 

Then in /templates/base.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>{% block title %}Django Auth Tutorial{% endblock %}</title>
</head>
<body>
  <main>
    {% block content %}
    {% endblock %}
  </main>
</body>
</html>

And a basic homepage /templates/homepage.html:

{% extends 'base.html' %}
{% block title %}Homepage{% endblock %}
{% block content %}
<a href="{% url 'login' %}">login</a> |
<a href="{% url 'signup' %}">signup</a>
{% endblock %}

3.1 Settings

The django.contrib.auth module makes use of three special settings in its /views.py file:

  • settings.LOGIN_URL:

    • Default: /accounts/login/
    • “The URL where requests are redirected for login, especially when using the login_required() decorator.”
  • settings.LOGIN_REDIRECT_URL

    • Default: /accounts/profile/
    • The URL where requests are redirected after login when the contrib.auth.login view gets no next parameter.
    • This is used by the login_required() decorator.
  • settings.LOGOUT_REDIRECT_URL:

    • Default: None
    • The URL where requests are redirected after a user logs out using LogoutView (if the view doesn’t get a next_page argument).

You can modify them as needed at dcu/settings.py:

# LOGIN_URL = '/accounts/login/'
# LOGIN_REDIRECT_URL = '/accounts/profile'
LOGOUT_REDIRECT_URL = 'homepage' #redirect to named pattern

3.2 Adding contrib URLs

The module comes already with the ability to handle the following URLs in /urls.py:

accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']

To use them, we include them in our main urls.py file at dcu/urls.py:

from django.views.generic.base import TemplateView

urlpatterns = [
	# ...
    path('', TemplateView.as_view(template_name='homepage.html'), name='homepage'),
	path('accounts/', include('users.urls')),
	path('accounts/', include('django.contrib.auth.urls')),
]

They are all defined with Class Based Views, and some of the above URLs use views that depend on templates, some of them exists and others not, lets have a look at the templates used at django/contrib/auth/:

class LoginView(SuccessURLAllowedHostsMixin, FormView):
	template_name = 'registration/login.html'
class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
	template_name = 'registration/logged_out.html'
class PasswordResetView(PasswordContextMixin, FormView):
	template_name = 'registration/password_reset_form.html'
class PasswordResetDoneView(PasswordContextMixin, TemplateView):
	template_name = 'registration/password_reset_done.html'
class PasswordResetConfirmView(PasswordContextMixin, FormView):
	template_name = 'registration/password_reset_confirm.html'
class PasswordChangeView(PasswordContextMixin, FormView):
	template_name = 'registration/password_change_form.html'
class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
	template_name = 'registration/password_change_done.html'

But in django/contrib/auth/templates we only have:

templates/
β”œβ”€β”€ auth
β”‚Β Β  └── widgets
β”‚Β Β      └── read_only_password_hash.html
└── registration
    └── password_reset_subject.txt

3 directories, 2 files

That means we need to create the following templates:

  • registration/login.html
  • registration/logged_out.html'
  • registration/password_reset_form.html'
  • registration/password_reset_done.html'
  • registration/password_reset_confirm.html'
  • registration/password_change_form.html'
  • registration/password_change_done.html'

3.2.1 Add login template

In /users/templates/registration/login.html:

{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>

<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

3.2.2 Add logged out template

In /users/templates/registration/logged_out.html:

{% extends "base.html" %}
{% block title %}Logged out{% endblock %}
{% block content %}
<p>You have been logged out</p>

<a href="{% url 'login'%}">Login</a>
{% endblock %}

In registration/password_reset_form.html:

{% extends "base.html" %}
{% block title %}Password reset form{% endblock %}
{% block content %}

<form action="" method="post">
	{% csrf_token %}
    {% if form.email.errors %}
	{{ form.email.errors }}
	{% endif %}
	<p>{{ form.email }}</p> 
    <input type="submit" value="Reset password" />
</form>
{% endblock %}

In registration/password_reset_done.html:

{% extends "base.html" %}
{% block title %}Password reset done{% endblock %}
{% block content %}
<p>Check your mail for resetting your password.</p>
{% endblock %}

In registration/password_reset_confirm.html:

{% extends "base.html" %}
{% block title %}Password reset confirm{% endblock %}

{% block content %}
    {% if validlink %}
        <p>Please enter your new password.</p>
        <form action="" method="post">
            <div style="display:none">
                <input type="hidden" value="{{ csrf_token }}" name="csrfmiddlewaretoken">
            </div>
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password" /></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Failed</h1>
        <p>The password reset link was invalid. Please request a new password.</p>
    {% endif %}

{% endblock %}

In registration/password_change_form.html:

{% extends "base.html" %}
{% block title %}Password change form{% endblock %}
{% block content %}
<form method="post">
  {% csrf_token %} 
  {{ form.as_p }}

  <input type="submit" value="Submit" />
</form>
{% endblock %}

In registration/password_change_done.html:

{% extends "base.html" %}
{% block title %}Password change done{% endblock %}

{% block content %}
<p>Password changed</p>
{% endblock %}

3.3 Add signup and profile page

In users/templates/signup.html:

{% extends 'base.html' %}
{% block title %}Sign Up{% endblock %}
{% block content %}
<h1>Sign up</h1>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <input type="submit">Sign up</input>
</form>
{% endblock %}

In users/templates/profile.html:

{% extends 'base.html' %}
{% block title %}User Profile{% endblock %}
{% block content %}
{% if user.is_authenticated %}
<p>User: {{ user.username }} logged in.</p>
<p><a href="{% url 'homepage' %}">homepage</a></p>
<p><a href="{% url 'logout' %}">logout</a></p>
{% else %}
<a href="{% url 'login' %}">login</a> |
<a href="{% url 'signup' %}">signup</a>
{% endif %}
{% endblock %}

And we map the route with it users/urls.py:

from django.urls import path
from . import views
from django.views.generic.base import TemplateView

urlpatterns = [
    path('signup/', views.SignUp.as_view(), name='signup'),
	path('profile/', TemplateView.as_view(template_name='profile.html'), name='profile'),
]

That url uses the views.Signup view as defined at users/views.py:

from django.urls import reverse_lazy
from django.views.generic import CreateView

from .forms import CustomUserCreationForm

class SignUp(CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy('login')
    template_name = 'signup.html'

Final tree

This is the final structure of the project:

.                                                                                                                                     
β”œβ”€β”€ db.sqlite3                                                                                                                        
β”œβ”€β”€ dcu                                                                                                                               
β”‚Β Β  β”œβ”€β”€ __init__.py                                                                                                                   
β”‚Β Β  β”œβ”€β”€ settings.py                                                                                                                   
β”‚Β Β  β”œβ”€β”€ urls.py                                                                                                                       
β”‚Β Β  └── wsgi.py                                                                                                                       
β”œβ”€β”€ Makefile                                                                                                                          
β”œβ”€β”€ manage.py
β”œβ”€β”€ README.md
β”œβ”€β”€ README.md~
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ templates
β”‚Β Β  β”œβ”€β”€ base.html
β”‚Β Β  β”œβ”€β”€ homepage.html
β”‚Β Β  └── homepage.html~
└── users
    β”œβ”€β”€ admin.py
    β”œβ”€β”€ apps.py
    β”œβ”€β”€ forms.py
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ migrations
    β”‚Β Β  β”œβ”€β”€ 0001_initial.py
    β”‚Β Β  └── __init__.py
    β”œβ”€β”€ models.py
    β”œβ”€β”€ templates
    β”‚Β Β  β”œβ”€β”€ profile.html
    β”‚Β Β  β”œβ”€β”€ profile.html~
    β”‚Β Β  β”œβ”€β”€ registration
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ logged_out.html
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ login.html
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ password_change_done.html
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ password_change_form.html
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ password_reset_confirm.html
    β”‚Β Β  β”‚Β Β  β”œβ”€β”€ password_reset_done.html
    β”‚Β Β  β”‚Β Β  └── password_reset_form.html
    β”‚Β Β  └── signup.html
    β”œβ”€β”€ tests.py
    β”œβ”€β”€ urls.py
    └── views.py

With the following classes:

model diagram

And this URLs:

/       django.views.generic.base.TemplateView  homepage
/accounts/login/        django.contrib.auth.views.LoginView     login
/accounts/logout/       django.contrib.auth.views.LogoutView    logout
/accounts/password_change/      django.contrib.auth.views.PasswordChangeView    password_change
/accounts/password_change/done/ django.contrib.auth.views.PasswordChangeDoneView        password_change_done
/accounts/password_reset/       django.contrib.auth.views.PasswordResetView     password_reset
/accounts/password_reset/done/  django.contrib.auth.views.PasswordResetDoneView password_reset_done
/accounts/profile/      django.views.generic.base.TemplateView  profile
/accounts/reset/<uidb64>/<token>/       django.contrib.auth.views.PasswordResetConfirmView      password_reset_confirm
/accounts/reset/done/   django.contrib.auth.views.PasswordResetCompleteView     password_reset_complete
/accounts/signup/       users.views.SignUp      signup
/admin/ django.contrib.admin.sites.index        admin:index
/admin/<app_label>/     django.contrib.admin.sites.app_index    admin:app_list
/admin/auth/group/      django.contrib.admin.options.changelist_view    admin:auth_group_changelist
/admin/auth/group/<path:object_id>/     django.views.generic.base.RedirectView
/admin/auth/group/<path:object_id>/change/      django.contrib.admin.options.change_view        admin:auth_group_change
/admin/auth/group/<path:object_id>/delete/      django.contrib.admin.options.delete_view        admin:auth_group_delete
/admin/auth/group/<path:object_id>/history/     django.contrib.admin.options.history_view       admin:auth_group_history
/admin/auth/group/add/  django.contrib.admin.options.add_view   admin:auth_group_add
/admin/auth/group/autocomplete/ django.contrib.admin.options.autocomplete_view  admin:auth_group_autocomplete
/admin/jsi18n/  django.contrib.admin.sites.i18n_javascript      admin:jsi18n
/admin/login/   django.contrib.admin.sites.login        admin:login
/admin/logout/  django.contrib.admin.sites.logout       admin:logout
/admin/password_change/ django.contrib.admin.sites.password_change      admin:password_change
/admin/password_change/done/    django.contrib.admin.sites.password_change_done admin:password_change_done
/admin/r/<int:content_type_id>/<path:object_id>/        django.contrib.contenttypes.views.shortcut      admin:view_on_site
/admin/users/customuser/        django.contrib.admin.options.changelist_view    admin:users_customuser_changelist
/admin/users/customuser/<id>/password/  django.contrib.auth.admin.user_change_password  admin:auth_user_password_change
/admin/users/customuser/<path:object_id>/       django.views.generic.base.RedirectView
/admin/users/customuser/<path:object_id>/change/        django.contrib.admin.options.change_view        admin:users_customuser_change
/admin/users/customuser/<path:object_id>/delete/        django.contrib.admin.options.delete_view        admin:users_customuser_delete
/admin/users/customuser/<path:object_id>/history/       django.contrib.admin.options.history_view       admin:users_customuser_history
/admin/users/customuser/add/    django.contrib.auth.admin.add_view      admin:users_customuser_add
/admin/users/customuser/autocomplete/   django.contrib.admin.options.autocomplete_view  admin:users_customuser_autocomplete

Repo

There is a Github repo after following all the above steps at: https://github.com/marcanuy/django-custom-users

References

Uruguay
Marcelo Canina
I'm Marcelo Canina, a developer from Uruguay. I build websites and web-based applications from the ground up and share what I learn here.
comments powered by Disqus

How to use Django's builtin User system, adding a User app to handle users to your Django project, including signup and login pages

Except as otherwise noted, the content of this page is licensed under CC BY-NC-ND 4.0 ·