Advanced Django cheat sheet

July 04, 2023

📜 Advanced Django Cheat Sheet

Be aware it's not an exhaustive list. If you have ideas, correction or recommendation do not hesitate and do so on Github.

Sections

Preparing enviro­nnement

  • Create project folder and navigate to it.
mkdir projec­t_name && cd $_
  • Create Python virtual env.
python -m venv env_name
  • Activate virtual env. (Replace "­bin­" by "­Scr­ipt­s" in Windows).
source env_na­me­\bin­\ac­tivate
  • Deactivate virtual env.
deactivate
  • Install Django.
pip install django~=4.2.2
  • Create requir­ements file.
pip freeze > requir­eme­nts.txt
  • Install all required depend­encies based on your pip freeze command.
pip install -r requir­eme­nts.txt

Creating a Django project

  • Starting a new Django project. A config directory wil be created in your current directory.
django-admin startproject config .
  • Running the server
python manage.py runserver

Creating a Django app

  • Creating an my_app directory and all default files/f­olders inside.
python manage.py startapp my_app
  • Adding the app to settings.py.
INSTAL­LED­_APPS = [
 'my_app',
 ...
  • Adding app urls into the urls.py from project folder.
urlpat­terns = [
 path('admin/', admin.s­it­e.u­rls),
 path('my_app/', include('my_app.urls')),
]

Custom User

Custom User Model

Django documentation: Using a custom user model when starting a project

  1. Create a CustomUser model
    The CustomUser model will live within its own app (for example, 'accounts').
    # accounts/models.py
    from django.contrib.auth.models import AbstractUser
    from django.db import models
    
    class CustomUser(AbstractUser):
    	pass
    
  2. Update settings.py
    • Add the 'accounts' app to the INSTALLED_APPS section
    • Add a AUTH_USER_MODEL config:
    AUTH_USER_MODEL = "accounts.CustomUser"
    
    • Create migrations file
    python manage.py makemigrations accounts
    
    • Migrate

Custom User Forms

Updating the built-in forms to point to the custom user model instead of User.

# accounts/forms.py
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, UserChangeForm

class CustomUserCreationForm(UserCreationForm):
	class Meta:
		model = get_user_model()
		fields = (
			"email",
			"username",
		)
	

class CustomUserChangeForm(UserChangeForm):
	class Meta:
		model = get_user_model()
		fields = (
			"email",
			"username",
		)

Custom User Admin

Extending the existing UserAdmin into CustomUserAdmin.

# accounts/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

CustomUser = get_user_model()

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

Superuser

python manage.py createsuperuser

Migration

makemigration and migrate

Migrations in the Django Doc

makemigrations: This command generates migration files based on the changes detected in your models. It compares the current state of your models with the migration files already created and determines the SQL commands required to propagate the changes to your database schema.

python manage.py makemigrations

migrate: This command applies the migration files to your database, executing the SQL commands generated by makemigrations.

python manage.py migrate

This will update your database schema with the changes made to your models.

Fake initial migration

In Django, a "fake initial migration" refers to a concept where you mark a migration as applied without actually executing the database schema changes associated with that migration. It allows you to synchronize the state of the migrations with the database without performing any database modifications.

python manage.py migrate --fake-initial

It's important to note that faking the initial migration assumes that the existing database schema matches what the initial migration would have created.

Models

Django Documentation: Models

Model Style Ordering

  • Choices
  • Database fields
  • Custom manager attributes
  • Meta class
  • def __str__()
  • def save()
  • def get_absolute_url()
  • Custom methods

Model and Field Names

# my_book_app/models.py
from django.db import models

class Book(models.Model):
	title = models.CharField(max_length=100)

Models represents a single object and should always be Capitalized and singular (Book, not Books).

Fields should all be snake_case, not camelCase.

Choices

If choices are defined for a given model field, define each choice as a tuple of tuples, with an all-uppercase name as a class attribute on the model (source).

# my_book_app/models.py
from django.db import models

class Book(models.Model):
	BOOK_CATEGORY = (
		("FICTION", "A fiction book"),
		("NON_FICTION", "A non-fiction book")
	)
	title = models.CharField(max_length=100)
	book_type = models.CharField(
		choices=BOOK_CATEGORY,
		max_lenght=100,
		verbose_name="type of book",
	)

Blank and Null Fields

  • Null: Database-related. Defines if a given database column will accept null values or not.
  • Blank: Validation-related. It will be used during forms validation, when calling form.is_valid().

Do not use null with string-based fields like CharField or TextField as this leads to two possible values for "no data". The Django convention is instead to use the empty string "", not NULL (source.

Meta class

Django Documentation: Meta class

An example, using [indexes](#indexes), ordering, verbose_name and verbose_name_plural.
(Don't order results if you don't need to. There can be performance hit to ordering results.)

# my_book_app/models.py
from django.db import models

class Book(models.Model):
	BOOK_CATEGORY = (
		("FICTION", "A fiction book"),
		("NON_FICTION", "A non-fiction book")
	)
	title = models.CharField(max_length=100)
	book_type = models.CharField(
		choices=BOOK_CATEGORY,
		max_lenght=100,
		verbose_name="type of book",
	)
	
	class Meta:
		indexes = [models.Index(fields=["title"])]
		ordering = ["-title"]
		verbose_name = "book"
		verbose_name_plural = "books"

The str Method

Django Documentation: str()
The str method defines a string representation, a more descriptive name/title, for any of our objects that is displayed in the Django admin site and in the Django shell.

# my_book_app/models.py
from django.db import models

class Book(models.Model):
	BOOK_CATEGORY = (
		("FICTION", "A fiction book"),
		("NON_FICTION", "A non-fiction book")
	)
	title = models.CharField(max_length=100)
	book_type = models.CharField(
		choices=BOOK_CATEGORY,
		max_lenght=100,
		verbose_name="type of book",
	)
	
	class Meta:
		indexes = [models.Index(fields=["title"])]
		ordering = ["-title"]
		verbose_name = "book"
		verbose_name_plural = "books"
	
	def __str__(self):
		return self.title

The get_absolute_url Method

Django Documentation: get_absolute_url()
The str method method sets a canonical URL for the model.

# my_book_app/models.py
from django.db import models

class Book(models.Model):
	BOOK_CATEGORY = (
		("FICTION", "A fiction book"),
		("NON_FICTION", "A non-fiction book")
	)
	title = models.CharField(max_length=100)
	book_type = models.CharField(
		choices=BOOK_CATEGORY,
		max_lenght=100,
		verbose_name="type of book",
	)
	
	class Meta:
		indexes = [models.Index(fields=["title"])]
		ordering = ["-title"]
		verbose_name = "book"
		verbose_name_plural = "books"
	
	def __str__(self):
		return self.title
		
	def get_absolute_url(self):
		return reverse("book_detail", args=[str(self.id)])

Using the get_absolute_url in our templates:

<a href="{{ object.get_absolute_url }}/">{{ object.title }}</a>

UniqueConstraint

Django Documentation: uniqueConstraint

Use UniqueConstraint when you want to enforce uniqueness on a combination of fields or need additional functionality like custom constraint names or conditional constraints

class Booking(models.Model):
    room = models.ForeignKey(Room, on_delete=models.CASCADE)
    date = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['room', 'date'], name='unique_booking')
        ]

Models: Further reading

Model Managers

Django Documentation: Managers

Giving a custom name to the default manager

class Author(models.Model):
    ...
    authors = models.Manager() //now the default manager is named as authors

All the operation on the student database table have to be done using the “authors” manager

Author.authors.filter(...)

Creating custom managers

Django Documentation: Custom managers

from django.db import models
from django.db.models.functions import Coalesce


class PollManager(models.Manager):
    def with_counts(self):
        return self.annotate(num_responses=Coalesce(models.Count("response"), 0))


class OpinionPoll(models.Model):
    question = models.CharField(max_length=200)
    objects = PollManager()


class Response(models.Model):
    poll = models.ForeignKey(OpinionPoll, on_delete=models.CASCADE)
    # ...

If you use custom Manager objects, take note that the first Manager Django encounters (in the order in which they’re defined in the model) has a special status. Django interprets the first Manager defined in a class as the “default” Manager, and several parts of Django (including dumpdata) will use that Manager exclusively for that model. As a result, it’s a good idea to be careful in your choice of default manager in order to avoid a situation where overriding get_queryset() results in an inability to retrieve objects you’d like to work with.

Modifying a manager’s initial QuerySet

Django Documenation: Modifying a managers's initial QuerySet

# First, define the Manager subclass.
class DahlBookManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(author="Roald Dahl")


# Then hook it into the Book model explicitly.
class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager()  # The default manager.
    dahl_objects = DahlBookManager()  # The Dahl-specific manager.

With this sample model, Book.objects.all() will return all books in the database, but Book.dahl_objects.all() will only return the ones written by Roald Dahl.

Model registration in admin

Django doc: ModelAdmin objects

Model registration in Django's admin interface is the process of making your models accessible through the admin site.

from django.contrib import admin
from .models import Author

admin.site.register(Author)
  • Customizing the display of a model
from django.contrib import admin
from .models import Book

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'publication_date')

admin.site.register(Book, BookAdmin)
  • Adding a search field
from django.contrib import admin
from .models import Publisher

class PublisherAdmin(admin.ModelAdmin):
    search_fields = ['name']

admin.site.register(Publisher, PublisherAdmin)
  • Adding filters
from django.contrib import admin
from .models import Category

class CategoryAdmin(admin.ModelAdmin):
    list_filter = ('is_active',)

admin.site.register(Category, CategoryAdmin)
  • Inline formsets
from django.contrib import admin
from .models import Order, OrderItem

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 1

class OrderAdmin(admin.ModelAdmin):
    inlines = [OrderItemInline]

admin.site.register(Order, OrderAdmin)

Django Signals

  • pre_save:
    Django Doc: pre_save
    Using a pre_save signal is required to execute code related to another part of your application prior to saving the object in the database.
from django.db.models.signals import pre_save
from django.dispatch import receiver

@receiver(pre_save, sender=NewOrder)
def validate_order(sender, instance, **kwargs):
    stock_item = Stock.objects.get(id=instance.stock_item.id)

    if instance.quantity > stock_item.quantity:
        raise Exception("Insufficient stock quantity.")
  • post_save:
    Django Doc: post_save
    Using a post_save signal is required to execute code related to another part of your application after the object is saved to the database.
from django.db.models.signals import post_save
from django.dispatch import receiver


@receiver(post_save, sender=NewOrder)
def remove_from_inventory(sender, instance, **kwargs):
    stock_item = Inventory.objects.get(id=instance.stock_item.id)
    stock_item.quantity = stock_item.quantity - instance.quantity

    stock_item.save()
  • pre_delete:
    Django Doc: pre_delete
    Using a pre_delete signal is necessary to execute code related to another part of your application before the deletion event of an object occurs.
from django.db.models.signals import pre_delete
from django.dispatch import receiver


@receiver(pre_delete, sender=Book)
def pre_delete_book(sender, **kwargs):
    print("You are about to delete a book")
  • post_delete Django Doc: post_delete Using a post_delete signal is necessary to execute code related to another part of your application after the deletion event of an object occurs.
@receiver(post_delete, sender=Book)
def delete_book(sender, **kwargs):
    print("You have just deleted a book")
  • m2m_changed Django Doc: m2m_changed To send a Django signal when a ManyToManyField is changed on a model instance.

Consider this model:

class Student(models.Model):
    # ...


class Course(models.Model):
    students = models.ManyToManyField(Student)

m2m_changed signal:

from django.db.models.signals import m2m_changed

def my_signal_name(sender, instance, **kwargs):
    students = instance.students.all()
    # ...

m2m_changed.connect(my_signal_name, sender=Course.students.through)

Queries and QuerySet

Django Documentation: Making queries

Using Q objects for complex queries

Django Documentation: Q objects

Q objects can be combined using the & (AND) and | (OR) operators

Inventory.objects.filter(
    Q(quantity__lt=10) &
    Q(next_shipping__gt=datetime.datetime.today()+datetime.timedelta(days=10))
)
Inventory.objects.filter(
    Q(name__icontains="robot") |
    Q(title__icontains="vacuum")

Aggregation

Django documenation: Aggregation

In Django, aggregation allows you to perform calculations such as counting, summing, averaging, finding the maximum or minimum value, and more, on a specific field or set of fields in a queryset.

from django.db.models import Sum

total_ratings = Movies.objects.aggregate(ratings_sum=Sum('ratings_count'))

Utilizing Aggregation in Views and Templates

In the view:

from django.shortcuts import render

def example(request):
    data = Movies.objects.aggregate(ratings_sum=Sum('ratings_count'))
    return render(request, 'index.html', {'data': data})

In the template:

<p>Total Ratings: {{ data.ratings_sum }}</p>

Latest element in QuerySet

Django Documentation: latest()

This example returns the latest Entry in the table, according to the pub_date field:

Entry.objects.latest("pub_date")

You can also choose the latest based on several fields. For example, to select the Entry with the earliest expire_date when two entries have the same pub_date:

Entry.objects.latest("pub_date", "-expire_date")

The negative sign in '-expire_date' means to sort expire_date in descending order. Since latest() gets the last result, the Entry with the earliest expire_date is selected.

Union of QuerySets

union() in the Django Doc

Uses SQL’s UNION operator to combine the results of two or more QuerySets. For example:

>>> qs1.union(qs2, qs3)

union() return model instances of the type of the first QuerySet even if the arguments are QuerySets of other models. Passing different models works as long as the SELECT list is the same in all QuerySets (at least the types, the names don’t matter as long as the types are in the same order). In such cases, you must use the column names from the first QuerySet in QuerySet methods applied to the resulting QuerySet. For example:

>>> qs1 = Author.objects.values_list("name")
>>> qs2 = Entry.objects.values_list("headline")
>>> qs1.union(qs2).order_by("name")

In addition, only LIMIT, OFFSET, COUNT(*), ORDER BY, and specifying columns (i.e. slicing, count(), exists(), order_by(), and values()/values_list()) are allowed on the resulting QuerySet.

Fixing the N+1 Queries Problem

See select_related and prefetch_related

Performing raw SQL queries

Django Documentation: Performing raw SQL queries

from django.db import models


class Project(models.Model):
    title = models.CharField(max_length=70)
Project.objects.raw('SELECT id, title FROM myapp_project')

Custom sql or raw queries sould be both used with extrement caution since they could open up a vulnerability to SQL injection.

View

Function-based views (FBVs)

Django docucmentation: Writing views

From Django Views - The Right Way: Why TemplateResponse over render ?

The issue with just using render is that you get a plain HttpResponse object back that has no memory that it ever came from a template. Sometimes, however, it is useful to have functions return a value that does remember what it’s “made of” — something that stores the template it is from, and the context. This can be really useful in testing, but also if we want to something outside of our view function (such as decorators or middleware) to check or even change what’s in the response before it finally gets ‘rendered’ and sent to the user.

from django.template.response import TemplateResponse
from django.shortcuts import get_object_or_404, redirect

from .forms import TaskForm, ConfirmForm
from .models import Task


def task_list_view(request):
    return TemplateResponse(request, 'task_list.html', {
        'tasks': Task.objects.all(),
    })


def task_create_view(request):
    if request.method == 'POST':
        form = TaskForm(data=request.POST)
        if form.is_valid():
            task = form.save()
            return redirect('task_detail', pk=task.pk)

    return TemplateResponse(request, 'task_create.html', {
        'form': TaskForm(),
    })


def task_detail_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    return TemplateResponse(request, 'task_detail.html', {
        'task': task,
    })


def task_update_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = TaskForm(instance=task, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('task_detail', pk=task.pk)

    return TemplateResponse(request, 'task_edit.html', {
        'task': task,
        'form': TaskForm(instance=task),
    })


def task_delete_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = ConfirmForm(data=request.POST)
        if form.is_valid():
            task.delete()
            return redirect('task_list')

    return TemplateResponse(request, 'task_delete.html', {
        'task': task,
        'form': ConfirmForm(),
    })

Class-based views (CBVs)

Django Documentation : Class-based views

from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy

from .models import Task


class TaskListView(ListView):
    model = Task
    template_name = "task_list.html"


class BlogDetailView(DetailView):
    model = Task
    template_name = "task_detail.html"


class TaskCreateView(CreateView):
    model = Task
    template_name = "task_create.html"
    fields = ["name", "body", "author"]


class TaskUpdateView(UpdateView):
    model = Task
    template_name = "task_edit.html"
    fields = ["name", "body"]


class TaskDeleteView(DeleteView):
    model = Task
    template_name = "task_delete.html"
    success_url = reverse_lazy("task_list")

Redirect from view:

Django Documentation: redirect()

from django.shortcuts import redirect

# Using the redirect() function by passing an object:
def my_view(request):
    ...
    obj = MyModel.objects.get(...)
    return redirect(obj)

# Using the redirect() function by passing the name of a view
# and optionally some positional or keyword arguments:
def my_view(request):
    ...
    return redirect("some-view-name", foo="bar")

# Using the redirect() function by passing an hardcoded URL:
def my_view(request):
    ...
    return redirect("/some/url/")
    # This also works with full URLs:
    # return redirect("https://example.com/")

By default, redirect() returns a temporary redirect.
All of the above forms accept a permanent argument; if set to True a permanent redirect will be returned:

def my_view(request):
    ...
    obj = MyModel.objects.get(...)
    return redirect(obj, permanent=True)

View: Further reading

Routing

Django Documentation: django.urls functions for use in URLconfs

  • path(): Returns an element for inclusion in urlpatterns
from django.urls import include, path

urlpatterns = [
    path("index/", views.index, name="main-view"),
    path("bio/<username>/", views.bio, name="bio"),
    path("articles/<slug:title>/", views.article, name="article-detail"),
    path("articles/<slug:title>/<int:section>/", views.section, name="article-section"),
    path("blog/", include("blog.urls")),
    ...,
]
  • re_path(): Returns eturns an element for inclusion in urlpatterns.
    The route argument should be a string or gettext_lazy() that contains a regular expression compatible with Python’s re module.
from django.urls import include, re_path

urlpatterns = [
    re_path(r"^index/$", views.index, name="index"),
    re_path(r"^bio/(?P<username>\w+)/$", views.bio, name="bio"),
    re_path(r"^blog/", include("blog.urls")),
    ...,
]
  • include(): A function that takes a full Python import path to another URLconf module that should be “included” in this place.
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
	path("/admin/", admin.site.urls),
	path("books/", include ("books.urls")),
]
  • static(): Helper function to return a URL pattern for serving files in debug mode.
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... the rest of your URLconf goes here ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Authentication

Authentication views and URLs

Django Documentation: Using the views

Add Django site authentication urls (for login, logout, password management):

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
]

Urls provided by the auth app:

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']

Updating settings.py with LOGIN_REDIRECT_URL and LOGOUT_REDIRECT_URL

# config/urls.py
    ...
    path("", TemplateView.as_view(template_name="home.html"), name="home"),
    ...

# config/settings.py
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"

Signup

To create a sign up page we will need to make our own view and url.

python manage.py startapp accounts
# config/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "accounts",
]

Then add a project-level url for the accounts app above our included Django auth app.

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),  # new
    path("accounts/", include("django.contrib.auth.urls")),
    path("", TemplateView.as_view(template_name="home.html"), name="home"),
]

The views file:

# accounts/views.py
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy("login")
    template_name = "registration/signup.html"

From LearnDjango:

We're subclassing the generic class-based view CreateView in our SignUp class. We specify the use of the built-in UserCreationForm and the not-yet-created template at signup.html. And we use reverse_lazy to redirect the user to the login page upon successful registration.

Create a new urls file in the accounts app.

# accounts/urls.py
from django.urls import path

from .views import SignUpView


urlpatterns = [
    path("signup/", SignUpView.as_view(), name="signup"),
]

Then, create a new template templates/registration/signup.html

<!-- templates/registration/signup.html -->
{% extends "base.html" %}

{% block title %}Sign Up{% endblock %}

{% block content %}
  <h2>Sign up</h2>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign Up</button>
  </form>
{% endblock %}

Password reset

For development purposes Django let us store emails either in the console or as a file.

  • Console backend:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
  • File backend:
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location

For production, see Sending Email

OAuth

The Django OAuth Toolkit package provides OAuth 2.0 support and uses OAuthLib.

Authentication: Further reading

Custom Permissions

Django Doc: Custom permissions

Adding custom permissions to a Django model:

from django.db import models

class Task(models.Model):
    title = models.CharField(max_length=70)
    body = models.TextField()
    is_opened = models.Boolean(default=False)

    class Meta:
        permissions = [
            ("set_task_status", "Can change the status of tasks")
        ]

the following checks if a user may close tasks:

user.has_perm("app.close_task")

You still have to enforce it in the views:

For function-based views, use the permission_required decorator:

from django.contrib.auth.decorators import permission_required

@permission_required("book.view_book")
def book_list_view(request):
    return HttpResponse()

For class-based views, use the PermissionRequiredMixin:

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView

from .models import Book

class BookListView(PermissionRequiredMixin, ListView):
    permission_required = "book.view_book"
    template_name = "books/book_list.html"
    model = Book

permission_required can be either a single permission or an iterable of permissions. If using an iterable, the user must possess all the permissions in order to access the view.

from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import ListView

from .models import Book

class BookListView(PermissionRequiredMixin, ListView):
    permission_required = ("book.view_book", "book.add_book")
    template_name = "books/book_list.html"
    model = Book

Checking permission in templates:
Using perms:

{% if perms.blog.view_post %}
  {# Your content here #}
{% endif %}

Middleware

Django Documentation: Middleware

Custom Middleware

# my_app/custom_middlware.py

import time

class CustomMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start_time = time.time()
        response = self.get_response(request)
        end_time = time.time()

        time_taken = end_time - start_time
        response['Time-Taken'] = str(time_taken)

        return response

Adding the custom middleware to our Django project:

MIDDLEWARE = [
    # ...
    'my_app.middleware.custom_middleware.CustomMiddleware',
    # ...
]

Middleware ordering
While processing request object middlware works from top to bottom and while processing response object middleware works from bottom to top.
Django Documentation: Middleware ordering

Form and Form Validation

Form

Django Documentation: Creating forms from models

ModelForm

from django.forms import ModelForm
from myapp.models import Article

class ArticleForm(ModelForm):
     class Meta:
         model = Article
         fields = '__all__'

The view:

#my_app/views.py

from .forms import ArticleForm

def article_create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save()
            return redirect('article-detail', article.id))

    else:
        form = ArticleForm()

    return render(request,
            'listings/article_create.html',
            {'form': form})

Selecting the fields to use

  • Set the fields attribute to the special value '__all__' to indicate that all fields in the model should be used.
from django.forms import ModelForm


class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = "__all__"
  • Set the exclude attribute of the ModelForm’s inner Meta class to a list of fields to be excluded from the form.
class PartialAuthorForm(ModelForm):
    class Meta:
        model = Article
        exclude = ["headline"]

Form template

<form action="" method="POST">
    {% csrf_token %}
    {{ form }}
    <input type="submit" name="save" value="Save">
    <input type="submit" name="preview" value="Preview">
</form>

Custom form field validators

# my_app/validators.py
from django.core.exceptions import ValidationError


def validate_proton_mail(value):
    """Raise a ValidationError if the value doesn't contains @proton.me.
    """
    if "@proton.me" in value:
        return value
    else:
        raise ValidationError("This field accepts mail id of Proton Mail only")

Adding validate_hello in our form:

# my_app/forms.py

from django import forms

from .models import MyModel
from .validators import validate_proton_mail

class MyModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['example_mail'].validators.append(validate_proton_mail)

    class Meta:
        model = MyModel
        fields = '__all__'

clean()

Performing validation on more than one field at a time.

# my_app/forms.py
from django import forms

from .models import MyModel


class MyForm(forms.ModelForm):

    class Meta:
        model = MyModel
        fields = '__all__'

    def clean(self):
        cleaned_data = super().clean()
        slug = cleaned_data.get('slug', '')
        title = cleaned_data.get('title', '')

        # slug and title should be same example
        if slug != title.lower():
            msg = "slug and title should be same"
            raise forms.ValidationError(msg)
        return cleaned_data

clean_field_name()

Performing validation on a specific field.

from django import forms

from .models import Product
from .validators import validate_amazing


class ProductForm(forms.ModelForm):

    class Meta:
        model = Product
        fields = '__all__'


    def clean_quantity(self):
        quantity = self.cleaned_data['quantity']
        if quantity > 100:
            msg = 'Quantity should be less than 100'
            raise forms.ValidationError(msg)
        return quantity

Template

Django Documentation: The Django template language

Template inheritance and inclusion

  • Inheritance
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="style.css">
    <title>{% block title %}My amazing site{% endblock %}</title>
</head>

<body>
    <div id="sidebar">
        {% block sidebar %}
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/blog/">Blog</a></li>
        </ul>
        {% endblock %}
    </div>

    <div id="content">
        {% block content %}{% endblock %}
    </div>
</body>
</html>
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}My amazing blog{% endblock %}

{% block content %}
{% for entry in blog_entries %}
    <h2>{{ entry.title }}</h2>
    <p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
  • Inclusion
{% include 'header.html' %}

Common template tags

  • static
{% load static %}
{% static 'css/main.css' %}
  • url passing positional arguments
{% url 'some-url-name' v1 v2 %}

Django Documentation: url

  • block (Defines a block that can be overridden by child templates)
<div id="content">
        {% block content %}{% endblock %}
    </div>

A child template might look like this:

{% extends "base.html" %}

{% block title %}My amazing blog{% endblock %}

{% block content %}
{% for entry in blog_entries %}
    <h2>{{ entry.title }}</h2>
    <p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
  • for
<ul>
{% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
{% endfor %}
</ul>
  • if, elif, else
{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}
  • now (Outputs the current date and/or time.)
{% now "SHORT_DATETIME_FORMAT" %}
  • Current path
{{ request.path }}
  • Dates and Times
<p>Copyright 2005-{% now "Y" %}</p>
  • Comments
{% comment "Optional note" %}
    <p>Commented out text with {{ create_date|date:"c" }}</p>
{% endcomment %}

Note that single lines of text can be commented out using {# and #}:

{# This is a comment. #}
  • Special Characters
{% autoescape off %}
    {{ content }}
{% endautoescape %}

Sending Email

Django Documentation: Sending email

Quick example:

from django.core.mail import send_mail

send_mail(
    "Subject here",
    "Here is the message.",
    "from@example.com",
    ["to@example.com"],
    fail_silently=False,
)

Email backend

Development:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Production:

config/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.yourserver.com'
EMAIL_USE_TLS = False
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = 'your@djangoapp.com'
EMAIL_HOST_PASSWORD = 'your password'

Performance

django-debug-toolbar

Django Debug Toolbar Documentation

Install:
python -m pip install django-debug-toolbar

settings.py

INSTALLED_APPS = [
    # ...
    "debug_toolbar",
    # ...
]

urls.py

from django.urls import include, path

urlpatterns = [
    # ...
    path("__debug__/", include("debug_toolbar.urls")),
]

Django provides two QuerySet methods that can turn the N queries back into one query, solving the performance issue.

These two methods are:

select_related
Django Documentation: select_related

select_related returns a QuerySet that will “follow” foreign-key relationships (either One-to-One or One-to-Many), selecting additional related-object data when it executes its query.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=50)

class Book(models.Model):
    title = models.CharField(max_length=50)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# With select_related
authors = Author.objects.all().select_related('book')
for author in authors:
    books = author.book_set.all()

prefetch_related
Django Documentation: prefetch_related

prefetch_related performs a separate lookup for each relationship and “joins” them together with Python, not SQL.
This allows it to prefetch many-to-many and many-to-one objects, which cannot be done using select_related

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=50)

class Book(models.Model):
    title = models.CharField(max_length=50)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# With prefetch_related
authors = Author.objects.all().prefetch_related('book')
for author in authors:
    books = author.book_set.all()

Indexes

Django Documentation: Model index reference

If a particular field is consistently utilized, accounting for around 10-25% of all queries, it is a prime candidate to be indexed.
The downside is that indexes require additional space on a disk so they must be used with care.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=50)
    
    class Meta:
    		indexes = [models.Index(fields=["name"])]

Caching

Redis

Django Documentation: Redis

  1. Setting up a Redis server locally or on a remote machine.
  2. Installing redis-py. Installing hiredis-py is also recommended.
  3. Set BACKEND to django.core.cache.backends.redis.RedisCache.
  4. Set LOCATION to the URL pointing to your Redis instance, using the appropriate scheme. See the redis-py docs for details on the available schemes. For example, if Redis is running on localhost (127.0.0.1) port 6379:
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    }
}

In order to supply a username and password, add them in the LOCATION along with the URL:

CACHES = {
   "default": {
       "BACKEND": "django.core.cache.backends.redis.RedisCache",
       "LOCATION": [
           "redis://127.0.0.1:6379",  # leader
           "redis://127.0.0.1:6378",  # read-replica 1
           "redis://127.0.0.1:6377",  # read-replica 2
       ],
   }
}

Database caching

Django Documentation: Database caching

Django can store its cached data in your database. This works best if you’ve got a fast, well-indexed database server. In this example, the cache table’s name is my_cache_table:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
        "LOCATION": "my_cache_table",
    }
}

Creating the cache table:

python manage.py createcachetable

per-view cache

Django Documentation: The per-view cache

In Django, the cache_page decorator is used to cache the output of a view function. It takes a single argument, timeout, which specifies the duration in seconds for which the output should be cached.

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Cache the page for 15 minutes
def my_view(request):
    # View logic ...

Specifying per-view cache in the URLconf:
You can do so by wrapping the view function with cache_page when you refer to it in the URLconf.

from django.views.decorators.cache import cache_page

urlpatterns = [
    path("foo/<int:code>/", cache_page(60 * 15)(my_view)),
]

per-site cache

Django Documentation: per-site cache

# config/settings.py

MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
]

The order of the middleware is important
UpdateCacheMiddleware must come before FetchFromCacheMiddleware.

template fragment caching

Django Documentation: Template fragment caching

{% load cache %}

{% cache 500 book_list %}
  <ul>
    {% for book in books %}
      <li>{{ book.title }}</li>
    {% endfor %}
  </ul>
{% endcache %}

The cache template tag expects a cache timeout in second with the name of the cache fragment book_list

Security

Django Documentation: Deployment checklist

Admin Hardening

Changing the URL path

# config/urls.py

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path("another_admin_path/", admin.site.urls),
]

Cross site request forgery (CSRF) protection

Django's CSRF protection is turned on by default. You should always use the {% csrf_token %} template tag in your forms and use POST for requests that might change or add data to the database.

Enforcing SSL HTTPS

ALLOWED_HOSTS

Django Documentation: ALLOWED_HOSTS Use ALLOWED_HOSTS to only accept requests from trusted hosts.

Further Reading