mirror of
https://github.com/duthaho/claudekit.git
synced 2026-08-03 02:00:21 +03:00
feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.
This commit is contained in:
@@ -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"]
|
||||
```
|
||||
Reference in New Issue
Block a user