Skip to content

Web Development with Django

Complete guide to building web applications with Django, covering views, templates, URL routing, middleware, and modern web development patterns.

Django Request-Response Cycle

Understanding how Django processes web requests is fundamental to building applications.

The Flow

Browser Request
URL Resolver (urls.py)
Middleware (request processing)
View (views.py)
Model (if needed)
Template (rendering)
Middleware (response processing)
HTTP Response to Browser

URL Configuration

Basic URL Patterns

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    # Simple path
    path('', views.home, name='home'),
    path('about/', views.about, name='about'),

    # Path with parameters
    path('posts/<int:post_id>/', views.post_detail, name='post_detail'),
    path('posts/<slug:slug>/', views.post_by_slug, name='post_by_slug'),

    # Multiple parameters
    path('blog/<int:year>/<int:month>/', views.monthly_archive, name='monthly_archive'),
]

Path Converters

from django.urls import path

urlpatterns = [
    # str - matches any non-empty string (default)
    path('articles/<str:section>/', views.article_section),

    # int - matches zero or any positive integer
    path('articles/<int:id>/', views.article_detail),

    # slug - matches any slug string (letters, numbers, hyphens, underscores)
    path('articles/<slug:slug>/', views.article_by_slug),

    # uuid - matches a formatted UUID
    path('users/<uuid:user_id>/', views.user_profile),

    # path - matches any non-empty string, including the path separator /
    path('pages/<path:page_path>/', views.page_view),
]

Custom Path Converters

# converters.py
class FourDigitYearConverter:
    regex = '[0-9]{4}'

    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return '%04d' % value

# urls.py
from django.urls import path, register_converter
from . import views, converters

register_converter(converters.FourDigitYearConverter, 'yyyy')

urlpatterns = [
    path('articles/<yyyy:year>/', views.year_archive),
]

Including Other URLconfs

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('api/', include('api.urls')),
    path('', include('pages.urls')),
]

# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'  # Namespace

urlpatterns = [
    path('', views.post_list, name='list'),
    path('<int:pk>/', views.post_detail, name='detail'),
    path('create/', views.post_create, name='create'),
]

# Usage in templates: {% url 'blog:detail' pk=post.pk %}

Views

Function-Based Views

# views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse
from .models import Post

def post_list(request):
    posts = Post.objects.all().order_by('-created_at')
    context = {'posts': posts}
    return render(request, 'blog/post_list.html', context)

def post_detail(request, post_id):
    post = get_object_or_404(Post, pk=post_id)
    return render(request, 'blog/post_detail.html', {'post': post})

def post_create(request):
    if request.method == 'POST':
        # Process form
        title = request.POST.get('title')
        content = request.POST.get('content')
        post = Post.objects.create(title=title, content=content)
        return redirect('post_detail', post_id=post.id)
    return render(request, 'blog/post_form.html')

def api_posts(request):
    posts = Post.objects.all().values('id', 'title', 'created_at')
    return JsonResponse(list(posts), safe=False)

Class-Based Views

from django.views.generic import (
    ListView, DetailView, CreateView,
    UpdateView, DeleteView, TemplateView
)
from django.urls import reverse_lazy
from .models import Post

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.filter(published=True).order_by('-created_at')

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

class PostCreateView(CreateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content', 'author']
    success_url = reverse_lazy('post_list')

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(UpdateView):
    model = Post
    template_name = 'blog/post_form.html'
    fields = ['title', 'content']

    def get_success_url(self):
        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})

class PostDeleteView(DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('post_list')

Generic Views URL Configuration

# urls.py
from django.urls import path
from .views import (
    PostListView, PostDetailView, PostCreateView,
    PostUpdateView, PostDeleteView
)

urlpatterns = [
    path('', PostListView.as_view(), name='post_list'),
    path('<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    path('create/', PostCreateView.as_view(), name='post_create'),
    path('<int:pk>/edit/', PostUpdateView.as_view(), name='post_update'),
    path('<int:pk>/delete/', PostDeleteView.as_view(), name='post_delete'),
]

Templates

Template Basics

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Site{% endblock %}</title>
    {% load static %}
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <nav>
        <a href="{% url 'home' %}">Home</a>
        <a href="{% url 'blog:list' %}">Blog</a>
        {% if user.is_authenticated %}
            <a href="{% url 'profile' %}">Profile</a>
            <a href="{% url 'logout' %}">Logout</a>
        {% else %}
            <a href="{% url 'login' %}">Login</a>
        {% endif %}
    </nav>

    <main>
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}

        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; 2024 My Site</p>
    </footer>

    {% block extra_js %}{% endblock %}
</body>
</html>

Template Inheritance

<!-- templates/blog/post_list.html -->
{% extends 'base.html' %}
{% load static %}

{% block title %}Blog Posts - {{ block.super }}{% endblock %}

{% block content %}
<h1>Blog Posts</h1>

{% if posts %}
    <div class="posts">
        {% for post in posts %}
            <article class="post">
                <h2><a href="{% url 'blog:detail' pk=post.pk %}">{{ post.title }}</a></h2>
                <p class="meta">
                    By {{ post.author.username }} on {{ post.created_at|date:"F d, Y" }}
                </p>
                <p>{{ post.content|truncatewords:30 }}</p>
            </article>
        {% endfor %}
    </div>

    <!-- Pagination -->
    {% if is_paginated %}
        <div class="pagination">
            {% if page_obj.has_previous %}
                <a href="?page=1">First</a>
                <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
            {% endif %}

            <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>

            {% if page_obj.has_next %}
                <a href="?page={{ page_obj.next_page_number }}">Next</a>
                <a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
            {% endif %}
        </div>
    {% endif %}
{% else %}
    <p>No posts available.</p>
{% endif %}
{% endblock %}

Template Filters and Tags

<!-- Built-in filters -->
{{ post.title|upper }}
{{ post.content|truncatewords:50 }}
{{ post.created_at|date:"Y-m-d H:i" }}
{{ post.price|floatformat:2 }}
{{ post.description|linebreaks }}
{{ post.html_content|safe }}
{{ post.title|default:"No title" }}

<!-- Custom filter -->
{% load blog_extras %}
{{ post.content|markdown }}

<!-- Template tags -->
{% for post in posts %}
    {{ forloop.counter }}. {{ post.title }}
    {% if forloop.first %}First!{% endif %}
    {% if forloop.last %}Last!{% endif %}
{% endfor %}

{% with total=posts.count %}
    <p>Total posts: {{ total }}</p>
{% endwith %}

{% include 'partials/post_card.html' with post=post %}

Custom Template Tags and Filters

# blog/templatetags/blog_extras.py
from django import template
import markdown as md

register = template.Library()

@register.filter
def markdown(value):
    return md.markdown(value, extensions=['extra'])

@register.simple_tag
def multiply(a, b):
    return a * b

@register.inclusion_tag('blog/latest_posts.html')
def show_latest_posts(count=5):
    posts = Post.objects.order_by('-created_at')[:count]
    return {'posts': posts}
<!-- Usage -->
{% load blog_extras %}

{{ post.content|markdown }}
{% multiply 5 10 %}
{% show_latest_posts 3 %}

Static Files

Configuration

# settings.py
import os

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

# For production
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

Directory Structure

myproject/
├── static/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── images/
│       └── logo.png
└── myapp/
    └── static/
        └── myapp/
            ├── css/
            │   └── app.css
            └── js/
                └── app.js

Using Static Files

{% load static %}

<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">

<!-- JavaScript -->
<script src="{% static 'js/main.js' %}"></script>

<!-- Images -->
<img src="{% static 'images/logo.png' %}" alt="Logo">

<!-- App-specific static files -->
<link rel="stylesheet" href="{% static 'myapp/css/app.css' %}">

Collecting Static Files

# Collect all static files to STATIC_ROOT
python manage.py collectstatic

# Clear existing files before collecting
python manage.py collectstatic --clear --noinput

Media Files

Configuration

# settings.py
import os

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

URL Configuration

# urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... your url patterns
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

File Uploads

# models.py
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    bio = models.TextField()

class Document(models.Model):
    title = models.CharField(max_length=200)
    file = models.FileField(upload_to='documents/%Y/%m/%d/')
    uploaded_at = models.DateTimeField(auto_now_add=True)
<!-- Template with file upload -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Upload</button>
</form>

<!-- Display uploaded file -->
{% if profile.avatar %}
    <img src="{{ profile.avatar.url }}" alt="Avatar">
{% endif %}

Middleware

Built-in Middleware

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Custom Middleware

# middleware.py
import time
import logging

logger = logging.getLogger(__name__)

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

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

        response = self.get_response(request)

        duration = time.time() - start_time
        logger.info(f'{request.method} {request.path} took {duration:.2f}s')

        return response

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

    def __call__(self, request):
        response = self.get_response(request)
        response['X-Custom-Header'] = 'MyValue'
        return response
# settings.py
MIDDLEWARE = [
    # ... other middleware
    'myapp.middleware.RequestTimingMiddleware',
    'myapp.middleware.CustomHeaderMiddleware',
]

Context Processors

Built-in Context Processors

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Custom Context Processor

# context_processors.py
from .models import Category

def site_info(request):
    return {
        'site_name': 'My Django Site',
        'categories': Category.objects.all(),
    }
# settings.py
TEMPLATES = [
    {
        'OPTIONS': {
            'context_processors': [
                # ... built-in processors
                'myapp.context_processors.site_info',
            ],
        },
    },
]
<!-- Now available in all templates -->
<h1>{{ site_name }}</h1>
<ul>
    {% for category in categories %}
        <li>{{ category.name }}</li>
    {% endfor %}
</ul>

Sessions

Session Configuration

# settings.py

# Database-backed sessions (default)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'

# Cache-backed sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'

# File-based sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.file'

# Session settings
SESSION_COOKIE_AGE = 1209600  # 2 weeks in seconds
SESSION_COOKIE_SECURE = True  # HTTPS only
SESSION_COOKIE_HTTPONLY = True
SESSION_SAVE_EVERY_REQUEST = False

Using Sessions

# views.py
def add_to_cart(request, product_id):
    cart = request.session.get('cart', {})
    cart[product_id] = cart.get(product_id, 0) + 1
    request.session['cart'] = cart
    request.session.modified = True
    return redirect('cart')

def view_cart(request):
    cart = request.session.get('cart', {})
    return render(request, 'cart.html', {'cart': cart})

def clear_cart(request):
    if 'cart' in request.session:
        del request.session['cart']
    return redirect('home')

Messages Framework

# views.py
from django.contrib import messages
from django.shortcuts import redirect

def create_post(request):
    if request.method == 'POST':
        # Process form
        messages.success(request, 'Post created successfully!')
        return redirect('post_list')
    return render(request, 'post_form.html')

def delete_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.delete()
    messages.info(request, f'Post "{post.title}" deleted.')
    return redirect('post_list')

def error_view(request):
    messages.error(request, 'Something went wrong!')
    messages.warning(request, 'Please check your input.')
    return render(request, 'form.html')
<!-- Display messages -->
{% if messages %}
    {% for message in messages %}
        <div class="alert alert-{{ message.tags }}">
            {{ message }}
        </div>
    {% endfor %}
{% endif %}

Pagination

# views.py
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def post_list(request):
    posts = Post.objects.all().order_by('-created_at')
    paginator = Paginator(posts, 10)  # 10 posts per page

    page = request.GET.get('page')
    try:
        posts_page = paginator.page(page)
    except PageNotAnInteger:
        posts_page = paginator.page(1)
    except EmptyPage:
        posts_page = paginator.page(paginator.num_pages)

    return render(request, 'post_list.html', {'posts': posts_page})
<!-- Pagination template -->
<div class="pagination">
    <span class="step-links">
        {% if posts.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ posts.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ posts.number }} of {{ posts.paginator.num_pages }}
        </span>

        {% if posts.has_next %}
            <a href="?page={{ posts.next_page_number }}">next</a>
            <a href="?page={{ posts.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
</div>

Signals

# signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

@receiver(pre_delete, sender=Post)
def delete_post_files(sender, instance, **kwargs):
    # Delete associated files before deleting post
    if instance.image:
        instance.image.delete(save=False)
# apps.py
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'blog'

    def ready(self):
        import blog.signals

Additional Resources