narancs's blog

Setting up a Django project with custom User model

Introduction

This is the first part of my JWT authentication with Django and React series. In this part I will explain the following:

  • Installing latest Python version from source (optional)
  • Setting up virtual environment for Python (optional)
  • Creating Django project
  • Configuring Django to use PostgreSQL
  • Customizing the User model

So here we are just going to setup the very basic things we will need in the later parts. I am going to use to following software versions:

Installing latest Python version from source (optional)

Debian comes with Python 3 install by default. However, it does not have the latest version installed. The version I have installed is 3.11.2. At the time of this writing, the latest stable version available is 3.12.3. So, I am going to download the source code for that and build it. (For Windows or MacOS you can just download the installer from https://www.python.org/downloads/release/python-3123/)

First, let’s install the dependencies. These packages are required to build Python with all the optional modules.

				
					sudo apt update
sudo apt upgrade -y
sudo apt-get build-dep python3
sudo apt-get install build-essential gdb lcov pkg-config \
      libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \
      libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \
      lzma lzma-dev tk-dev uuid-dev zlib1g-dev
				
			

Once these are installed, let’s grab the source code for CPython, then configure it. (CPython is the default and most widely used implementation of the Python programming language. It’s written in C and is maintained by the Python Software Foundation.)

				
					wget https://www.python.org/ftp/python/3.12.3/Python-3.12.3.tgz
tar -xf Python-3.12.3.tgz
cd Python-3.12.3/
./configure --with-pydebug
				
			

Once configure is done, we can compile CPython.

				
					make -s -j8
				
			

The -j8 flag means it will utilize up to 8 CPU cores for the building process. You can configure it according to your machine.

At the end of the build you should see a success message, followed by a list of extension modules that haven’t been built because their dependencies were missing. However, since we installed all optional dependencies, we should only get a message like this:

				
					narancs@debian:~/Python-3.12.3$ make -s -j8
Checked 111 modules (31 built-in, 79 shared, 1 n/a on linux-x86_64, 0 disabled, 0 missing, 0 failed on import)

				
			
Finally, lets run make altinstall. Like make install, make altinstall installs Python on your system. However, it installs it in such a way that it doesn’t interfere with the system Python (if there is one) or any other versions of Python that might already be installed. It does it by installing python with a different name (e.g. python3.12 instead of python)
				
					sudo make altinstall
				
			

At this point, we should be able to run Python version 3.12.3:

				
					narancs@debian:~/Documents/Python-3.12.3$ python3.12
Python 3.12.3 (main, Apr 26 2024, 21:03:33) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

				
			

For further information about building Python from source code please visit https://devguide.python.org/getting-started/setup-building/index.html.

Create virtual environment (optional)

A Python virtual environment is an isolated environment where you can install packages and dependencies separately from your system-wide Python installation. It allows you to create a sandboxed environment for each project, preventing conflicts between different project dependencies. This isolation ensures that your project remains unaffected by changes or updates to the system-wide Python or other projects. It’s best practice to use virtual environments to maintain project consistency and dependency management.

We can create virtual environments in different ways. One option is to use the virtualenv tool. This is what I am going to do, but with an extra helper package on top of it called virtualenvwrapper. It is an extension of the virtualenv tool to help with the creation and deletion of virtual environments.

Installing virtualenvwrapper

Install the required packages:

				
					sudo apt install python3-pip
python3.12 -m pip install virtualenv virtualenvwrapper
				
			

If your receive a similar warning during pip install, then you need to add the mentioned path to the PATH environment variable.

				
					WARNING: The script virtualenv is installed in '/home/narancs/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
				
			
To add ‘/home/narancs/.local/bin’ to PATH, we need to edit the .bashrc file in our home directory, and add the following line:
				
					export PATH="$HOME/.local/bin:$PATH"
				
			

These changes only take effect if we reboot our machine, or if we source the file manually:

				
					source ~/.bashrc
				
			
If everything was done properly, now we should be able to find the executables with the which command:
				
					narancs@debian:~/Documents/Python-3.12.3$ which virtualenv
/home/narancs/.local/bin/virtualenv
narancs@debian:~/Documents/Python-3.12.3$ which virtualenvwrapper.sh
/home/narancs/.local/bin/virtualenvwrapper.sh
				
			
Now that we have virtualenv and virtalenvwrapper installed, we need to edit our .bashrc file again to maintain the required configuration for virtualenvwrapper. These are the following:
  • path to python3 binary executable: run which python3.12 to get this (it is most likely /usr/local/bin/python3.12)
  • path to virtualenv binary executable: take this from the output of the which command we executed above (in my case /home/narancs/.local/bin/virtualenv)
  • path to virtualenvwrapper.sh script: take this from the output of the which command we executed above (in my case /home/narancs/.local/bin/virtualenvwrapper.sh)
Based on the above results, add the following to ~/.bashrc:
				
					export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3.12
export VIRTUALENVWRAPPER_VIRTUALENV=/home/narancs/.local/bin/virtualenv
source /home/narancs/.local/bin/virtualenvwrapper.sh
				
			
Then let’s source .bashrc to make these changes effective:
				
					narancs@debian:~/Documents/Python-3.12.3$ source ~/.bashrc
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/premkproject
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/postmkproject
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/initialize
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/premkvirtualenv
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/postmkvirtualenv
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/prermvirtualenv
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/postrmvirtualenv
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/predeactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/postdeactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/preactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/postactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/get_env_details
narancs@debian:~/Documents/Python-3.12.3$ 

				
			

Now we can create virtual environments. Let’s create one for our django project.

Creating the virtual environment

The virtualenvwrapper utility provides us convenient helper commands to manage virtual environments. To create a new environment, we just need to run mkvirtualenv followed by the name of the environment we want to create.
				
					mkvirtualenv django-jwt-auth
				
			

We should get an output like this:

				
					narancs@debian:~/Documents/Python-3.12.3$ mkvirtualenv django-jwt-auth
created virtual environment CPython3.12.3.final.0-64 in 337ms
  creator CPython3Posix(dest=/home/narancs/.virtualenvs/django-jwt-auth, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, via=copy, app_data_dir=/home/narancs/.local/share/virtualenv)
    added seed packages: pip==24.0
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/django-jwt-auth/bin/predeactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/django-jwt-auth/bin/postdeactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/django-jwt-auth/bin/preactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/django-jwt-auth/bin/postactivate
virtualenvwrapper.user_scripts creating /home/narancs/.virtualenvs/django-jwt-auth/bin/get_env_details
(django-jwt-auth) narancs@debian:~/Documents/Python-3.12.3$
				
			

We can see, that it created a virtual environment with CPython version 3.12.3. Also, we can see the the environment was created in ~/.virtualenvs directory. Virtualenvwrapper organizes virtual environments in this central directory by default.

The environment is also activated immediately. We can see that from our prompt that starts with (django-jwt-auth). If we want to leave the environment, we can do that simply by running deactivate command. To re-activate the environment, we need to run workon django-jwt-auth. Finally, if we ever want to remove the environment, we can do that with rmvirtualenv django-jwt-auth.

Now we have our virtual environment created. Let’s create the Django project.

Create Django project

Let’s install Django package and also Django REST Framework that we are going to use extensively in the future. I am also installing REST Framework’s Simple JWT package that we are going to use in the next part, when we set up JWT authentication.

				
					pip install Django djangorestframework djangorestframework-simplejwt
				
			

Please note that the directory where we are at the moment is irrelevant. When we utilize the ‘pip’ command to install packages within a virtual environment, they will automatically be stored in the ‘~/.virtualenvs’ directory. This feature is another advantage of virtualenvwrapper.

Now we have Django installed, so we can create the Django project.

				
					cd ~/Documents
django-admin startproject django_jwt_auth
cd django_jwt_auth
				
			
This will create an django_jwt_auth directory that contains another directory with the same name. I like to rename that subdirectory to config:
				
					mv django_jwt_auth/ config
				
			
As a result we will end up with the following file structure inside our project directory (django_jwt_auth):
				
					.
├── config/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
				
			

Lets create a requirements.txt file to track our project dependencies.

				
					pip freeze > requirements.txt
				
			

We renamed a directory that was created by Django’s startproject command. This directory is referenced in few places in the code, so currently the server will not start. We need to fix these references.

In settings.py make the following changes:
				
					# Replace django_jwt_auth to config
ROOT_URLCONF = 'django_jwt_auth.urls'
ROOT_URLCONF = 'config.urls'
# Replace django_jwt_auth to config
WSGI_APPLICATION = 'django_jwt_auth.wsgi.application'
WSGI_APPLICATION = 'config.wsgi.application'
				
			
In manage.py make the following changes:
				
					# Replace django_jwt_auth with config
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_jwt_auth.settings')
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
				
			
Now we can start the server with python manage.py runserver:
				
					(django-jwt-auth) narancs@debian:~/Documents/django_jwt_auth$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
April 26, 2024 - 17:27:42
Django version 5.0.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
				
			

The code written so far can be found in the following pull request: https://github.com/narancs93/django-jwt-auth/pull/1

Do NOT apply the migrations just now! We will need to overwrite the default User model, so we can customize it later on.

If we apply the migrations right now, the default User model will be migrated, and it will be cumbersome to change it to our custom model.

But before we do that, let’s configure Django to use PostgreSQL database instead of the default SQLite.

Change database to Postgres with Docker

The default database setting uses SQLite that is good for development purposes, but I prefer to switch to PostgreSQL. It is easy to do using Docker. We can spin up a postgres docker container quickly and use that as our database.

Create postgres docker container

Follow installation guide for Docker: https://docs.docker.com/engine/install/

To spin up a Postgres DB we will use Docker compose. First create docker-compose.yml:

				
					services:
  postgres:
    image: postgres:latest
    ports:
      - "5432:5432"
    env_file:
      - .env
    volumes:
      - postgresql_data:/var/lib/postgresql/data
volumes:
  postgresql_data:
				
			

Then create .env file to store the required environment variables:

				
					POSTGRES_USER=django-jwt-auth
POSTGRES_PASSWORD="mrL:nkQjz&$+Bv2DKV*)}x"
POSTGRES_DB=django
				
			

Finally, start the container:

				
					docker compose up -d
				
			

Verify that the container is running:

				
					docker ps
				
			

We should see a similar output:

				
					narancs@debian:~/Documents/django_jwt_auth$ docker ps
CONTAINER ID   IMAGE             COMMAND                  CREATED              STATUS        PORTS                                       NAMES
0026ca8ecb02   postgres:latest   "docker-entrypoint.s…"   About a minute ago   Up 1 second   0.0.0.0:5432->5432/tcp, :::5432->5432/tcp   django_jwt_auth-postgres-1
				
			

Configure Django to use PostgreSQL

If we want to connect to a PostgreSQL database via Python, we need to install the database adapter. I also install the dotenv package, so we can access the variables defined in the .env file using Python code.

				
					pip install psycopg2-binary python-dotenv
				
			

Save the dependencies:

				
					pip freeze > requirements.txt
				
			

Let’s add a helper function at the beginning of settings.py file that can be used to access the environment variables.

				
					import os
from django.core.exceptions import ImproperlyConfigured
def get_env_variable(var_name):
    """Get the environment variable or return exception."""
    try:
        from dotenv import load_dotenv
        load_dotenv()
        return os.environ[var_name]
    except KeyError:
        error_msg = f'Set the {var_name} environment variable'
        raise ImproperlyConfigured(error_msg)
				
			
Now make the following change in settings.py :
				
					# Change this
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
# To this
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql_psycopg2",
        "NAME": get_env_variable("POSTGRES_DB"),
        "USER": get_env_variable("POSTGRES_USER"),
        "PASSWORD": get_env_variable("POSTGRES_PASSWORD"),
        "HOST": "localhost",
        "PORT": "5432",
    }
}
				
			

Test if we can start the server:

				
					python manage.py runserver
				
			

If the changes have been done properly, then the server should start without issues. Remember to not apply the migrations yet. As the next step we will override the default User model, and we want that to be included in the first migration.

For a detailed view of the changes, check out the pull request: https://github.com/narancs93/django-jwt-auth/pull/2

I forgot to add volume to PostgreSQL at the beginning, so I included that in a separate pull request: https://github.com/narancs93/django-jwt-auth/pull/3

Customize the User model

We will create the first app in our Django project where we will create our own User model. The app name will be users.

				
					python manage.py startapp users
				
			
In the users app open the models.py file that currently contains only this:
				
					from django.db import models
# Create your models here.
				
			

Here we are going to create the User model. The required fields for a user to login is username and password by default. I want to change that so the users are logging in via email address and not username. This is my personal preference, and I think is the most common. So we will need to:

  • override the default username field
  • override the create methods, so when we create users it will require email parameter instead of username. We will do this in a custom UserManager class.
				
					from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models

class UserManager(BaseUserManager):
    """Define a model manager for User model with no username field."""
    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('User must have email address.')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user
    def create_user(self, email, password=None):
        return self._create_user(email, password)
    def create_superuser(self, email, password, **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, password, **extra_fields)

class User(AbstractUser):
    username = None
    email = models.EmailField(
        verbose_name='email address',
        max_length=255,
        unique=True,
    )
    objects = UserManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []
    def __str__(self):
        return f'{self.email} (id={self.id})'
				
			

After we defined our model, we need to tell Django that this is what we want to use instead of the default model. Django allows us to do that using the AUTH_USER_MODEL setting that references a custom model.

In settings.py add the following line:

				
					AUTH_USER_MODEL = "users.User"
				
			
This setting is not included in settings.py by default, so we need to add it ourselves. I like to put it between DATABASES and AUTH_PASSWORD_VALIDATORS settings.

The last thing we need to do is to add the ‘users’ app to INSTALLED_APPS. I like to separate my installed apps into 3 groups: Django built-ins, packaged apps and apps developed by me.

Lets change this code in settings.py:

				
					# Change this
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
# To this
DEFAULT_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
THIRD_PARTY_APPS = ["rest_framework"]
DEVELOPED_APPS = ["users"]
INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + DEVELOPED_APPS
				
			

Now let’s create migration for our custom user model and apply the migrations:

				
					python manage.py makemigrations
python manage.py migrate
				
			

For a detailed view of the changes, check out the pull request: https://github.com/narancs93/django-jwt-auth/pull/4

To finish up the custom User model part, the last thing we will do is register the model on the admin page.

Register User model on admin page

The default user model is registered by default, but since we have overridden that model, we won’t see it. We have to register our own model.

Edit admin.py:

				
					from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from .models import User

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    fieldsets = (
        (None, {"fields": ("email", "password")}),
        (_("Personal info"), {"fields": ("first_name", "last_name")}),
        (
            _("Permissions"),
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                    "user_permissions",
                ),
            },
        ),
        (_("Important dates"), {"fields": ("last_login", "date_joined")}),
    )
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "password1", "password2"),
            },
        ),
    )
    list_display = ("email", "first_name", "last_name", "is_staff")
    list_filter = ("is_staff", "is_superuser", "is_active", "groups")
    search_fields = ("first_name", "last_name", "email")
    ordering = ("email",)
    filter_horizontal = (
        "groups",
        "user_permissions",
    )

				
			

We have basically taken the original UserAdmin from Django source code, and made the necessary changes to use “email” field instead of “username” field.

For a detailed view of the changes, check out the pull request: https://github.com/narancs93/django-jwt-auth/pull/5

Conclusion

We are finished with the basic Django project setup. We have installed the latest Python version, and created a virtual environment for our project. Then we created our Django project, and added a custom User model that we can extend later on if required. We also configured PostgreSQL instead of the default SQLite database. Finally we registered our User model on the admin page.

As a final step, let’s create a super user and login to the admin page to verify that everything works as expected so far.

				
					python manage.py createsuperuser
				
			

Once the user is created, open http://localhost:8000/admin in our browser.

Admin page login with email address and password

We can see that the username field is changed to email address as we have configured it. Also, the model is registered to the admin page with the correct fields. For example, the add user form is asking for an email field instead of username, because we adjusted the UserAdmin class.

We have successfully verified that the configuration is working as expected.

In the next part we will start setting up the JWT authentication by creating the token obtain and token refresh URLs.

Table of Contents

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

Related posts

0
Would love your thoughts, please comment.x
()
x