| name | django-conventions |
| description | Django web framework conventions: app layout, settings split (base/local/production), URLconf with app namespacing, CBV and DRF ViewSets, DRF serializers and permissions, form validation, signals, Django admin registration, and middleware. Activated automatically by django-plugin/stack.md. Works alongside python-foundation:python-conventions and django-plugin:django-orm-patterns.
Use this skill to:
- Structure a Django project with multiple apps and correct URLconf hierarchy.
- Write CBVs and DRF ViewSets with appropriate permissions and serializers.
- Configure settings correctly for different environments.
- Use signals for decoupled event handling between apps.
- Register models in Django admin with useful list_display and search_fields.
Do NOT use this skill for:
- Django ORM model field finalization and migration patterns — see django-plugin:django-orm-patterns.
- Python language idioms (type hints, dataclasses, enums) — see python-foundation:python-conventions.
- Testing patterns — see python-foundation:pytest-testing.
|
Django Conventions
This skill encodes the conventions used across modern Django projects (Django 4.x / 5.x). Apply alongside python-foundation:python-conventions (language idioms) and django-plugin:django-orm-patterns (model/query patterns) when implementing features.
1. Detection
Before writing code, read:
manage.py — confirms this is a Django project.
settings.py or settings/base.py — check DJANGO_VERSION, INSTALLED_APPS, REST_FRAMEWORK dict (DRF present?), AUTH_USER_MODEL.
- The project
urls.py — understand the existing URL hierarchy and namespace conventions.
requirements.txt or pyproject.toml — note the exact Django version and whether DRF, django-environ, django-filter, pytest-django, etc. are present.
2. Project and app layout
Prefer a dedicated apps/ directory for application modules and a config/ directory for project-level config. New apps are registered in INSTALLED_APPS using their AppConfig dotted path.
myproject/
manage.py
config/
settings/
base.py
local.py
production.py
urls.py
wsgi.py
asgi.py
apps/
users/
migrations/
models.py
views.py
serializers.py
urls.py
admin.py
apps.py
signals.py
orders/
migrations/
models.py
views.py
serializers.py
urls.py
admin.py
apps.py
apps.py for each application:
from django.apps import AppConfig
class OrdersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.orders'
verbose_name = 'Orders'
def ready(self) -> None:
import apps.orders.signals
3. Settings split
Production settings must never contain DEBUG = True or a hardcoded SECRET_KEY.
import environ
env = environ.Env()
SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'apps.users',
'apps.orders',
]
DATABASES = {
'default': env.db('DATABASE_URL'),
}
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
from .base import *
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
4. URLconf
Project urls.py uses include() with app namespaces. DRF ViewSets are registered on a DefaultRouter in the app's urls.py.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('apps.orders.urls', namespace='orders')),
path('', include('apps.users.urls', namespace='users')),
]
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
app_name = 'orders'
router = DefaultRouter()
router.register(r'orders', views.OrderViewSet, basename='order')
urlpatterns = [
path('', include(router.urls)),
path('orders/<int:pk>/cancel/', views.OrderCancelView.as_view(), name='order-cancel'),
]
5. DRF ViewSets and serializers
ModelViewSet
from rest_framework import viewsets, permissions
from django_filters.rest_framework import DjangoFilterBackend
from .models import Order
from .serializers import OrderSerializer, OrderCreateSerializer
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.select_related('user').order_by('-created_at')
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['status']
def get_serializer_class(self):
if self.action == 'create':
return OrderCreateSerializer
return OrderSerializer
def get_queryset(self):
return super().get_queryset().filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Serializers
Use separate serializers when the create/update payload differs from the list/retrieve shape.
from rest_framework import serializers
from .models import Order
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['id', 'status', 'product', 'qty', 'created_at']
read_only_fields = ['id', 'status', 'created_at']
class OrderCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['product', 'qty']
def validate_qty(self, value: int) -> int:
if value <= 0:
raise serializers.ValidationError("Quantity must be positive.")
return value
def validate(self, attrs: dict) -> dict:
return attrs
Nested serializer
class OrderWithUserSerializer(serializers.ModelSerializer):
user = UserSummarySerializer(read_only=True)
class Meta:
model = Order
fields = ['id', 'user', 'status', 'created_at']
6. DRF permissions
Always declare permission_classes explicitly on every ViewSet and APIView. Do not rely on global defaults alone.
from rest_framework.permissions import BasePermission, IsAuthenticated, IsAuthenticatedOrReadOnly
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadOnly]
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj) -> bool:
return obj.user == request.user
Combine permissions with a list: permission_classes = [IsAuthenticated, IsOwner].
7. Class-based views (CBV)
Use LoginRequiredMixin and PermissionRequiredMixin for access control. Override get_queryset() to scope results to the requesting user. Keep views thin — delegate business logic to service functions.
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView, CreateView
from django.urls import reverse_lazy
from .models import Order
from .forms import OrderForm
class OrderListView(LoginRequiredMixin, ListView):
model = Order
template_name = 'orders/list.html'
context_object_name = 'orders'
paginate_by = 20
def get_queryset(self):
return Order.objects.filter(user=self.request.user).order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['total'] = self.get_queryset().count()
return context
class OrderCreateView(LoginRequiredMixin, CreateView):
model = Order
form_class = OrderForm
template_name = 'orders/create.html'
success_url = reverse_lazy('orders:list')
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
8. Forms
Use ModelForm for model-backed forms. Validate in clean_<field> or clean() — never in the view.
from django import forms
from .models import Order
class OrderForm(forms.ModelForm):
class Meta:
model = Order
fields = ['product', 'qty', 'notes']
widgets = {
'notes': forms.Textarea(attrs={'rows': 3}),
}
def clean_qty(self) -> int:
qty = self.cleaned_data['qty']
if qty <= 0:
raise forms.ValidationError("Quantity must be positive.")
return qty
def clean(self) -> dict:
cleaned_data = super().clean()
product = cleaned_data.get('product')
qty = cleaned_data.get('qty')
if product and qty and qty > 100:
raise forms.ValidationError("Cannot order more than 100 units of a single product.")
return cleaned_data
9. Signals
Register signal handlers in signals.py and connect them in AppConfig.ready(). Keep handlers thin — for heavy work, dispatch to a background task (Celery, Django Q, etc.).
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order
@receiver(post_save, sender=Order)
def notify_on_status_change(sender, instance: Order, created: bool, **kwargs) -> None:
if not created and instance.tracker.has_changed('status'):
send_status_notification.delay(order_id=instance.pk, new_status=instance.status)
class OrdersConfig(AppConfig):
name = 'apps.orders'
def ready(self) -> None:
import apps.orders.signals
Best practices:
- Never do database writes in
post_save handlers without update_fields guards — it can cause infinite loops.
- Use
transaction.on_commit() to enqueue async tasks only after the DB transaction commits.
- Prefer custom model methods or service functions over signals for logic that must be tested directly.
10. Django admin
Register models with @admin.register and provide useful list_display, list_filter, and search_fields.
from django.contrib import admin
from .models import Order
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'product', 'status', 'qty', 'created_at']
list_filter = ['status', 'created_at']
search_fields = ['user__email', 'product']
readonly_fields = ['created_at', 'updated_at']
ordering = ['-created_at']
11. Checklist before completing development phase