feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.

This commit is contained in:
duthaho
2026-03-30 12:18:00 +07:00
parent 0ff5ae4082
commit 7fa9a48c6c
89 changed files with 25808 additions and 923 deletions
+662 -38
View File
@@ -1,91 +1,715 @@
---
name: django
description: >
Use this skill when working with Django web applications, Django ORM models, Django REST Framework APIs, or Django admin interfaces. Trigger for any mention of Django views, serializers, migrations, URL routing, class-based views, or Django middleware. Also applies when building Python web apps with templates, managing database schemas through Django migrations, or setting up admin panels.
---
# Django
## Description
Django web framework with ORM, views, and REST framework patterns.
## 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 `frameworks/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
### Models
### 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']
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)
```
### Views (Class-based)
#### Custom managers and QuerySet methods
```python
from django.views.generic import ListView, DetailView
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class UserListView(ListView):
model = User
template_name = 'users/list.html'
context_object_name = 'users'
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
class UserDetailView(DetailView):
model = User
template_name = 'users/detail.html'
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")
```
### Django REST Framework
#### Mixins for reuse
```python
from rest_framework import serializers, viewsets
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 UserSerializer(serializers.ModelSerializer):
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 = User
fields = ['id', 'email', 'name', 'created_at']
model = Project
fields = ["title", "description", "tags"]
widgets = {
"description": forms.Textarea(attrs={"rows": 4}),
"tags": forms.CheckboxSelectMultiple(),
}
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
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
```
### URLs
#### Formsets
```python
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.forms import inlineformset_factory
router = DefaultRouter()
router.register('users', UserViewSet)
TaskFormSet = inlineformset_factory(
Project,
Task,
fields=["title", "assigned_to", "due_date"],
extra=2, # Number of empty forms
can_delete=True,
max_num=20,
)
urlpatterns = [
path('api/', include(router.urls)),
# 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 class-based views for standard CRUD
2. Define model methods for business logic
3. Use serializers for validation
4. Add proper permissions
5. Use select_related/prefetch_related for queries
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
- **N+1 queries**: Use select_related/prefetch_related
- **Missing migrations**: Run makemigrations
- **No validation**: Use serializers properly
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
- `languages/python` — Python language patterns and best practices
- `databases/postgresql` — Database integration and query optimization
- `testing/pytest` — Testing Django applications with pytest-django
@@ -0,0 +1,250 @@
# Django Patterns Quick Reference
## QuerySet Patterns
### select_related (FK/OneToOne - single JOIN)
```python
# BAD: N+1 queries
for order in Order.objects.all():
print(order.customer.name) # Hits DB each iteration
# GOOD: 1 query with JOIN
for order in Order.objects.select_related("customer"):
print(order.customer.name)
# Chain through multiple FKs
Order.objects.select_related("customer__company")
```
### prefetch_related (M2M/reverse FK - separate query)
```python
# BAD: N+1 on reverse FK
for author in Author.objects.all():
print(author.book_set.all()) # Query per author
# GOOD: 2 queries total
for author in Author.objects.prefetch_related("books"):
print(author.books.all())
# Custom prefetch with filtering
from django.db.models import Prefetch
Author.objects.prefetch_related(
Prefetch("books", queryset=Book.objects.filter(published=True), to_attr="published_books")
)
```
### F Objects (reference model fields in queries)
```python
from django.db.models import F
Product.objects.filter(stock__lt=F("reorder_level")) # Compare fields
Product.objects.filter(id=1).update(stock=F("stock") - 1) # Atomic update
Order.objects.filter(amount__gt=F("customer__credit_limit")) # Across relations
```
### Q Objects (complex lookups with OR/NOT)
```python
from django.db.models import Q
# OR
User.objects.filter(Q(role="admin") | Q(is_superuser=True))
# NOT
User.objects.filter(~Q(status="banned"))
# Complex combinations
User.objects.filter(
(Q(role="admin") | Q(role="staff")) & ~Q(status="inactive")
)
# Dynamic query building
conditions = Q()
if name: conditions &= Q(name__icontains=name)
if email: conditions &= Q(email__icontains=email)
User.objects.filter(conditions)
```
### Subquery and OuterRef
```python
from django.db.models import Subquery, OuterRef, Exists
# Subquery: latest order date per customer
latest_order = Order.objects.filter(
customer=OuterRef("pk")
).order_by("-created_at").values("created_at")[:1]
Customer.objects.annotate(last_order=Subquery(latest_order))
# Exists: customers with orders
Customer.objects.annotate(
has_orders=Exists(Order.objects.filter(customer=OuterRef("pk")))
).filter(has_orders=True)
```
### Aggregation
```python
from django.db.models import Count, Sum, Avg
# Aggregate (returns dict)
Order.objects.aggregate(total=Sum("amount"), avg=Avg("amount"))
# Annotate (per-row)
Customer.objects.annotate(order_count=Count("orders"))
```
---
## Model Patterns
### Abstract Base Model
```python
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # No DB table created
class Order(TimestampMixin):
amount = models.DecimalField(max_digits=10, decimal_places=2)
# Inherits created_at, updated_at
```
### Proxy Model (same table, different behavior)
```python
class PendingOrderManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status="pending")
class PendingOrder(Order):
objects = PendingOrderManager()
class Meta:
proxy = True # Same DB table as Order
```
### Custom Manager and QuerySet
```python
class PublishedQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published")
def by_author(self, author):
return self.filter(author=author)
class Article(models.Model):
objects = PublishedQuerySet.as_manager()
# Chainable: Article.objects.published().by_author(user)
```
---
## View Patterns
### Class-Based View Mixins
| Mixin | Purpose |
|-------|---------|
| `LoginRequiredMixin` | Require authentication |
| `PermissionRequiredMixin` | Require specific permission |
| `UserPassesTestMixin` | Custom permission test |
| `FormView` | Handle form display + submission |
| `CreateView` / `UpdateView` | Model form CRUD |
| `ListView` / `DetailView` | Read operations |
```python
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Order
permission_required = "orders.view_order"
paginate_by = 25
def get_queryset(self):
return super().get_queryset().filter(customer=self.request.user)
```
---
## Django REST Framework Patterns
### Nested Serializers
```python
class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ["street", "city", "zip_code"]
class CustomerSerializer(serializers.ModelSerializer):
address = AddressSerializer()
class Meta:
model = Customer
fields = ["id", "name", "address"]
def create(self, validated_data):
address_data = validated_data.pop("address")
address = Address.objects.create(**address_data)
return Customer.objects.create(address=address, **validated_data)
```
### Custom Permissions
```python
from rest_framework.permissions import BasePermission
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in ("GET", "HEAD", "OPTIONS"):
return True
return request.user.is_staff
# Usage
class OrderViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwner]
```
### ViewSet Actions
```python
from rest_framework.decorators import action
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer
@action(detail=True, methods=["post"])
def cancel(self, request, pk=None):
order = self.get_object()
order.cancel()
return Response({"status": "cancelled"})
@action(detail=False, methods=["get"])
def summary(self, request):
return Response(self.get_queryset().aggregate(total=Sum("amount"), count=Count("id")))
```
### Filtering (django-filter)
```python
class OrderFilter(django_filters.FilterSet):
min_amount = django_filters.NumberFilter(field_name="amount", lookup_expr="gte")
max_amount = django_filters.NumberFilter(field_name="amount", lookup_expr="lte")
class Meta:
model = Order
fields = ["status", "customer"]
```