--- name: django description: 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. license: 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 ```python 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. ```python async def article_detail(request, pk): article = await Article.objects.aget(pk=pk) return render(request, "articles/detail.html", {"article": article}) ``` ### Class-Based Views ```python 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 ```python 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 ```python # urls.py from django.urls import path, re_path, include app_name = "articles" # namespace urlpatterns = [ path("", views.ArticleListView.as_view(), name="list"), path("/", views.ArticleDetailView.as_view(), name="detail"), path("/edit/", views.ArticleUpdateView.as_view(), name="edit"), path("/", views.article_by_slug, name="by-slug"), re_path(r"^archive/(?P[0-9]{4})/$", views.archive, name="archive"), ] ``` **Including in the root URLconf:** ```python urlpatterns = [ path("articles/", include("articles.urls")), path("admin/", admin.site.urls), path("api/", include("api.urls")), ] ``` **Reversing URLs:** ```python 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 | |---|---| | `` | Zero or positive integer | | `` | Any non-empty string without `/` | | `` | ASCII letters, numbers, hyphens, underscores | | `` | UUID string | | `` | Any non-empty string including `/` | --- ## Templates & Forms ### Template Basics All templates extend a base: ```html {% extends "base.html" %} {% block extra_head %} {% endblock %} {% block content %}

{{ article.title }}

{% for tag in article.tags.all %} {{ tag.name }} {% endfor %} {% endblock %} {% block extra_scripts %} {% endblock %} ``` **Commonly used template tags:** ```html {% 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:** ```html {{ value|default:"n/a" }} {{ body|truncatewords:30 }} {{ created_at|date:"M j, Y" }} {{ price|floatformat:2 }} {{ name|lower }} ``` ### Forms ```python 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:** ```python 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:** ```html
{% csrf_token %} {{ form.as_p }}
{# Manual field rendering #}
{{ form.title.label_tag }} {{ form.title }} {{ form.title.errors }}
``` **New in Django 5.2 — form widgets:** - `forms.ColorInput` → `` - `forms.SearchInput` → `` - `forms.TelInput` → `` ### Django 5.2 — `simple_block_tag` Create template block tags that capture content: ```python from django import template register = template.Library() @register.simple_block_tag def card(content, title=""): return f'

{title}

{content}
' ``` --- ## Models ### Defining Models ```python 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 ```python class Release(models.Model): pk = models.CompositePrimaryKey("version", "name") version = models.IntegerField() name = models.CharField(max_length=20) ``` ### QuerySet Patterns ```python # 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 ```bash 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:** ```python # 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 ```python 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 ```python 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 ```python # 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 ```python 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 ```python 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"}) ``` ```python # 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 ```python 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): ```python # Include in request headers: # Authorization: Bearer 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 ```ini # 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: ```python 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 ```python 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 ```python 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 ```python @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) ```python 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 ```python 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 ```python STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" ``` ### Logging ```python 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}, }, } ```