Skip to content

Django projects, models, views, templates, forms, authentication, testing, and deployment reference.

Django is a batteries-included Python web framework for database-backed websites and applications. It provides URL routing, views, templates, forms, an ORM, migrations, authentication, an administration site, security protections, and testing tools.

Create an isolated environment and install Django:

Terminal window
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# PowerShell
.\.venv\Scripts\Activate.ps1
python -m pip install Django
python -m django --version

A project contains site-wide configuration. An app is a focused feature package such as blog, accounts, or payments.

Terminal window
django-admin startproject config .
python manage.py startapp blog
python manage.py migrate
python manage.py runserver

Typical structure:

manage.py
config/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py
blog/
admin.py
apps.py
migrations/
models.py
tests.py
views.py

Add the application to INSTALLED_APPS:

config/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
blog/views.py
from django.http import HttpResponse
def home(request):
return HttpResponse("Hello, Django")

Map a URL to the view:

blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.home, name="home"),
]

Include application URLs in the project:

config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]
Terminal window
python manage.py runserver

Open http://127.0.0.1:8000/ in a browser.

blog/urls.py
urlpatterns = [
path("posts/<int:post_id>/", views.post_detail, name="post-detail"),
path("authors/<slug:username>/", views.author_detail, name="author-detail"),
]
blog/views.py
from django.http import JsonResponse
def post_detail(request, post_id):
return JsonResponse({"post_id": post_id})

Common path converters include str, int, slug, uuid, and path.

Models describe stored data and relationships:

blog/models.py
from django.conf import settings
from django.db import models
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name="posts",
)
status = models.CharField(
max_length=10,
choices=Status.choices,
default=Status.DRAFT,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("blog:post-detail", kwargs={"slug": self.slug})

Create and apply migration files whenever model structure changes:

Terminal window
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py sqlmigrate blog 0001

Commit migrations with the code change that requires them. Do not manually edit a production database schema instead of applying migrations.

Use the Django ORM to create, retrieve, update, and delete rows:

Terminal window
python manage.py shell
from blog.models import Category, Post
category = Category.objects.create(name="Python", slug="python")
Post.objects.all()
Post.objects.filter(status=Post.Status.PUBLISHED)
Post.objects.filter(title__icontains="django")
Post.objects.get(slug="getting-started")
Post.objects.order_by("-created_at")[:5]
Post.objects.filter(category__slug="python").count()
Post.objects.filter(status=Post.Status.DRAFT).update(status=Post.Status.PUBLISHED)
Post.objects.filter(slug="old-post").delete()

Reduce repeated database queries when loading related objects:

posts = Post.objects.select_related("author", "category")
categories = Category.objects.prefetch_related("posts")

Use select_related() for foreign-key or one-to-one relationships and prefetch_related() for many-to-many or reverse relationships.

blog/views.py
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(status=Post.Status.PUBLISHED)
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, status=Post.Status.PUBLISHED)
return render(request, "blog/post_detail.html", {"post": post})
blog/urls.py
urlpatterns = [
path("posts/", views.post_list, name="post-list"),
path("posts/<slug:slug>/", views.post_detail, name="post-detail"),
]

Place templates in an app-scoped directory:

blog/
templates/
blog/
base.html
post_list.html
post_detail.html
blog/templates/blog/base.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Blog{% endblock %}</title>
</head>
<body>
<nav><a href="{% url 'blog:post-list' %}">Posts</a></nav>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
blog/templates/blog/post_list.html
{% extends "blog/base.html" %}
{% block title %}Posts{% endblock %}
{% block content %}
<h1>Posts</h1>
{% for post in posts %}
<article>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{% endblock %}

Templates escape variable output by default. Avoid marking user-supplied content as safe unless it has been sanitized deliberately.

blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "category", "status"]
def clean_title(self):
title = self.cleaned_data["title"].strip()
if len(title) < 5:
raise forms.ValidationError("Title must contain at least 5 characters.")
return title
blog/views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from .forms import PostForm
@login_required
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
blog/templates/blog/post_form.html
{% extends "blog/base.html" %}
{% block content %}
<h1>New post</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}

For internal HTML forms that submit POST, include {% csrf_token %} and keep Django’s CSRF middleware enabled.

Terminal window
python manage.py createsuperuser
python manage.py runserver

Visit http://127.0.0.1:8000/admin/.

blog/admin.py
from django.contrib import admin
from .models import Category, Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "created_at"]
list_filter = ["status", "created_at", "category"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "created_at"
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}

The admin is excellent for trusted internal management. Build dedicated public views and authorization rules for end users.

Generic views reduce repeated list, detail, create, update, and delete logic:

blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 20
def get_queryset(self):
return Post.objects.filter(status=Post.Status.PUBLISHED)
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ["title", "slug", "body", "category", "status"]
success_url = reverse_lazy("blog:post-list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
blog/urls.py
from .views import PostCreateView, PostDetailView, PostListView
urlpatterns = [
path("posts/", PostListView.as_view(), name="post-list"),
path("posts/new/", PostCreateView.as_view(), name="post-create"),
path("posts/<slug:slug>/", PostDetailView.as_view(), name="post-detail"),
]

Use function views when they make behavior clearer; use generic views when their lifecycle fits the feature naturally.

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

Provide templates such as registration/login.html, then use authentication helpers:

from django.contrib.auth.decorators import login_required, permission_required
@login_required
def profile(request):
...
@permission_required("blog.change_post", raise_exception=True)
def moderate_post(request, post_id):
...
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
permission_required = "blog.change_post"

For a new project that may need additional user fields, define a custom user model before the first migration:

accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
config/settings.py
AUTH_USER_MODEL = "accounts.User"

Changing the user model after tables and relationships already exist is considerably more involved.

config/settings.py
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
{% load static %}
<link rel="stylesheet" href="{% static 'blog/site.css' %}">
Terminal window
python manage.py collectstatic

In production, serve collected static assets through an appropriate static-file strategy rather than relying on Django’s development server.

config/settings.py
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
class Post(models.Model):
image = models.ImageField(upload_to="posts/", blank=True)
Terminal window
python -m pip install Pillow # Required for ImageField processing
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
form = PostForm(request.POST, request.FILES)

Validate uploaded file types and sizes, and use a suitable storage backend in production.

Keep secrets outside source control:

config/settings.py
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

Database example:

DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ.get("POSTGRES_HOST", "localhost"),
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
}
}

SQLite is convenient locally. Production applications commonly use a database server such as PostgreSQL.

blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Category, Post
class PostTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_user(
username="author",
password="test-password",
)
category = Category.objects.create(name="Python", slug="python")
self.post = Post.objects.create(
title="Django basics",
slug="django-basics",
body="A post",
author=user,
category=category,
status=Post.Status.PUBLISHED,
)
def test_post_string(self):
self.assertEqual(str(self.post), "Django basics")
def test_published_post_is_listed(self):
response = self.client.get(reverse("blog:post-list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Django basics")
Terminal window
python manage.py test
python manage.py test blog
response = self.client.get("/posts/")
response = self.client.post("/posts/new/", {"title": "New post"})
self.client.login(username="author", password="test-password")
self.client.force_login(user)

Common built-in commands:

Terminal window
python manage.py check # Validate project configuration
python manage.py shell # Interactive shell with Django configured
python manage.py dbshell # Open the database command-line client
python manage.py dumpdata blog # Export app data as JSON
python manage.py loaddata seed.json # Load fixture data
python manage.py changepassword admin
python manage.py clearsessions

Create a custom command for repeatable operational tasks:

blog/
management/
__init__.py
commands/
__init__.py
publish_scheduled.py
blog/management/commands/publish_scheduled.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Publish scheduled posts"
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS("Scheduled posts published"))
Terminal window
python manage.py publish_scheduled
from django.db import transaction
from django.db.models import Count
popular_categories = Category.objects.annotate(post_count=Count("posts"))
@transaction.atomic
def publish_post(post):
post.status = Post.Status.PUBLISHED
post.save(update_fields=["status"])
from django.core.paginator import Paginator
def post_list(request):
posts = Post.objects.filter(status=Post.Status.PUBLISHED)
page = Paginator(posts, 20).get_page(request.GET.get("page"))
return render(request, "blog/post_list.html", {"page": page})

Optimize after inspecting query behavior: reduce unnecessary columns, avoid repeated related-object queries, add indexes for real lookup patterns, and paginate large result sets.

Do not deploy with development defaults:

DEBUG = False
ALLOWED_HOSTS = ["www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://www.example.com"]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Before a release:

Terminal window
python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py test

Use an ASGI or WSGI production server, configure HTTPS at the application or reverse-proxy layer, keep SECRET_KEY private, and arrange logging, backups, and error monitoring.

The generated project includes:

config/asgi.py # ASGI application for asynchronous-capable servers
config/wsgi.py # WSGI application for traditional servers

Run through an installed production server according to the chosen hosting stack; python manage.py runserver is only for development.

ProblemBetter approach
Editing models without migrationsRun makemigrations and commit migration files
Hardcoding secrets in settings.pyLoad secrets from environment or secret storage
Using DEBUG=True in productionSet production security settings and run check --deploy
Submitting forms without CSRF tokensInclude {% csrf_token %} for internal POST forms
Querying related data in a loopUse select_related() or prefetch_related()
Trusting admin access for public permissionsAdd explicit authentication and authorization checks
Terminal window
python -m pip install Django # Install
django-admin startproject config . # Create project
python manage.py startapp blog # Create app
python manage.py makemigrations # Generate schema migration
python manage.py migrate # Apply migrations
python manage.py createsuperuser # Create admin login
python manage.py runserver # Development server
python manage.py shell # Django shell
python manage.py test # Test suite
python manage.py collectstatic # Gather static files for deployment
python manage.py check --deploy # Production setting checks
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import path, reverse
from .models import Post
Post.objects.create(...)
Post.objects.filter(status="published")
Post.objects.get(pk=1)
Post.objects.select_related("author")
Post.objects.prefetch_related("tags")