Skip to content

Django

Django is a high-level Python web framework that follows the “batteries included” philosophy, providing everything you need to build robust web applications out of the box. This article covers the core concepts you need to go from installation through models, views, templates, ORM, forms, middleware, and authentication.

Framework Fundamentals

A web framework typically handles three responsibilities: the socket layer (HTTP server), URL routing, and template rendering. Django covers all three with its own implementations, building on the WSGI standard (wsgiref):

  • Incoming requests are automatically parsed from HTTP format into a convenient Python dictionary.
  • Outgoing responses are re-serialised into valid HTTP format before being sent.

Django splits application code across well-defined files:

  • urls.py — URL-to-view mapping (the routing layer)
  • views.py — business logic (the view layer)
  • models.py — data models and ORM
  • templates/ — HTML template files

Python’s Three Major Web Frameworks

Django   — "aircraft carrier": large, full-featured, includes everything
Flask    — "cavalry scout": small, minimal core, relies on third-party extensions
Tornado  — async, non-blocking, supports high concurrency; can even serve game servers

Installation

pip install django==1.11.22
django-admin startproject mysite   # Create a project
cd mysite
python manage.py runserver         # Start the dev server
python manage.py startapp app01    # Create an application

Notes:

  • The computer name must not contain Chinese characters.
  • Each PyCharm window should contain only one project.
  • Python 3.4–3.6 is recommended for Django 1.x (3.7+ requires Django 1.17+).

Project Structure

mysite/
  mysite/
    settings.py   — configuration file
    urls.py        — root URL routing
    wsgi.py        — WSGI entry point
  manage.py        — management command entry point
  db.sqlite3       — built-in SQLite database
  app01/
    admin.py       — Django admin registration
    apps.py        — app configuration
    migrations/    — database migration history
    models.py      — ORM model classes
    tests.py       — test file
    views.py       — view functions

Every created application must be registered in INSTALLED_APPS before Django recognises it:

INSTALLED_APPS = [
    ...
    'app01.apps.App01Config',  # full form
    # 'app01',                 # short form
]

The Three Essential Shortcuts

from django.shortcuts import HttpResponse, render, redirect

# Return a plain string
return HttpResponse('Hello')

# Render an HTML template
return render(request, 'login.html', {'username': 'alice'})
# Pass all local variables at once
return render(request, 'login.html', locals())

# Redirect
return redirect('/home/')
return redirect('https://example.com/')

To disable automatic trailing-slash appending: add APPEND_SLASH = False in settings.py.

Static Files

# settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

Reference static files dynamically in templates:

{% load static %}
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/bootstrap.min.js' %}"></script>

To submit POST forms, comment out the CSRF middleware during early development:

# MIDDLEWARE = [
#     ...
#     # 'django.middleware.csrf.CsrfViewMiddleware',
#     ...
# ]

The Request Object

request.method          # 'GET' or 'POST' (always uppercase)
request.POST            # POST data (excludes files)
request.POST.get('key')          # Get one value
request.POST.getlist('key')      # Get a list
request.GET             # Query string data
request.FILES           # Uploaded files
request.body            # Raw binary request body
request.path            # URL path without query string
request.get_full_path() # Full URL including query string

Connecting to MySQL

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django_base',
        'USER': 'root',
        'PASSWORD': 'your_password',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'CHARSET': 'UTF8',
    }
}

Django defaults to mysqldb; switch to pymysql for better compatibility:

# In the project's __init__.py
import pymysql
pymysql.install_as_MySQLdb()

URL Routing

from django.conf.urls import url
from app01 import views

urlpatterns = [
    url(r'^index/$', views.index),
    # Unnamed group — passes captured value as positional arg
    url(r'^article/(\d+)/$', views.article),
    # Named group — passes as keyword arg
    url(r'^article/(?P<article_id>\d+)/$', views.article),
]

Reverse URL resolution:

# Give a route an alias
url(r'^func/', views.func, name='my_func')

# Back end
from django.shortcuts import reverse
reverse('my_func')          # /func/
reverse('my_func', args=(1,))  # with positional arg

# Front end (template)
# <a href="{% url 'my_func' %}">link</a>

URL distribution (include):

# Root urls.py
url(r'^app01/', include('app01.urls')),
url(r'^app02/', include('app02.urls')),

The View Layer

Function-Based Views (FBV)

from django.http import HttpResponse

def index(request):
    return HttpResponse('index')

Class-Based Views (CBV)

from django.views import View

class MyLogin(View):
    def get(self, request):
        return render(request, 'login.html')

    def post(self, request):
        return HttpResponse('POST OK')

CBV route registration:

url(r'^login/', views.MyLogin.as_view())

Django’s dispatch() method uses getattr(self, request.method.lower()) to route to the correct handler automatically.

JsonResponse

from django.http import JsonResponse

def ab_json(request):
    data = {'username': 'alice', 'age': 18}
    return JsonResponse(data, json_dumps_params={'ensure_ascii': False})

# For non-dict types, add safe=False
# return JsonResponse([1, 2, 3], safe=False)

The Template Layer

Template syntax uses {{ }} for variables and {% %} for logic:

{{ variable }}
{{ dict.key }}
{{ list.0 }}

{% for item in list %}
    <p>{{ item }}</p>
{% endfor %}

{% if condition %}
    <p>Yes</p>
{% elif other %}
    <p>Maybe</p>
{% else %}
    <p>No</p>
{% endif %}

Common filters:

{{ s|length }}
{{ b|default:'nothing' }}
{{ current_time|date:'Y-m-d H:i:s' }}
{{ info|truncatechars:9 }}
{{ msg|cut:' ' }}
{{ l|join:'$' }}
{{ html_string|safe }}

Template inheritance:

{# base.html #}
{% block css %}{% endblock %}
{% block content %}{% endblock %}
{% block js %}{% endblock %}

{# child.html #}
{% extends 'base.html' %}
{% block content %}
    <p>Child content here</p>
{% endblock %}

Template inclusion:

{% include 'sidebar.html' %}

ORM

Django ORM maps Python classes to database tables:

# models.py
from django.db import models

class Book(models.Model):
    title   = models.CharField(max_length=32)
    price   = models.DecimalField(max_digits=8, decimal_places=2)
    publish = models.ForeignKey(to='Publish')          # many-to-one
    authors = models.ManyToManyField(to='Author')      # many-to-many

class Publish(models.Model):
    name = models.CharField(max_length=32)
    addr = models.CharField(max_length=32)

class Author(models.Model):
    name          = models.CharField(max_length=32)
    age           = models.IntegerField()
    author_detail = models.OneToOneField(to='AuthorDetail')

class AuthorDetail(models.Model):
    phone = models.BigIntegerField()
    addr  = models.CharField(max_length=32)

Apply migrations:

python manage.py makemigrations   # Record changes
python manage.py migrate          # Apply to database

Essential 13 ORM Methods

# Query
Model.objects.all()
Model.objects.filter(age=18)
Model.objects.get(pk=1)          # Raises exception if not found
Model.objects.first()
Model.objects.last()
Model.objects.values('name', 'age')       # Returns list of dicts
Model.objects.values_list('name', 'age')  # Returns list of tuples
Model.objects.distinct()
Model.objects.order_by('age')    # ascending
Model.objects.order_by('-age')   # descending
Model.objects.reverse()          # only works on ordered querysets
Model.objects.count()
Model.objects.exclude(name='bob')
Model.objects.filter(pk=10).exists()

Create, Update, Delete

# Create
obj = Model.objects.create(name='alice', age=18)

user_obj = Model(name='alice', age=18)
user_obj.save()

# Update
Model.objects.filter(pk=1).update(age=20)  # batch update

obj = Model.objects.get(pk=1)
obj.age = 20
obj.save()                                  # full object update

# Delete
Model.objects.filter(pk=1).delete()

Double-Underscore Lookups

filter(age__gt=18)        # age > 18
filter(age__lt=18)        # age < 18
filter(age__gte=18)       # age >= 18
filter(age__lte=18)       # age <= 18
filter(age__in=[18, 20])  # age IN (18, 20)
filter(age__range=[18, 30])  # 18 <= age <= 30
filter(name__contains='a')   # LIKE '%a%'
filter(name__icontains='a')  # case-insensitive LIKE
filter(name__startswith='j')
filter(name__endswith='n')
filter(register_time__year='2020')
filter(register_time__month='1')

Cross-Table Queries

# Forward (FK field is on this model)
book_obj.publish          # one-to-one / FK
book_obj.authors.all()    # many-to-many

# Reverse (FK field is on the other model)
publish_obj.book_set.all()

# Double-underscore cross-table query
Book.objects.filter(pk=1).values('title', 'publish__name')
Publish.objects.filter(book__id=1).values('name', 'book__title')

Aggregate and Group-By Queries

from django.db.models import Max, Min, Sum, Count, Avg

# Aggregate across all records
Book.objects.aggregate(Avg('price'), Max('price'))

# Group by (annotate)
Book.objects.annotate(author_count=Count('authors')).values('title', 'author_count')
Publish.objects.annotate(min_price=Min('book__price')).values('name', 'min_price')

F and Q Queries

from django.db.models import F, Q

# F: reference another field's value
Book.objects.filter(sold__gt=F('stock'))
Book.objects.update(price=F('price') + 50)

# Q: OR / NOT logic
Book.objects.filter(Q(sold__gt=100) | Q(price__lt=600))
Book.objects.filter(~Q(sold__gt=100))

Transactions

from django.db import transaction

try:
    with transaction.atomic():
        # All ORM operations inside this block share one transaction
        pass
except Exception as e:
    print(e)

Query Optimisation

# only(): fetch specified fields; accessing other fields triggers an extra query
Book.objects.only('title')

# defer(): exclude specified fields; accessing them triggers an extra query
Book.objects.defer('title')

# select_related(): INNER JOIN to fetch related objects in one query (FK / O2O)
Book.objects.select_related('publish')

# prefetch_related(): sub-query to fetch related objects (any relation type)
Book.objects.prefetch_related('publish')

choices Parameter

class User(models.Model):
    gender_choices = ((1, 'Male'), (2, 'Female'), (3, 'Other'))
    gender = models.IntegerField(choices=gender_choices)

# Retrieve the display value
user_obj.get_gender_display()

Many-to-Many Creation Strategies

# Full-auto: ORM creates the third table automatically (limited extensibility)
authors = models.ManyToManyField(to='Author')

# Semi-auto: you control the third table but still get ORM query support
class Book2Author(models.Model):
    book   = models.ForeignKey(to='Book')
    author = models.ForeignKey(to='Author')

class Book(models.Model):
    authors = models.ManyToManyField(
        to='Author',
        through='Book2Author',
        through_fields=('book', 'author'),
    )

Bulk Insert

book_list = [Book(title=f'Book {i}') for i in range(100000)]
Book.objects.bulk_create(book_list)

Forms Component

from django import forms

class MyForm(forms.Form):
    username = forms.CharField(min_length=3, max_length=8, label='Username',
                               error_messages={
                                   'min_length': 'Username must be at least 3 characters',
                                   'max_length': 'Username cannot exceed 8 characters',
                                   'required':   'Username is required',
                               })
    password = forms.CharField(min_length=3, max_length=8, label='Password')
    email    = forms.EmailField(label='Email')

Validation and rendering in a view:

def index(request):
    form_obj = MyForm()
    if request.method == 'POST':
        form_obj = MyForm(request.POST)
        if form_obj.is_valid():
            return HttpResponse('OK')
    return render(request, 'index.html', locals())

Template rendering (recommended — third approach):

{% for form in form_obj %}
    <p>{{ form.label }}: {{ form }}
        <span style="color:red">{{ form.errors.0 }}</span>
    </p>
{% endfor %}

Hook functions:

# Local hook — for a single field
def clean_username(self):
    username = self.cleaned_data.get('username')
    if '666' in username:
        self.add_error('username', 'Invalid username')
    return username

# Global hook — for cross-field validation
def clean(self):
    password         = self.cleaned_data.get('password')
    confirm_password = self.cleaned_data.get('confirm_password')
    if password != confirm_password:
        self.add_error('confirm_password', 'Passwords do not match')
    return self.cleaned_data

Middleware

Middleware is a hook system that sits between the request and response lifecycle. Django executes process_request methods from top to bottom, and process_response methods from bottom to top.

from django.utils.deprecation import MiddlewareMixin

class MyMiddleware(MiddlewareMixin):
    def process_request(self, request):
        # Return HttpResponse to short-circuit further processing
        pass

    def process_response(self, request, response):
        # Must return response
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        pass  # Runs after routing, before the view function

    def process_exception(self, request, exception):
        pass  # Runs when a view raises an exception

Register in settings.py:

MIDDLEWARE = [
    ...
    'myapp.middleware.my_middleware.MyMiddleware',
]

CSRF Protection

Django protects against Cross-Site Request Forgery by embedding a unique token in every form:

<form action="" method="post">
    {% csrf_token %}
    ...
</form>

For AJAX requests:

// Option 1: read from the DOM
data: { csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val() }

// Option 2: use the template tag
data: { csrfmiddlewaretoken: '{{ csrf_token }}' }

CSRF decorators:

from django.views.decorators.csrf import csrf_protect, csrf_exempt
from django.utils.decorators import method_decorator

@csrf_exempt   # Disable CSRF check for this view
def my_view(request): ...

# For CBV, apply to dispatch
@method_decorator(csrf_exempt, name='dispatch')
class MyView(View): ...

Cookie and Session

# Set a cookie
response = HttpResponse('OK')
response.set_cookie('username', 'alice', max_age=3600)
return response

# Read a cookie
username = request.COOKIES.get('username')

# Delete a cookie
response.delete_cookie('username')
# Session
request.session['key'] = 'value'       # Set
value = request.session.get('key')     # Get
request.session.set_expiry(3600)       # Set expiry (seconds)
request.session.delete()               # Delete server-side session only
request.session.flush()                # Delete both cookie and server-side session

Auth Module

Django’s built-in auth module manages user authentication against the auth_user table:

from django.contrib import auth
from django.contrib.auth.models import User

# Authenticate credentials
user_obj = auth.authenticate(request, username=username, password=password)

# Log in (saves session)
auth.login(request, user_obj)

# Check login status
request.user.is_authenticated()

# Require login decorator
from django.contrib.auth.decorators import login_required

@login_required(login_url='/login/')
def home(request): ...

# Global redirect target
# LOGIN_URL = '/login/'

# Verify current password
request.user.check_password(old_password)

# Change password
request.user.set_password(new_password)
request.user.save()

# Log out
auth.logout(request)

# Create users
User.objects.create_user(username=username, password=password)
User.objects.create_superuser(username=username, email='x@x.com', password=password)

Extending the User Table

from django.contrib.auth.models import AbstractUser

class UserInfo(AbstractUser):
    """
    Extend auth_user with additional fields.
    Prerequisites:
      1. Run this before the first migration (auth_user must not yet exist).
      2. Do not override existing AbstractUser fields.
      3. Declare in settings: AUTH_USER_MODEL = 'app01.UserInfo'
    """
    phone = models.BigIntegerField()
# settings.py
AUTH_USER_MODEL = 'app01.UserInfo'

Project: Book Management System

A minimal book-management CRUD example that ties together the ORM and view concepts covered above — list, add, edit, and delete books, each linked to a publisher (ForeignKey) and one or more authors (ManyToManyField):

from django.shortcuts import render, redirect, HttpResponse
from app01 import models


def home(request):
    return render(request, 'home.html')


def book_list(request):
    # Query all books and pass them to the template
    book_queryset = models.Book.objects.all()
    return render(request, 'book_list.html', locals())


def book_add(request):
    if request.method == 'POST':
        # Read all submitted form data
        title = request.POST.get('title')
        price = request.POST.get('price')
        publish_date = request.POST.get('publish_date')
        publish_id = request.POST.get('publish')
        authors_list = request.POST.getlist('authors')  # [1, 2, 3, 4]
        # Create the book record
        book_obj = models.Book.objects.create(
            title=title, price=price, publish_date=publish_date, publish_id=publish_id
        )
        # Populate the many-to-many author relationship
        book_obj.authors.add(*authors_list)
        # redirect() accepts a URL or a named-route alias directly.
        # If the alias needs extra arguments, use reverse() instead.
        return redirect('book_list')

    # GET: supply the publisher and author choices for the add form
    publish_queryset = models.Publish.objects.all()
    author_queryset = models.Author.objects.all()
    return render(request, 'book_add.html', locals())


def book_edit(request, edit_id):
    # Look up the book being edited so the form can be pre-filled
    edit_obj = models.Book.objects.filter(pk=edit_id).first()
    if request.method == 'POST':
        title = request.POST.get('title')
        price = request.POST.get('price')
        publish_date = request.POST.get('publish_date')
        publish_id = request.POST.get('publish')
        authors_list = request.POST.getlist('authors')  # [1, 2, 3, 4]
        models.Book.objects.filter(pk=edit_id).update(
            title=title, price=price, publish_date=publish_date, publish_id=publish_id
        )
        # Replace the many-to-many relationship with the new selection
        edit_obj.authors.set(authors_list)
        return redirect('book_list')

    publish_queryset = models.Publish.objects.all()
    author_queryset = models.Author.objects.all()
    return render(request, 'book_edit.html', locals())


def book_delete(request, delete_id):
    # Straightforward hard delete
    models.Book.objects.filter(pk=delete_id).delete()
    return redirect('book_list')

MTV vs MVC

Django calls itself an MTV framework:

  • M — Models
  • T — Templates
  • V — Views

This corresponds to the traditional MVC pattern (Model, View, Controller), where the Django view plays the controller role.

Last updated on