Files
claudekit/skills/backend-frameworks/references/django.md
T
2026-04-19 14:10:38 +07:00

713 lines
23 KiB
Markdown

# Backend Frameworks — Django Patterns
# Django
## When to Use
- Python web applications
- Admin interfaces
- Django REST Framework APIs
- Content-heavy sites with ORM-driven data models
## When NOT to Use
- FastAPI projects — use the `fastapi` skill instead for async APIs and microservices
- JavaScript/Node.js backends (Express, NestJS) — this skill is Python-only
- Microservices architectures — consider FastAPI instead for lightweight, async services
---
## Core Patterns
### 1. Models & ORM
#### Field types and relationships
```python
from django.db import models
from django.utils import timezone
class Organization(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class User(models.Model):
class Role(models.TextChoices):
ADMIN = "admin", "Administrator"
MEMBER = "member", "Member"
VIEWER = "viewer", "Viewer"
email = models.EmailField(unique=True)
name = models.CharField(max_length=100)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="members",
)
role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["email"]),
models.Index(fields=["organization", "role"]),
]
constraints = [
models.UniqueConstraint(
fields=["organization", "email"],
name="unique_org_email",
),
]
def __str__(self):
return self.email
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Project(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owned_projects")
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, blank=True, related_name="projects")
# OneToOneField for 1:1 relationships
settings = models.OneToOneField(
"ProjectSettings", on_delete=models.CASCADE, null=True, blank=True
)
class ProjectSettings(models.Model):
is_public = models.BooleanField(default=False)
max_members = models.IntegerField(default=10)
```
#### Custom managers and QuerySet methods
```python
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class UserQuerySet(models.QuerySet):
def admins(self):
return self.filter(role=User.Role.ADMIN)
def in_organization(self, org_id):
return self.filter(organization_id=org_id)
def with_project_count(self):
return self.annotate(project_count=models.Count("owned_projects"))
class User(models.Model):
# ... fields ...
objects = UserQuerySet.as_manager()
active = ActiveManager()
```
#### F objects, Q objects, and annotations
```python
from django.db.models import F, Q, Count, Avg, Sum, Value, When, Case
# F objects: reference model fields in queries
Project.objects.filter(updated_at__gt=F("created_at"))
User.objects.update(login_count=F("login_count") + 1) # Atomic increment
# Q objects: complex lookups with OR, AND, NOT
User.objects.filter(
Q(role="admin") | Q(role="member"),
~Q(is_active=False), # NOT inactive
)
# Annotations and aggregations
orgs = Organization.objects.annotate(
member_count=Count("members"),
admin_count=Count("members", filter=Q(members__role="admin")),
avg_projects=Avg("members__owned_projects"),
).filter(member_count__gte=5)
# Conditional expressions
users = User.objects.annotate(
tier=Case(
When(owned_projects__count__gte=10, then=Value("power")),
When(owned_projects__count__gte=3, then=Value("active")),
default=Value("starter"),
)
)
# Subqueries
from django.db.models import Subquery, OuterRef
latest_project = Project.objects.filter(
owner=OuterRef("pk")
).order_by("-created_at").values("title")[:1]
users = User.objects.annotate(latest_project_title=Subquery(latest_project))
```
### 2. Views
#### Function-based views
```python
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
@login_required
def project_detail(request, project_id):
project = get_object_or_404(
Project.objects.select_related("owner", "organization"),
pk=project_id,
)
if request.method == "POST":
form = ProjectForm(request.POST, instance=project)
if form.is_valid():
form.save()
return redirect("project-detail", project_id=project.id)
else:
form = ProjectForm(instance=project)
return render(request, "projects/detail.html", {
"project": project,
"form": form,
})
```
#### Class-based views
```python
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
class ProjectListView(LoginRequiredMixin, ListView):
model = Project
template_name = "projects/list.html"
context_object_name = "projects"
paginate_by = 20
def get_queryset(self):
qs = super().get_queryset().select_related("owner", "organization")
search = self.request.GET.get("q")
if search:
qs = qs.filter(
Q(title__icontains=search) | Q(description__icontains=search)
)
return qs
class ProjectCreateView(LoginRequiredMixin, CreateView):
model = Project
form_class = ProjectForm
template_name = "projects/form.html"
success_url = reverse_lazy("project-list")
def form_valid(self, form):
form.instance.owner = self.request.user
form.instance.organization = self.request.user.organization
return super().form_valid(form)
class ProjectDeleteView(PermissionRequiredMixin, DeleteView):
model = Project
permission_required = "projects.delete_project"
success_url = reverse_lazy("project-list")
```
#### Mixins for reuse
```python
class OrganizationFilterMixin:
"""Filter queryset to the current user's organization."""
def get_queryset(self):
return super().get_queryset().filter(
organization=self.request.user.organization
)
class ProjectListView(LoginRequiredMixin, OrganizationFilterMixin, ListView):
model = Project
# queryset is automatically filtered by organization
```
#### API views with Django REST Framework
```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def project_list(request):
if request.method == "GET":
projects = Project.objects.filter(organization=request.user.organization)
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
serializer = ProjectCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
```
### 3. Migrations
#### Creating and running migrations
```bash
# Generate migrations after model changes
python manage.py makemigrations app_name
# Preview SQL without applying
python manage.py sqlmigrate app_name 0001
# Apply migrations
python manage.py migrate
# Show migration status
python manage.py showmigrations
```
#### Data migrations with RunPython
```python
from django.db import migrations
def populate_slugs(apps, schema_editor):
Organization = apps.get_model("myapp", "Organization")
from django.utils.text import slugify
for org in Organization.objects.filter(slug=""):
org.slug = slugify(org.name)
org.save(update_fields=["slug"])
def reverse_populate_slugs(apps, schema_editor):
pass # No-op reverse
class Migration(migrations.Migration):
dependencies = [
("myapp", "0005_add_slug_field"),
]
operations = [
migrations.RunPython(populate_slugs, reverse_populate_slugs),
]
```
#### Squashing migrations
```bash
# Squash migrations 0001 through 0010 into one
python manage.py squashmigrations app_name 0001 0010
```
**Tips:**
- Always provide a reverse function for `RunPython` (even if it is a no-op)
- Use `apps.get_model()` in data migrations, never import models directly
- Test migrations on a copy of production data before deploying
### 4. Forms
#### ModelForm with custom validation
```python
from django import forms
from django.core.exceptions import ValidationError
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ["title", "description", "tags"]
widgets = {
"description": forms.Textarea(attrs={"rows": 4}),
"tags": forms.CheckboxSelectMultiple(),
}
def clean_title(self):
title = self.cleaned_data["title"]
if "test" in title.lower() and not self.instance.pk:
raise ValidationError("Title cannot contain 'test' for new projects.")
return title
def clean(self):
cleaned = super().clean()
title = cleaned.get("title", "")
description = cleaned.get("description", "")
if len(title) + len(description) < 20:
raise ValidationError("Title + description must be at least 20 characters.")
return cleaned
```
#### Formsets
```python
from django.forms import inlineformset_factory
TaskFormSet = inlineformset_factory(
Project,
Task,
fields=["title", "assigned_to", "due_date"],
extra=2, # Number of empty forms
can_delete=True,
max_num=20,
)
# In a view
def project_tasks(request, project_id):
project = get_object_or_404(Project, pk=project_id)
if request.method == "POST":
formset = TaskFormSet(request.POST, instance=project)
if formset.is_valid():
formset.save()
return redirect("project-detail", project_id=project.id)
else:
formset = TaskFormSet(instance=project)
return render(request, "projects/tasks.html", {"formset": formset})
```
### 5. Signals
```python
from django.db.models.signals import post_save, pre_save, m2m_changed
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(pre_save, sender=Project)
def set_project_slug(sender, instance, **kwargs):
if not instance.slug:
from django.utils.text import slugify
instance.slug = slugify(instance.title)
# Custom signals
from django.dispatch import Signal
project_published = Signal() # Accepts sender
@receiver(project_published)
def notify_members(sender, project, **kwargs):
for member in project.organization.members.all():
send_notification(member, f"Project '{project.title}' published")
# Firing a custom signal
project_published.send(sender=Project, project=project)
```
**When to use signals vs overriding `save()`:**
- Use signals when the action is a side effect (notifications, logging, cache invalidation)
- Override `save()` when the logic is core to the model's behavior (setting computed fields)
### 6. Middleware
```python
import time
from django.utils.deprecation import MiddlewareMixin
class TimingMiddleware(MiddlewareMixin):
def process_request(self, request):
request._start_time = time.perf_counter()
def process_response(self, request, response):
if hasattr(request, "_start_time"):
duration = time.perf_counter() - request._start_time
response["X-Process-Time"] = f"{duration:.4f}"
return response
# New-style middleware (function-based)
def organization_middleware(get_response):
def middleware(request):
if request.user.is_authenticated:
request.organization = request.user.organization
else:
request.organization = None
response = get_response(request)
return response
return middleware
# Register in 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",
"myapp.middleware.organization_middleware", # Custom
"myapp.middleware.TimingMiddleware", # Custom
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
```
### 7. Django REST Framework
#### Serializers
```python
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
project_count = serializers.IntegerField(read_only=True)
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "email", "name", "role", "full_name", "project_count", "created_at"]
read_only_fields = ["id", "created_at"]
def get_full_name(self, obj):
return f"{obj.name} ({obj.role})"
class ProjectSerializer(serializers.ModelSerializer):
owner = UserSerializer(read_only=True)
tags = serializers.SlugRelatedField(
many=True, slug_field="name", queryset=Tag.objects.all()
)
class Meta:
model = Project
fields = ["id", "title", "description", "owner", "tags", "created_at"]
def validate_title(self, value):
if len(value) < 3:
raise serializers.ValidationError("Title must be at least 3 characters.")
return value
class ProjectCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["title", "description", "tags"]
```
#### ViewSets and routers
```python
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
class ProjectViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["owner", "tags"]
search_fields = ["title", "description"]
ordering_fields = ["created_at", "title"]
ordering = ["-created_at"]
def get_queryset(self):
return Project.objects.filter(
organization=self.request.user.organization
).select_related("owner").prefetch_related("tags")
def get_serializer_class(self):
if self.action == "create":
return ProjectCreateSerializer
return ProjectSerializer
def perform_create(self, serializer):
serializer.save(
owner=self.request.user,
organization=self.request.user.organization,
)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
project = self.get_object()
project.is_published = True
project.save(update_fields=["is_published"])
return Response({"status": "published"})
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("projects", ProjectViewSet, basename="project")
router.register("users", UserViewSet, basename="user")
urlpatterns = [
path("api/", include(router.urls)),
]
```
#### Permissions
```python
from rest_framework.permissions import BasePermission
class IsOrganizationAdmin(BasePermission):
def has_permission(self, request, view):
return (
request.user.is_authenticated
and request.user.role == User.Role.ADMIN
)
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
```
#### Pagination
```python
from rest_framework.pagination import PageNumberPagination, CursorPagination
class StandardPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
class TimelinePagination(CursorPagination):
page_size = 50
ordering = "-created_at"
# settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "myapp.pagination.StandardPagination",
"PAGE_SIZE": 20,
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
```
### 8. Admin
```python
from django.contrib import admin
from django.utils.html import format_html
class TaskInline(admin.TabularInline):
model = Task
extra = 0
fields = ["title", "assigned_to", "status", "due_date"]
readonly_fields = ["created_at"]
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ["title", "owner_name", "organization", "tag_list", "created_at"]
list_filter = ["organization", "tags", "created_at"]
search_fields = ["title", "description", "owner__email"]
readonly_fields = ["created_at", "updated_at"]
autocomplete_fields = ["owner", "organization"]
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "created_at"
inlines = [TaskInline]
fieldsets = (
(None, {
"fields": ("title", "slug", "description"),
}),
("Ownership", {
"fields": ("owner", "organization", "tags"),
}),
("Metadata", {
"classes": ("collapse",),
"fields": ("created_at", "updated_at"),
}),
)
def owner_name(self, obj):
return obj.owner.name
owner_name.short_description = "Owner"
owner_name.admin_order_field = "owner__name"
def tag_list(self, obj):
return ", ".join(t.name for t in obj.tags.all())
tag_list.short_description = "Tags"
def get_queryset(self, request):
return super().get_queryset(request).select_related(
"owner", "organization"
).prefetch_related("tags")
# Custom admin actions
@admin.action(description="Mark selected projects as published")
def make_published(self, request, queryset):
count = queryset.update(is_published=True)
self.message_user(request, f"{count} projects published.")
actions = [make_published]
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ["email", "name", "organization", "role", "is_active"]
list_filter = ["role", "is_active", "organization"]
search_fields = ["email", "name"]
list_editable = ["role", "is_active"]
list_per_page = 50
```
---
## Best Practices
1. **Use `select_related` and `prefetch_related` on every query that touches relations**`select_related` for ForeignKey/OneToOne (SQL JOIN), `prefetch_related` for ManyToMany and reverse ForeignKey (separate query). Check queries with `django-debug-toolbar`.
2. **Keep business logic in model methods or service functions, not in views** — views should handle HTTP, forms should handle validation, models/services should handle domain logic. This makes code testable without needing HTTP.
3. **Use `get_queryset()` for dynamic filtering instead of hardcoding querysets** — both in views and DRF ViewSets. This enables mixin composition and per-request filtering (e.g., by organization).
4. **Write data migrations for schema changes that require backfills** — never assume fields can be added as non-nullable without a migration to populate existing rows. Use `RunPython` with a reverse function.
5. **Configure Django REST Framework defaults in settings** — set `DEFAULT_PAGINATION_CLASS`, `DEFAULT_PERMISSION_CLASSES`, `DEFAULT_AUTHENTICATION_CLASSES` in `REST_FRAMEWORK` dict to avoid repeating yourself on each ViewSet.
6. **Use `TextChoices` / `IntegerChoices` for enum fields** — they integrate with admin filters, serializer validation, and migrations automatically. Avoid plain strings or integers for status/role fields.
7. **Index frequently queried fields** — add `db_index=True` on individual fields or use `Meta.indexes` for composite indexes. Add `UniqueConstraint` for business-rule uniqueness.
8. **Use Django's `transaction.atomic()` for multi-step writes** — wrap create/update sequences that must succeed or fail together. DRF's `perform_create` and `perform_update` are good places for this.
```python
from django.db import transaction
@transaction.atomic
def transfer_project(project, new_owner):
old_owner = project.owner
project.owner = new_owner
project.save(update_fields=["owner"])
AuditLog.objects.create(
action="transfer",
project=project,
from_user=old_owner,
to_user=new_owner,
)
```
---
## Common Pitfalls
1. **N+1 queries** — accessing `project.owner.name` in a loop without `select_related("owner")` fires one query per iteration. Use `django-debug-toolbar` or `nplusone` to detect these. Always optimize queryset in `get_queryset()`.
2. **Importing models directly in data migrations** — models change over time, but migrations are frozen. Always use `apps.get_model("app_name", "ModelName")` inside `RunPython` functions, never `from myapp.models import Model`.
3. **Forgetting to call `full_clean()` in model saves** — Django's `save()` does NOT run validators by default. Only forms and serializers call `full_clean()`. If you save models directly, add explicit validation.
4. **Circular imports between apps** — referencing models across apps can cause import cycles. Use string references in ForeignKey: `models.ForeignKey("other_app.ModelName", ...)` instead of importing the class.
5. **Overusing signals for core logic** — signals make code harder to trace and debug. Use them for side effects (sending emails, cache invalidation), not for core domain logic. If logic should always run on save, override `save()` instead.
6. **Returning entire QuerySets from service functions** — QuerySets are lazy, which is usually good, but returning them from service layers can lead to unexpected queries executing in templates. Use `.values()`, `.values_list()`, or serialize to dicts when crossing layer boundaries.
---
## Related Skills
- `python` — Python language patterns and best practices
- `postgresql` — Database integration and query optimization
- `pytest` — Testing Django applications with pytest-django