Frameworks reference

Django Reference

Models, views, templates, the ORM, authentication and deployment, with the reasoning behind Django's conventions rather than only the syntax.

How a request becomes a page

Almost every Django question is easier once you can say where in the request cycle you are. Five steps, in order, every single time.

The request cycle

A URL arrives, Django matches it to a view, the view does the work, it asks a template to render, and a response goes back.

# 1. urls.py     which view handles this address
# 2. views.py    the work: read input, query, decide
# 3. models.py   the data, via the ORM
# 4. template    HTML with the values filled in
# 5. response    sent back to the browser
  • When something is wrong, work out which of the five steps it is in before changing anything.
  • A 404 is step 1. A 500 is usually step 2 or 3. A blank spot on the page is usually step 4.

MVT, and why the names differ

Django calls it Model, View, Template. What most frameworks call a controller, Django calls a view; what they call a view, Django calls a template.

  • Model: the data and the rules about it.
  • View: what happens when a URL is requested. This is the controller in other frameworks.
  • Template: the presentation. This is the view in other frameworks.
  • The mismatch causes real confusion when reading tutorials written for other stacks.

Project versus app

A project is the whole site and its settings. An app is one self-contained piece of functionality inside it.

django-admin startproject sankofa_journal .
python manage.py startapp posts

sankofa_journal/    # settings, root urls, wsgi
posts/              # models, views, urls, templates
manage.py
  • One project, many apps. An app should do one thing and be plausibly reusable.
  • The trailing dot on startproject avoids a redundant nested folder of the same name.
  • Every app must be added to INSTALLED_APPS or Django ignores it entirely, silently.

manage.py

The command you run for nearly everything during development.

python manage.py runserver
python manage.py startapp posts
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py shell
python manage.py test
python manage.py collectstatic
  • makemigrations writes the plan. migrate applies it. They are two steps on purpose.
  • shell gives you a Python prompt with Django loaded, which is the fastest way to try a query.

URLs

Routing

A list of patterns, matched top to bottom, each pointing at a view.

# project urls.py
from django.urls import path, include

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

# posts/urls.py
app_name = "posts"

urlpatterns = [
    path("", views.index, name="index"),
    path("post/<int:pk>/", views.detail, name="detail"),
    path("tag/<slug:slug>/", views.by_tag, name="by_tag"),
]
  • First match wins, so put specific patterns above general ones.
  • include() keeps each app's routes with the app rather than in one enormous file.
  • The converter matters: <int:pk> gives you an int, <slug:slug> and <str:name> give strings.

Named URLs

Give every route a name and refer to it by that name. Then the address can change without breaking anything.

# in a template
<a href="{% url 'posts:detail' post.pk %}">Read</a>

# in Python
from django.urls import reverse
return redirect(reverse("posts:detail", args=[post.pk]))

# or, if the model defines it
return redirect(post)
  • Hard-coded paths like /post/3/ break the day you rename a route. Named URLs do not.
  • The app_name prefix keeps names unique between apps: posts:detail, accounts:detail.
  • get_absolute_url on the model lets redirect(obj) and {{ obj.get_absolute_url }} just work.

Views

Function views

A function taking a request and returning a response. The clearest place to start.

from django.shortcuts import render, get_object_or_404

def index(request):
    posts = Post.objects.filter(published=True)
    return render(request, "posts/index.html", {"posts": posts})

def detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, "posts/detail.html", {"post": post})
  • The third argument to render is the context: the names the template can use.
  • get_object_or_404 raises a proper 404 instead of an exception page when nothing matches.
  • A view must return a response. Forgetting the return gives you 'did not return an HttpResponse'.

Class-based views

Prewritten views for the patterns that recur constantly: list, detail, create, update, delete.

from django.views.generic import ListView, DetailView

class PostList(ListView):
    model = Post
    paginate_by = 20
    # template: posts/post_list.html
    # context:  object_list, and post_list

class PostDetail(DetailView):
    model = Post
    # template: posts/post_detail.html
    # context:  object, and post
  • Far less code when your view is a standard shape, and awkward when it is not.
  • The template name and context name are derived by convention, which is why nothing seems to be declared.
  • Override get_queryset to change what is listed, get_context_data to add to the context.
  • Start with function views. Move to these when you have written the same view three times.

Handling POST

One view usually handles both showing a form and receiving it.

def create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            return redirect(post)
    else:
        form = PostForm()
    return render(request, "posts/form.html", {"form": form})
  • Redirect after a successful POST, always. Otherwise a refresh submits it again.
  • That pattern has a name, Post/Redirect/Get, and it is the fix for duplicate submissions.
  • On an invalid form, render again with the same form object so the errors and input survive.

Models

Defining a model

A Python class that becomes a database table. One class, one table; one attribute, one column.

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    body = models.TextField()
    published = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created"]

    def __str__(self):
        return self.title
  • Always write __str__. Without it the admin and the shell show Post object (1) for everything.
  • auto_now_add is set once on creation; auto_now updates on every save.
  • Meta.ordering gives a sensible default order so lists are not arbitrary.
  • CharField needs max_length. TextField does not, and is right for long text.

Relationships

Three kinds, matching the three ways records relate.

class Post(models.Model):
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="posts"
    )
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT, null=True
    )
    tags = models.ManyToManyField(Tag, blank=True)

# using them
post.author            # the User
user.posts.all()       # their posts, via related_name
post.tags.add(tag)
  • ForeignKey is many-to-one: many posts, one author.
  • on_delete is required and is a real decision. CASCADE deletes the posts with the user; PROTECT refuses to delete a user who has posts; SET_NULL keeps the posts and empties the field.
  • related_name is what you call the reverse direction. Without it you get the clumsy post_set.
  • blank=True affects forms. null=True affects the database. They are not the same thing.

Migrations

The record of how the database schema changed. Written by Django, checked in with your code, applied in order.

python manage.py makemigrations
python manage.py migrate

python manage.py showmigrations
python manage.py sqlmigrate posts 0002
  • Migrations are source code. Commit them; never edit an applied one by hand.
  • Changing a model without making a migration means the database no longer matches it, and the error appears later and elsewhere.
  • Adding a non-nullable field to a table with rows will ask you for a default. That prompt is not a bug.
  • sqlmigrate shows the SQL a migration will run, which is the fastest way to see what it actually does.

The ORM

Queries are written in Python and become SQL. The parts worth learning first are laziness, the N+1 problem, and the difference between filter and get.

Querying

Manager methods that return querysets, which can be chained.

Post.objects.all()
Post.objects.filter(published=True)
Post.objects.exclude(author=user)
Post.objects.get(pk=3)
Post.objects.filter(title__icontains="sankofa")
Post.objects.filter(created__year=2026)
Post.objects.order_by("-created")[:10]
Post.objects.count()
Post.objects.exists()
  • filter returns a queryset, possibly empty. get returns one object or raises.
  • get raises DoesNotExist when there is nothing and MultipleObjectsReturned when there is more than one. Use get_object_or_404 in views.
  • The double underscore is a lookup: __icontains, __gte, __in, __isnull, __year.
  • Slicing applies a LIMIT rather than fetching everything and cutting it up.

Querysets are lazy

Building a queryset touches the database not at all. It runs when you iterate it, index it, or ask for its length.

qs = Post.objects.filter(published=True)   # no query yet
qs = qs.order_by("-created")               # still none
qs = qs[:10]                               # still none

for post in qs:                            # now it runs
    print(post.title)
  • This is why you can build a query in pieces across several lines of a view.
  • It is also why a query inside a template loop can quietly run hundreds of times.

The N+1 problem

One query for the list, then one more per row when the template touches a related object. The single most common Django performance bug.

# 1 query for posts, then 1 per post for the author
posts = Post.objects.all()

# 1 query total, joined
posts = Post.objects.select_related("author")

# 2 queries total, for many-to-many and reverse relations
posts = Post.objects.prefetch_related("tags")
  • select_related for ForeignKey and OneToOne: it joins.
  • prefetch_related for ManyToMany and reverse relations: it runs a second query and matches them up.
  • Install Django Debug Toolbar and look at the query count on your list pages. It is usually a surprise.

Creating and updating

Save one object, or update many at once.

post = Post.objects.create(title="Kindred", body="…")

post.title = "Kindred, revisited"
post.save()

Post.objects.filter(published=False).update(published=True)

post.delete()
  • .update() on a queryset runs one SQL statement and does NOT call save() or fire signals.
  • That makes it fast and means custom logic in save() is skipped. Know which you want.
  • save(update_fields=["title"]) writes only that column.

Templates

Syntax

Double braces print a value. Brace-percent is a tag that does something.

{{ post.title }}
{{ post.created|date:"j F Y" }}
{{ post.body|truncatewords:40 }}

{% for post in posts %}
  <h2>{{ post.title }}</h2>
{% empty %}
  <p>Nothing published yet.</p>
{% endfor %}

{% if user.is_authenticated %}
  <a href="{% url 'posts:create' %}">Write</a>
{% endif %}
  • {% empty %} handles the empty list without a separate if. Use it.
  • Filters transform on the way out: date, truncatewords, length, default, join.
  • Templates deliberately cannot call functions with arguments. Logic belongs in the view.
  • Dots do a lot: attribute, dictionary key, then list index, in that order.

Inheritance

One base layout, and pages that fill in its holes.

{# base.html #}
<title>{% block title %}Sankofa Journal{% endblock %}</title>
<main>{% block content %}{% endblock %}</main>

{# post_detail.html #}
{% extends "base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
  <h1>{{ post.title }}</h1>
{% endblock %}
  • {% extends %} must be the first tag in the file.
  • Anything outside a block in a child template is ignored, which silently loses content.
  • {% include %} is for a fragment reused in several places; extends is for the page shape.

Autoescaping

Django escapes HTML in variables by default, which is what stops user content becoming a script tag.

{{ comment.body }}          {# escaped, safe #}
{{ comment.body|safe }}     {# NOT escaped, dangerous #}
  • Only use |safe on content you generated, never on anything a person typed.
  • If you need to allow some HTML from users, sanitise it on the way in with a library built for that.

Forms

ModelForm

A form derived from a model. Validation, rendering and saving, mostly for free.

from django import forms

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body", "published"]

    def clean_title(self):
        title = self.cleaned_data["title"]
        if len(title) < 3:
            raise forms.ValidationError("Title is too short.")
        return title
  • List fields explicitly. fields = "__all__" will happily expose a column you did not mean to.
  • clean_<fieldname> validates one field; clean() validates the form as a whole.
  • Use cleaned_data, never request.POST, once the form has validated.

Rendering a form

The form renders itself, and always needs the CSRF token.

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Save</button>
</form>

{# or field by field, for real control #}
{{ form.title.errors }}
<label for="{{ form.title.id_for_label }}">Title</label>
{{ form.title }}
  • Leaving out {% csrf_token %} gives a 403 on submit. It is the most common Django form error.
  • GET forms do not need it, because they should not change anything.
  • Render field by field when you care about the markup, which for accessibility you usually do.

Authentication and permissions

What comes built in

A User model, password hashing, login, logout, sessions, password reset and permissions, all present before you write anything.

from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required

@login_required
def create(request):
    ...

# in a template
{% if user.is_authenticated %}{{ user.username }}{% endif %}
  • Never store a password yourself. Django hashes them properly and upgrades the algorithm over time.
  • request.user is always present: either a real user or AnonymousUser.
  • login_required redirects to LOGIN_URL with a next parameter so they return where they were going.

Authentication is not authorisation

Knowing who someone is does not decide what they may do. Both checks are needed, and the second is the one people forget.

@login_required
def edit(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if post.author != request.user:
        raise PermissionDenied
    ...

# better: make it impossible to fetch someone else's
post = get_object_or_404(Post, pk=pk, author=request.user)
  • login_required alone means any signed-in person can edit any post by changing the number in the URL.
  • Filtering the query by the owner is safer than checking afterwards, because there is nothing to forget.
  • Hiding a button in the template is not access control. The URL is still there.

Custom user model

If you might ever need one, create it before your first migration. Changing it later is genuinely painful.

class User(AbstractUser):
    bio = models.TextField(blank=True)

# settings.py
AUTH_USER_MODEL = "accounts.User"

# always refer to it indirectly
from django.contrib.auth import get_user_model
User = get_user_model()
  • This is the one decision that is very hard to reverse. Django's own docs recommend doing it up front.
  • Use get_user_model() or settings.AUTH_USER_MODEL rather than importing User directly.

The admin

Registering a model

A working management interface for your data, from about four lines.

from django.contrib import admin

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "published", "created"]
    list_filter = ["published", "created"]
    search_fields = ["title", "body"]
    prepopulated_fields = {"slug": ("title",)}
  • Genuinely useful for staff and for your own debugging.
  • It is not a user-facing interface. It assumes trusted staff and shows everything.
  • python manage.py createsuperuser gets you in.

APIs

JSON without a framework

Fine for a handful of endpoints.

from django.http import JsonResponse

def post_list(request):
    posts = Post.objects.filter(published=True).values(
        "id", "title", "created"
    )
    return JsonResponse({"results": list(posts)})
  • .values() returns dictionaries, which JsonResponse can serialise directly.
  • Wrap a list in an object rather than returning a bare array, so you can add fields later.

Django REST Framework

The standard choice once an API is a real part of the product.

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "body", "created"]

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
  • Serializers are to APIs what forms are to HTML: validation plus conversion.
  • Set permission_classes deliberately on every viewset. The default is whatever is in settings.

Security

Django handles most of the classic vulnerabilities for you, provided you do not switch the protection off. These are the ways people switch it off.

The settings that matter in production

A short list, and every one of them has caused a real incident somewhere.

DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = ["sankofacode.org"]

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
  • DEBUG = True in production shows your settings, your paths and your queries on any error page.
  • The secret key signs sessions and password reset links. It belongs in the environment, never in git.
  • python manage.py check --deploy audits this for you and explains each warning.

What Django protects you from, and how to undo it

The protections are on by default. Each has a specific way of being disabled by accident.

# SQL injection: safe
Post.objects.filter(title=user_input)

# unsafe: raw SQL built by hand
Post.objects.raw("SELECT * FROM posts WHERE title = '%s'" % user_input)

# XSS: safe by default, unsafe with |safe on user content
# CSRF: safe with {% csrf_token %}, unsafe with @csrf_exempt
  • The ORM parameterises queries, so ordinary filtering is safe.
  • If you must use raw(), pass parameters as the second argument rather than formatting them in.
  • @csrf_exempt on a POST view removes a real protection. Almost nobody needs it.

Testing

Writing a test

Django gives you a test client that makes requests without a running server.

from django.test import TestCase
from django.urls import reverse

class PostTests(TestCase):
    def setUp(self):
        self.post = Post.objects.create(
            title="Kindred", body="…", published=True
        )

    def test_detail_page_shows_title(self):
        url = reverse("posts:detail", args=[self.post.pk])
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Kindred")

    def test_draft_is_not_public(self):
        self.post.published = False
        self.post.save()
        response = self.client.get(self.post.get_absolute_url())
        self.assertEqual(response.status_code, 404)
  • Each test runs in a transaction against a temporary database and is rolled back afterwards.
  • Test behaviour people depend on: that a draft is not public, that a stranger cannot edit.
  • The second test above is the kind that catches a real security mistake.
  • python manage.py test runs them.

Deployment

What changes from development

The development server is not for production, and several settings must differ.

pip freeze > requirements.txt
python manage.py check --deploy
python manage.py collectstatic
python manage.py migrate

gunicorn sankofa_journal.wsgi
  • runserver is single-threaded and unhardened. Use gunicorn or uvicorn behind a real web server.
  • Django does not serve static files when DEBUG is False. Use WhiteNoise or your host's static hosting.
  • Run migrations as part of deploying, not by hand afterwards and hoping.
  • Settings that differ per environment come from environment variables, not from an if in settings.py.

Common errors, and what they mean

TemplateDoesNotExist

Django looked in every configured directory and found nothing at that path.

  • Templates belong at app/templates/app/name.html. The repeated app name is deliberate and prevents collisions.
  • The app must be in INSTALLED_APPS for its template folder to be searched at all.

You have unapplied migrations

The models changed and the database has not caught up.

python manage.py makemigrations
python manage.py migrate

NoReverseMatch

A {% url %} tag or reverse() call names a route that does not exist, or passes the wrong arguments.

  • Check the name, the app_name prefix, and whether the route expects an argument you did not pass.

CSRF verification failed

A POST arrived without a valid token.

<form method="post">
  {% csrf_token %}
</form>
  • Almost always a missing {% csrf_token %} in the form.

The view did not return an HttpResponse

A branch of the view falls off the end without returning anything.

  • Usually an if with no else, on the path you did not test.

RelatedObjectDoesNotExist / DoesNotExist

get() found nothing.

post = get_object_or_404(Post, pk=pk)

# or handle it yourself
post = Post.objects.filter(pk=pk).first()   # None if absent

Everything shows as Post object (1)

The model has no __str__ method.

def __str__(self):
    return self.title

Want to learn how to build with this?

A reference tells you what exists. The course teaches you when to reach for it, and has you build something real while you learn.