21 KiB
name, description, license
| name | description | license |
|---|---|---|
| django | Guides development with Django 5.2.11. Use when writing models, views, URL routing, templates, forms, migrations, the Django admin, Django REST Framework (DRF) APIs, or pytest-django tests. | Internal Use Only |
Django 5.2.11
Overview
Use this skill whenever working with Django — whether building views, writing models and migrations, working with templates and forms, configuring the admin, building REST APIs with DRF, or writing tests with pytest-django.
Keywords: django, models, views, urls, templates, forms, migrations, admin, rest framework, drf, serializers, viewsets, pytest, testing, async, orm, queryset
Version: Django 5.2.11 (LTS — security updates through 2028), Python 3.11, PostgreSQL
Official docs: https://docs.djangoproject.com/en/5.2/
Views & URL Routing
Function-Based Views
from django.http import HttpResponse, Http404
from django.shortcuts import render, get_object_or_404, redirect
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, "articles/detail.html", {"article": article})
Async Views
Async views require an ASGI server to gain performance benefits. All handlers in a view must be either sync or async — never mixed.
async def article_detail(request, pk):
article = await Article.objects.aget(pk=pk)
return render(request, "articles/detail.html", {"article": article})
Class-Based Views
from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
paginate_by = 20
def get_queryset(self):
return Article.objects.filter(published=True).order_by("-created_at")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["categories"] = Category.objects.all()
return context
Generic CBV quick-reference:
| Class | Purpose |
|---|---|
TemplateView |
Renders a template; override get_context_data() |
ListView |
List of objects; use paginate_by for pagination |
DetailView |
Single object by pk or slug |
CreateView |
Form to create an object; set fields or form_class |
UpdateView |
Form to edit an existing object |
DeleteView |
Confirm and delete an object |
RedirectView |
HTTP redirect; set url or pattern_name |
FormView |
Generic form handling without a model |
Access Control Mixins & Decorators
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.decorators import login_required, permission_required
# CBV
class MyView(LoginRequiredMixin, View):
login_url = "/login/"
class AdminOnlyView(PermissionRequiredMixin, View):
permission_required = "myapp.change_article"
# FBV
@login_required
def my_view(request): ...
@permission_required("myapp.add_article")
def create_article(request): ...
URL Configuration
# urls.py
from django.urls import path, re_path, include
app_name = "articles" # namespace
urlpatterns = [
path("", views.ArticleListView.as_view(), name="list"),
path("<int:pk>/", views.ArticleDetailView.as_view(), name="detail"),
path("<int:pk>/edit/", views.ArticleUpdateView.as_view(), name="edit"),
path("<slug:slug>/", views.article_by_slug, name="by-slug"),
re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive, name="archive"),
]
Including in the root URLconf:
urlpatterns = [
path("articles/", include("articles.urls")),
path("admin/", admin.site.urls),
path("api/", include("api.urls")),
]
Reversing URLs:
from django.urls import reverse, reverse_lazy
reverse("articles:detail", args=[pk])
reverse("articles:list", query={"page": 2}) # Django 5.2 — adds ?page=2
reverse_lazy("articles:list") # safe for use in class attributes
Path converters:
| Converter | Matches |
|---|---|
<int:pk> |
Zero or positive integer |
<str:name> |
Any non-empty string without / |
<slug:slug> |
ASCII letters, numbers, hyphens, underscores |
<uuid:id> |
UUID string |
<path:rest> |
Any non-empty string including / |
Templates & Forms
Template Basics
All templates extend a base:
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'myapp/style.css' %}">
{% endblock %}
{% block content %}
<h1>{{ article.title }}</h1>
{% for tag in article.tags.all %}
<span>{{ tag.name }}</span>
{% endfor %}
{% endblock %}
{% block extra_scripts %}
<script src="{% static 'myapp/app.js' %}"></script>
{% endblock %}
Commonly used template tags:
{% url 'articles:detail' article.pk %}
{% static 'myapp/logo.png' %}
{% include '_sidebar.html' %}
{% with total=articles.count %}{{ total }}{% endwith %}
{% if user.is_authenticated %}...{% endif %}
{% csrf_token %}
Built-in filters:
{{ value|default:"n/a" }}
{{ body|truncatewords:30 }}
{{ created_at|date:"M j, Y" }}
{{ price|floatformat:2 }}
{{ name|lower }}
Forms
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "body", "category", "published"]
widgets = {
"body": forms.Textarea(attrs={"rows": 10}),
}
def clean_title(self):
title = self.cleaned_data["title"]
if len(title) < 5:
raise forms.ValidationError("Title must be at least 5 characters.")
return title
def clean(self):
cleaned_data = super().clean()
# Cross-field validation here
return cleaned_data
Handling a form in a view:
def create_article(request):
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
return redirect("articles:detail", pk=article.pk)
else:
form = ArticleForm()
return render(request, "articles/form.html", {"form": form})
Rendering forms in templates:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{# Manual field rendering #}
<div class="field{% if form.title.errors %} error{% endif %}">
{{ form.title.label_tag }}
{{ form.title }}
{{ form.title.errors }}
</div>
New in Django 5.2 — form widgets:
forms.ColorInput→<input type="color">forms.SearchInput→<input type="search">forms.TelInput→<input type="tel">
Django 5.2 — simple_block_tag
Create template block tags that capture content:
from django import template
register = template.Library()
@register.simple_block_tag
def card(content, title=""):
return f'<div class="card"><h2>{title}</h2>{content}</div>'
Models
Defining Models
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(
"auth.User",
on_delete=models.SET_NULL,
null=True,
related_name="articles",
)
category = models.ForeignKey(
"Category",
on_delete=models.PROTECT,
related_name="articles",
)
tags = models.ManyToManyField("Tag", blank=True, related_name="articles")
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name_plural = "articles"
indexes = [models.Index(fields=["slug"])]
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse("articles:detail", args=[self.pk])
on_delete options:
| Option | Behavior |
|---|---|
CASCADE |
Delete child when parent is deleted |
PROTECT |
Raise ProtectedError if related objects exist |
SET_NULL |
Set FK to NULL (requires null=True) |
SET_DEFAULT |
Set FK to field default |
DO_NOTHING |
Do nothing (risks integrity errors) |
Django 5.2 — Composite Primary Keys
class Release(models.Model):
pk = models.CompositePrimaryKey("version", "name")
version = models.IntegerField()
name = models.CharField(max_length=20)
QuerySet Patterns
# Filtering
Article.objects.filter(published=True, author__username="john")
Article.objects.exclude(created_at__year=2023)
Article.objects.filter(title__icontains="django")
# get_or_create / update_or_create
article, created = Article.objects.get_or_create(
slug="my-article",
defaults={"title": "My Article", "body": "..."},
)
# Aggregation
from django.db.models import Count, Avg, Q
Article.objects.annotate(comment_count=Count("comments"))
Article.objects.aggregate(avg_views=Avg("views"))
# Complex filters with Q
Article.objects.filter(
Q(title__icontains="django") | Q(body__icontains="django")
)
# F expressions (compare fields without fetching)
from django.db.models import F
Article.objects.filter(views__gt=F("likes") * 2)
# select_related (SQL JOIN for FK/OneToOne)
Article.objects.select_related("author", "category")
# prefetch_related (separate query for M2M/reverse FK)
Article.objects.prefetch_related("tags", "comments")
# Bulk operations
Article.objects.filter(published=False).update(published=True)
Article.objects.bulk_create([Article(...), Article(...)])
# Async ORM (for async views)
article = await Article.objects.aget(pk=pk)
articles = [a async for a in Article.objects.filter(published=True)]
Migrations
python manage.py makemigrations # Generate migration files
python manage.py migrate # Apply migrations
python manage.py showmigrations # Show status
python manage.py sqlmigrate myapp 0003 # Preview SQL
python manage.py migrate myapp 0002 # Roll back to 0002
Data migration:
# Create empty migration, then add:
from django.db import migrations
def populate_slugs(apps, schema_editor):
Article = apps.get_model("articles", "Article") # always use historical model
for article in Article.objects.all():
article.slug = article.title.lower().replace(" ", "-")
article.save()
class Migration(migrations.Migration):
dependencies = [("articles", "0003_article_slug")]
operations = [migrations.RunPython(populate_slugs, migrations.RunPython.noop)]
Django Admin
Registering Models
from django.contrib import admin
from .models import Article, Category
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ["title", "author", "category", "published", "created_at"]
list_filter = ["published", "category", "created_at"]
search_fields = ["title", "body", "author__username"]
search_help_text = "Search by title, body, or author username"
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
list_per_page = 50
fieldsets = [
(None, {"fields": ["title", "slug", "author", "category"]}),
("Content", {"fields": ["body", "tags"]}),
("Publishing", {"fields": ["published"]}),
("Metadata", {"classes": ["collapse"], "fields": ["created_at", "updated_at"]}),
]
@admin.action(description="Mark selected articles as published")
def make_published(self, request, queryset):
queryset.update(published=True)
self.message_user(request, "Selected articles marked as published.")
actions = ["make_published"]
def save_model(self, request, obj, form, change):
if not change:
obj.author = request.user
super().save_model(request, obj, form, change)
Inlines
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ["created_at"]
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
inlines = [CommentInline]
Django REST Framework
Setup
# settings.py
INSTALLED_APPS = [..., "rest_framework", "rest_framework.authtoken"]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
Serializers
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
author_username = serializers.CharField(source="author.username", read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Article
fields = ["id", "title", "slug", "body", "author_username",
"published", "created_at", "comment_count"]
read_only_fields = ["id", "slug", "created_at"]
def get_comment_count(self, obj):
return obj.comments.count()
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters.")
return value
def validate(self, data):
# Cross-field validation
return data
def create(self, validated_data):
validated_data["author"] = self.context["request"].user
return super().create(validated_data)
ViewSets & Routers
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Article.objects.filter(published=True).select_related("author")
@action(detail=False, methods=["get"])
def my_articles(self, request):
qs = Article.objects.filter(author=request.user)
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
article = self.get_object()
article.published = True
article.save()
return Response({"status": "published"})
# urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"articles", views.ArticleViewSet, basename="article")
urlpatterns = router.urls
ViewSet types:
| Class | Actions included |
|---|---|
ViewSet |
None — define all manually |
GenericViewSet |
None — mix in DRF mixins |
ReadOnlyModelViewSet |
list, retrieve |
ModelViewSet |
list, create, retrieve, update, partial_update, destroy |
Per-View Auth/Permission Override
from rest_framework.permissions import AllowAny, IsAdminUser
class ArticleViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [AllowAny()]
return [IsAuthenticated()]
Bearer Token Authentication
For requests requiring a bearer token (e.g., from Vision-Frontend to Vision-BackendAPI):
# Include in request headers:
# Authorization: Bearer <token>
class BearerTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header.split(" ", 1)[1]
# Validate token against Wristband or token store
...
Testing with pytest-django
Configuration
# pytest.ini or pyproject.toml [tool.pytest.ini_options]
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings.test
Database Access
Database access is blocked by default. Grant it via mark or fixture:
import pytest
@pytest.mark.django_db
def test_article_creation():
article = Article.objects.create(title="Hello", body="World")
assert article.pk is not None
@pytest.mark.django_db(transaction=True)
def test_with_transactions():
# Needed when testing code that uses transactions explicitly
...
@pytest.mark.django_db(reset_sequences=True)
def test_with_reset_sequences():
# Needed when auto-increment IDs matter
...
Key Fixtures
def test_view(client):
"""Anonymous test client"""
response = client.get("/articles/")
assert response.status_code == 200
def test_authenticated(client, django_user_model):
"""Log in and test protected views"""
user = django_user_model.objects.create_user(username="tester", password="pass")
client.force_login(user)
response = client.get("/dashboard/")
assert response.status_code == 200
def test_request_factory(rf):
"""RequestFactory — builds request objects without the middleware stack"""
request = rf.get("/articles/")
request.user = AnonymousUser()
response = ArticleListView.as_view()(request)
assert response.status_code == 200
def test_email(mailoutbox):
"""Assert emails sent during a test"""
send_welcome_email("user@example.com")
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == "Welcome!"
def test_with_settings(settings):
"""Override settings within a test"""
settings.DEBUG = True
settings.CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
...
DRF Testing
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
return APIClient()
@pytest.mark.django_db
def test_article_api(api_client, django_user_model):
user = django_user_model.objects.create_user(username="tester", password="pass")
api_client.force_authenticate(user=user)
response = api_client.get("/api/articles/")
assert response.status_code == 200
assert "results" in response.data
Fixtures with Database Access
@pytest.fixture
def article(db):
"""Use the db fixture (not the mark) when a fixture needs DB access"""
return Article.objects.create(title="Test Article", body="Body")
@pytest.fixture
def published_article(article):
article.published = True
article.save()
return article
Django 5.2 — Notable New Features
| Feature | Details |
|---|---|
| Composite Primary Keys | CompositePrimaryKey("field1", "field2") on the pk attribute |
| Auto model imports in shell | manage.py shell imports all models automatically |
reverse() query argument |
reverse("name", query={"page": 2}) appends query string |
HttpResponse.text |
String representation of response content |
HttpRequest.get_preferred_type() |
Query client's preferred media type |
preserve_request on redirects |
Preserves HTTP method on redirect() |
AlterConstraint migration op |
Change constraint metadata without drop/recreate |
| New form widgets | ColorInput, SearchInput, TelInput |
simple_block_tag decorator |
Create block-capturing template tags |
| Async auth methods | aauthenticate(), alogin(), alogout(), ahas_perm() |
CharField.max_length optional |
On SQLite only; still required on PostgreSQL |
QuerySet.explain() options |
memory and serialize kwargs on PostgreSQL 17+ |
Settings Reference
Database (PostgreSQL)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": env("DB_NAME"),
"USER": env("DB_USER"),
"PASSWORD": env("DB_PASSWORD"),
"HOST": env("DB_HOST", default="127.0.0.1"),
"PORT": env("DB_PORT", default="5432"),
}
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Security Checklist
SECRET_KEY = env("DJANGO_SECRET_KEY") # Never hardcode
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
# HTTPS in production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = True
Static & Media Files
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
Logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
"loggers": {
"django": {"handlers": ["console"], "level": "INFO", "propagate": False},
},
}