# Django

> Comprehensive Django guide - security, ORM, PostgreSQL, GeoDjango, Django 6.0 features, admin extensions, middleware, authentication, sessions, and ecosystem tools

- Skill: `codeatcode/django` (Agent Skill)
- Install (CLI): `npx skillmds@latest add codeatcode/django`
- Raw SKILL.md: https://api.skillmd.com/api/skills/codeatcode/django/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: codeatcode (https://skillmd.com/u/codeatcode)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/codeatcode/django

---


# Django

Comprehensive guide to Django covering security, ORM, PostgreSQL, GeoDjango, Django 6.0 essentials, admin extensions, and ecosystem tools.

## Overview

Django provides a batteries-included web framework with robust features out of the box:
- **Security** - CSRF protection, authentication, sessions, password hashing, security middleware
- **ORM** - Powerful database abstraction with query optimization
- **PostgreSQL** - Full-text search, array fields, JSONB, range fields
- **GeoDjango** - Geographic database operations with GPS extraction
- **Django 6.0** - Middleware changes, built-in tasks framework, CSP, GeneratedField
- **Admin Extensions** - Operational dashboards and monitoring tools
Django provides robust security features out of the box:
- **CSRF Protection** - Prevents cross-site request forgery
- **Authentication** - User login/logout, password management
- **Sessions** - Secure session management
- **Security Middleware** - Various security headers
- **Password Hashing** - Secure password storage

## Specialized Skills

For deeper coverage of specific domains, see these dedicated skills:

- ↳ **[django-admin](frameworks/django-admin/SKILL.md)** — Admin save_formset/get_search_results/db_index patterns
- ↳ **[django-transaction](frameworks/django-transaction/SKILL.md)** — atomic/select_for_update/on_commit/upserts

---

## CSRF Protection

### How CSRF Works

CSRF (Cross-Site Request Forgery) prevents malicious sites from submitting forms on behalf of authenticated users.

```
User logs in → Django sets session cookie → User visits malicious site
                                                      ↓
                                    Malicious site submits form to your site
                                                      ↓
                                    CSRF token missing → Request rejected
```

### CsrfViewMiddleware

Django's `CsrfViewMiddleware` provides CSRF protection:

```python
# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',  # Must be here
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]
```

> **Important**: `CsrfViewMiddleware` must come AFTER `SessionMiddleware`.

### Using CSRF Token in Forms

```html+django
<!-- Required in every POST form -->
<form method="post">
    {% csrf_token %}
    <input type="text" name="username">
    <input type="password" name="password">
    <button type="submit">Login</button>
</form>
```

```html+django
<!-- AJAX requests -->
<script>
function submitForm() {
    fetch('/submit/', {
        method: 'POST',
        body: new FormData(document.getElementById('myForm')),
        headers: {
            'X-CSRFToken': '{{ csrf_token }}'
        }
    });
}
</script>
```

```javascript
// JavaScript helper
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

// Usage
fetch('/api/', {
    method: 'POST',
    body: JSON.stringify(data),
    headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': getCookie('csrftoken')
    }
});
```

### csrf_protect Decorator

Apply CSRF protection to specific views:

```python
from django.views.decorators.csrf import csrf_protect
from django.middleware.csrf import csrf_exempt

@csrf_protect
def protected_view(request):
    """This view requires CSRF protection."""
    pass

@csrf_exempt
def exempt_view(request):
    """This view is exempt from CSRF (use carefully!)."""
    pass
```

### AJAX with CSRF

```python
# Using Django's CSRF helper in JavaScript
import Cookies from 'js-cookie';

const csrftoken = Cookies.get('csrftoken');

// Fetch API
fetch('/api/', {
    method: 'POST',
    headers: {
        'X-CSRFToken': csrftoken
    },
    body: formData
});

// Axios
axios.defaults.headers.common['X-CSRFToken'] = csrftoken;

// jQuery
$.ajaxSetup({
    headers: {
        'X-CSRFToken': '{{ csrf_token }}'
    }
});
```

### CSRF Exemption (Use Carefully)

```python
# Only exempt when absolutely necessary
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.views import View

@method_decorator(csrf_exempt, name='dispatch')
class WebhookView(View):
    """Webhooks from trusted services."""
    def post(self, request):
        # Process webhook
        return JsonResponse({'status': 'ok'})
```

### Testing CSRF

```python
from django.test import Client, override_settings

@override_settings(CSRFmiddleware=None)  # Disable for testing
def test_view_without_csrf(client):
    """Test without CSRF (not recommended)."""
    response = client.post('/url/', {'data': 'value'})
    assert response.status_code == 200

# Better: Use CSRF client
def test_view_with_csrf(client):
    """Test with proper CSRF token."""
    # Get the form first to obtain CSRF token
    response = client.get('/form-url/')
    csrf_token = client.cookies.get('csrftoken').value
    
    # POST with token
    response = client.post('/form-url/', {
        'field': 'value',
        'csrfmiddlewaretoken': csrf_token
    })
    assert response.status_code == 200
```

---

## Authentication

### Built-in Authentication Views

```python
# urls.py
from django.contrib.auth import views as auth_views
from django.urls import path

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
    path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_done'),
    path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
```

### LoginView Configuration

```python
# views.py
from django.contrib.auth.views import LoginView
from django.contrib.auth.forms import AuthenticationForm

class CustomLoginView(LoginView):
    template_name = 'registration/login.html'
    authentication_form = AuthenticationForm
    redirect_authenticated_user = True
    
    def get_success_url(self):
        return self.request.GET.get('next', '/dashboard/')
```

```python
# settings.py
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/dashboard/'
LOGOUT_REDIRECT_URL = '/'
```

### Manual Authentication

```python
from django.contrib.auth import authenticate, login, logout
from secrets import compare_digest

def login_view(request):
    username = request.POST.get('username')
    password = request.POST.get('password')
    
    # Authenticate user
    user = authenticate(request, username=username, password=password)
    
    if user is not None:
        if user.is_active:
            login(request, user)
            # Redirect to success page
            return redirect('dashboard')
        else:
            return render(request, 'login.html', {
                'error': 'Account disabled'
            })
    else:
        return render(request, 'login.html', {
            'error': 'Invalid credentials'
        })

def logout_view(request):
    logout(request)
    return redirect('home')

### Constant-Time Token Comparison

**CRITICAL**: Use `secrets.compare_digest()` for token/API key comparison - prevents timing attacks:

```python
from secrets import compare_digest
from django.conf import settings

def verify_api_key(requested_key: str) -> bool:
    """Constant-time comparison prevents timing attacks."""
    # NEVER use: requested_key == settings.API_KEY
    # Timing attack: attacker measures response time to guess key char by char
    return compare_digest(requested_key, settings.API_KEY)

# Case-insensitive token comparison
def verify_token(requested_token: str, expected_token: str) -> bool:
    """Case-insensitive constant-time comparison."""
    return compare_digest(
        requested_token.lower().strip(),
        expected_token.lower().strip()
    )
```

### Never Trust POSTed Identity Fields (HTMX)

**CRITICAL**: With HTMX partial submissions, POST data can be manipulated. Always use `request.user`:

```python
# BAD - Trusting POSTed identity
def update_profile(request):
    user_id = request.POST.get('user_id')  # ⚠️ Attacker can change this!
    user = User.objects.get(id=user_id)
    user.name = request.POST.get('name')
    user.save()

# GOOD - Use request.user
@login_required
def update_profile(request):
    # Identity comes from authentication, not POST
    user = request.user  # ✅ Authenticated user
    user.name = request.POST.get('name')  # Only update allowed fields
    user.save()
```

**HTMX-specific vulnerability**: HTMX forms often submit partial data. If your form includes `user_id` or other identity fields in the POST, attackers can manipulate them. The authentication middleware already set `request.user` - use it.
```

### Authentication Form

```python
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm

# Login form
form = AuthenticationForm(request, data=request.POST)

if form.is_valid():
    user = form.get_user()
    login(request, user)

# Registration form
form = UserCreationForm(request.POST)
if form.is_valid():
    user = form.save()
    login(request, user)  # Auto-login after registration
```

### LoginRequiredMixin

```python
from django.contrib.auth.mixins import LoginRequiredMixin

class DashboardView(LoginRequiredMixin, View):
    login_url = '/accounts/login/'
    redirect_field_name = 'next'
    
    def get(self, request):
        return render(request, 'dashboard.html')

# Function-based view
from django.contrib.auth.decorators import login_required

@login_required(login_url='/accounts/login/')
def dashboard(request):
    return render(request, 'dashboard.html')
```

### Custom User Model Authentication

```python
# For custom User models with email instead of username
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model

User = get_user_model()

class EmailBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=username)
        except User.DoesNotExist:
            return None
        
        if user.check_password(password):
            return user
        return None
    
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
```

```python
# settings.py
AUTHENTICATION_BACKENDS = [
    'path.to.EmailBackend',
    'django.contrib.auth.backends.ModelBackend',
]
```

---

## Custom Permission Backends

### Why Custom Backends

Django's built-in `ModelBackend` only handles model-level permissions (`add`, `change`, `delete`, `view`). Custom backends add:
- **Per-object permissions** (row-level authorization)
- **External auth systems** (LDAP, OAuth providers)
- **Permission composition** (multiple backends chained)

### Custom Backend Implementation

```python
# myapp/backends.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User

class ObjectPermissionBackend(BaseBackend):
    """Backend for per-object permissions."""
    
    def has_perm(self, user_obj, perm, obj=None):
        if obj is None:
            # Fall back to model-level check
            return None
        
        app_label, codename = perm.split('.')
        
        # Check object-level permission
        return self._check_object_perm(user_obj, obj, codename)
    
    def _check_object_perm(self, user_obj, obj, action):
        """Check if user can perform action on specific object."""
        if action == 'view':
            return self._can_view(user_obj, obj)
        if action == 'change':
            return self._can_change(user_obj, obj)
        if action == 'delete':
            return self._can_delete(user_obj, obj)
        return False
    
    def _can_view(self, user, obj):
        if hasattr(obj, 'owner'):
            return obj.owner_id == user.id or user.is_staff
        return True
    
    def _can_change(self, user, obj):
        if hasattr(obj, 'owner'):
            return obj.owner_id == user.id
        return user.is_staff
```

### Configuration

Chaining multiple backends:

```python
# settings.py
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',  # Default model-level
    'myapp.backends.ObjectPermissionBackend',      # Custom object-level
]
```

Django tries each backend in order; first `True` or `False` wins. `None` means "I don't know, ask the next backend".

### Per-Object Permissions

The row-level authorization pattern:

```python
from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404

@permission_required('myapp.change_document')
def edit_document(request, pk):
    document = get_object_or_404(Document, pk=pk)
    
    # Check object-level permission
    if not request.user.has_perm('myapp.change_document', document):
        from django.core.exceptions import PermissionDenied
        raise PermissionDenied
    
    # Proceed with edit...
```

### Permission Flow Design

The request → check → grant/deny pattern:

```python
# myapp/permissions.py
class PermissionFlow:
    """Centralized permission checking with audit logging."""
    
    def __init__(self, user):
        self.user = user
    
    def can_access(self, resource, action, obj=None):
        """Check permission and log the decision."""
        allowed = self.user.has_perm(
            f'{resource}.{action}', 
            obj=obj
        )
        
        if not allowed:
            # Log denied access for audit trail
            import logging
            logger = logging.getLogger('permissions')
            logger.warning(
                f"Permission denied: user={self.user.id}, "
                f"resource={resource}, action={action}, obj={obj}"
            )
        
        return allowed
```

### Class-Based View Mixin

Reusable permission checks in CBVs:

```python
from django.core.exceptions import PermissionDenied

class ObjectPermissionMixin:
    """Mixin for per-object permission checks in CBVs."""
    permission_required = None  # e.g., 'myapp.change_document'
    
    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        if self.permission_required:
            if not self.request.user.has_perm(
                self.permission_required, obj
            ):
                raise PermissionDenied
        return obj
```

### Testing Permissions

How to test custom backends:

```python
from django.test import TestCase
from django.contrib.auth.models import User, Permission
from myapp.models import Document

class ObjectPermissionTest(TestCase):
    def setUp(self):
        self.owner = User.objects.create_user('owner', 'o@e.com', 'pass')
        self.other = User.objects.create_user('other', 'x@e.com', 'pass')
        self.doc = Document.objects.create(title='Test', owner=self.owner)
    
    def test_owner_can_change(self):
        self.assertTrue(
            self.owner.has_perm('myapp.change_document', self.doc)
        )
    
    def test_other_cannot_change(self):
        self.assertFalse(
            self.other.has_perm('myapp.change_document', self.doc)
        )
```

### Common Pitfalls

| Issue | Cause | Solution |
|-------|-------|----------|
| `has_perm` returns True for all | Backend returns `True` instead of `None` for unknown perms | Return `None` when backend doesn't handle the permission |
| Object perm not checked | Called `has_perm(perm)` without `obj` arg | Always pass `obj=obj` for object checks |
| Backend not called | Not in `AUTHENTICATION_BACKENDS` | Add backend to settings list |
| Permissions cached incorrectly | Django caches per-user perms | Call `user_obj._perm_cache.clear()` if needed |

### Additional Permission Libraries

Companions to django-guardian:
- **django-rules** (https://github.com/dfunckt/django-rules) - Object-level permissions without database (pre-save hooks)
- **django-role-permissions** (https://github.com/vintasoftware/django-role-permissions) - Role-based access control on top of Django permissions

---

## Sessions

### Session Configuration

```python
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db'  # Default
# Or:
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'  # Faster
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'  # No server storage

SESSION_COOKIE_NAME = 'sessionid'
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7  # 1 week in seconds
SESSION_COOKIE_SECURE = True  # HTTPS only
SESSION_COOKIE_HTTPONLY = True  # No JavaScript access
SESSION_COOKIE_SAMESITE = 'Lax'  # CSRF protection
```

### Using Sessions

```python
# Set session data
request.session['user_id'] = user.id
request.session['preferences'] = {'theme': 'dark', 'lang': 'en'}

# Get session data
user_id = request.session.get('user_id')
preferences = request.session.get('preferences', {})

# Delete session data
del request.session['user_id']
request.session.flush()  # Clear all session data

# Check if key exists
if 'user_id' in request.session:
    pass
```

### Session Middleware

```python
# settings.py - Ensure these are in MIDDLEWARE
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
```

---

## Password Management

### Password Validation

```python
# settings.py
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {'min_length': 8},
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
```

### Custom Password Validation

```python
# validators.py
from django.core.exceptions import ValidationError
import re

class CustomPasswordValidator:
    def __init__(self, min_length=8):
        self.min_length = min_length
    
    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(f'Password must be at least {self.min_length} characters.')
        
        if not re.search(r'[A-Z]', password):
            raise ValidationError('Password must contain at least one uppercase letter.')
        
        if not re.search(r'[!@#$%^&*]', password):
            raise ValidationError('Password must contain at least one special character.')
    
    def help_text(self):
        return f'Password must be at least {self.min_length} characters with uppercase and special characters.'
```

```python
# settings.py
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'myapp.validators.CustomPasswordValidator',
    },
]
```

### Changing Password

```python
from django.contrib.auth import update_session_auth_hash

def change_password(request):
    if request.method == 'POST':
        form = PasswordChangeForm(user=request.user, data=request.POST)
        if form.is_valid():
            user = form.save()
            # Keep user logged in
            update_session_auth_hash(request, user)
            return redirect('password_change_done')
    else:
        form = PasswordChangeForm(user=request.user)
    
    return render(request, 'password_change.html', {'form': form})
```

---

## Security Middleware

```python
# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    # ... other middleware
]

# Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'

# HTTPS settings
SECURE_SSL_REDIRECT = True  # Redirect HTTP to HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# HSTS (HTTP Strict Transport Security)
SECURE_HSTS_SECONDS = 31536000  # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
```

### SecurityMiddleware Options

```python
# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True  # Prevent MIME sniffing
X_FRAME_OPTIONS = 'DENY'  # Prevent clickjacking
SECURE_BROWSER_XSS_FILTER = True  # XSS filter
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'  # Referrer policy

# Custom headers
SECURE_CONTENT_SECURITY_POLICY = "default-src 'self'"
```

---

## Login Templates

```html+django
<!-- registration/login.html -->
{% extends 'base.html' %}

{% block content %}
<div class="login-container">
    <h2>Login</h2>
    
    {% if form.errors %}
    <div class="error">
        <p>Your username and password didn't match. Please try again.</p>
    </div>
    {% endif %}
    
    {% if next %}
        {% if user.is_authenticated %}
        <p>Your account doesn't have access to this page.</p>
        {% else %}
        <p>Please login to see this page.</p>
        {% endif %}
    {% endif %}
    
    <form method="post" action="{% url 'login' %}">
        {% csrf_token %}
        
        <div class="form-group">
            <label for="id_username">Username</label>
            <input type="text" name="username" id="id_username" required>
        </div>
        
        <div class="form-group">
            <label for="id_password">Password</label>
            <input type="password" name="password" id="id_password" required>
        </div>
        
        <button type="submit">Login</button>
        <input type="hidden" name="next" value="{{ next }}">
    </form>
    
    <p><a href="{% url 'password_reset' %}">Forgot password?</a></p>
</div>
{% endblock %}
```

---

## Best Practices

1. **Always use {% csrf_token %}** in POST forms
2. **Use HTTPS** in production (SECURE_SSL_REDIRECT = True)
3. **Enable HSTS** for secure connections
4. **Set secure cookies** (SESSION_COOKIE_SECURE = True)
5. **Use strong password validation**
6. **Use @login_required** for protected views
7. **Never expose sensitive data** in URLs or logs
8. **Validate file uploads** carefully
9. **Use prepared statements** (Django ORM does this automatically)

---

## ORM Optimization

### Indexing Strategy

**db_index on filter/search fields** - Critical for API performance:

```python
class Product(models.Model):
    # Add db_index to frequently filtered fields
    sku = models.CharField(max_length=50, db_index=True, unique=True)
    category = models.ForeignKey(Category, db_index=True)
    status = models.CharField(max_length=20, db_index=True)  # Filter by status
    created_at = models.DateTimeField(db_index=True)  # Date range queries
    
    # Composite index for common query patterns
    class Meta:
        indexes = [
            models.Index(fields=['category', 'status']),
            models.Index(fields=['-created_at']),
        ]
```

> **Admin-specific optimization**: For admin queryset optimization (select_related/prefetch_related patterns, N+1 prevention in list_display), see the [django-admin skill](frameworks/django-admin/SKILL.md).

### Avoiding Duplicate Objects with Exists Subquery

When filtering across relationships (one-to-many or many-to-many), JOINs produce duplicate parent objects:

```python
# Problem: duplicates returned
Author.objects.filter(books__title__startswith="Book")
# [<Author: Charlie>, <Author: Alice>, <Author: Alice>]  # Alice appears twice
```

**Solution: Use Exists Subquery** (fastest, no ordering issues):

```python
from django.db.models import Exists, OuterRef

Author.objects.filter(
    Exists(Book.objects.filter(
        author=OuterRef("id"),
        title__startswith="Book",
    ))
).order_by("name")
```

- Stops evaluation on first match
- No ordering restrictions
- Works with all databases

**PostgreSQL-only alternative:**

```python
Author.objects.filter(books__title__startswith="Book").distinct("id")
```

### N+1 Query Prevention

**Problem:**
```python
for user in User.objects.all()[:100]:
    user.groups.count()  # 100 extra queries!
```

**Solution: Use prefetch_related with Prefetch object:**

```python
from django.db.models import Prefetch

staff_groups = Group.objects.filter(name__in=["admin", "superuser"])
users = User.objects.prefetch_related(
    "groups",
    Prefetch("groups", to_attr="staff_groups", queryset=staff_groups),
).order_by("id")[:100]

for user in users:
    groups_total = user.groups.count()  # Uses cached data
    is_staff = len(user.staff_groups) > 0  # No new query!
```

**Avoid querying prefetched objects unnecessarily:**
```python
# BAD: Makes new query
first_group = user.groups.first()
first_group = user.groups.all()[0]

---

### N+1 Detection Tools

For automated N+1 detection in development:
- **django-debug-toolbar** (https://github.com/django-commons/django-debug-toolbar) - SQL panel shows query count/origin
- **django-zeal** (https://github.com/taobojlen/django-zeal) - N+1 detector with warnings/errors
- **django-silk** (https://github.com/jazzband/django-silk) - Profiling with SQL inspection
- **django-auto-prefetch** (https://github.com/adamchainz/django-auto-prefetch) - Auto prefetch FKs on serializer-like access

### Time-Based Lookups Performance

**Problem:** `timestamp__date` lookup **bypasses indexes**:

```python
# SLOW (30s on 25M rows)
Event.objects.filter(timestamp__date=datetime.date(2026, 1, 5))
# SQL: WHERE timestamp::date='2026-01-05'  # Full table scan!
```

**Solution: Use range boundaries:**

```python
import datetime
start = datetime.datetime(2026, 1, 5, tzinfo=datetime.UTC)
end = start + datetime.timedelta(days=1)

Event.objects.filter(timestamp__gte=start, timestamp__lt=end)
# Uses index, drops to <1s
```

### Deferring Large Fields

```python
# Defer large fields you don't need
books = Book.objects.defer("content", "notes")

# Or explicitly load only needed fields
books = Book.objects.only("title", "pub_date")
```

### Statement Timeouts (PostgreSQL)

```python
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "OPTIONS": {
            "options": "-c statement_timeout=30s",  # Terminate queries >30s
        },
    }
}
```

### Caching Libraries

- **django-cachalot** (https://github.com/noripyt/django-cachalot) - Auto-invalidating cache for ORM queries
- **django-cacheops** (https://github.com/Suor/django-cacheops) - Transaction-aware cache with auto-invalidation

## Django Tasks Framework (Django 6.0+)

Django 6.0 introduced a built-in tasks framework - an abstraction without a production-ready worker.

### Define a Task

```python
from django.tasks import task

@task(priority=2, queue_name="emails", backend="default")
def send_welcome_email(user_id):
    user = User.objects.get(id=user_id)
    send_mail("Welcome!", "Thanks for signing up.", "noreply@example.com", [user.email])
```

**Parameters:**
- `priority` (int): -100 to 100, defaults to 0
- `queue_name` (str): defaults to "default"
- `backend` (str): backend alias
- `takes_context` (bool): whether function accepts TaskContext

### Enqueue the Task

```python
# Synchronous
send_welcome_email.enqueue(user_id=user.id)

# Asynchronous
await send_welcome_email.aenqueue(user_id=user.id)
```

### Built-in Backends (Development Only)

| Backend | Behavior | Use Case |
| ------- |----------|----------|
| `ImmediateBackend` (default) | Runs synchronously | Development |
| `DummyBackend` | Stores without executing | Testing |

### Production: django-tasks-local

```python
# settings.py
INSTALLED_APPS = ["django_tasks_local"]

TASKS = {
    "default": {
        "BACKEND": "django_tasks_local.ThreadPoolBackend",
        "OPTIONS": {"MAX_WORKERS": 10}
    }
}
```

**When to use Django Tasks vs Celery:**

- **Django Tasks**: Fire-and-forget, no infrastructure (emails, webhooks, MVPs)
- **Celery**: Scheduled tasks, retries, persistence, distributed processing

---

## Django Permissions

### Custom Permissions in Model Meta

```python
class Experiment(models.Model):
    name = models.CharField(max_length=100)
    
    class Meta:
        permissions = [
            ("change_experiment_status", "Can change status"),
            ("view_experiment_details", "Can view details"),
        ]
```

### Groups for Role-Based Access

```python
from django.contrib.auth.models import Group

# Create groups
read_only = Group.objects.create(name="Read only")
maintainer = Group.objects.create(name="Maintainer")

# Assign permission to group
maintainer.permissions.add(permission)

# Assign user to group
maintainer.user_set.add(user)
```

### Function-Based View Protection

```python
from django.contrib.auth.decorators import login_required, permission_required

@login_required
def my_view(request):
    ...

@permission_required("blog.view_post")
def restricted_view(request):
    ...
```

### Class-Based View Protection

```python
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import TemplateView

class RestrictedView(LoginRequiredMixin, TemplateView):
    template_name = 'restricted.html'
    raise_exception = True

class PermissionView(PermissionRequiredMixin, TemplateView):
    permission_required = ('posts.can_edit', 'posts.can_view')
    template_name = 'permission_required.html'
```

### Object-Level Permissions with Django Guardian

```python
from guardian.shortcuts import assign_perm, remove_perm

# Assign object-level permission
assign_perm("change_post", user, post)
assign_perm("view_post", group, post)

# Check permission
user.has_perm("change_post", post)
```

### Signal-Based Auto Permission Assignment

```python
from django.db.models.signals import post_save
from django.dispatch import receiver
from guardian.shortcuts import assign_perm

@receiver(post_save, sender=Post)
def set_permission(sender, instance, **kwargs):
    assign_perm("change_post", instance.author, instance)
    assign_perm("view_post", instance.author, instance)
```

---

## Caching

### View Caching

```python
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Cache for 15 minutes
def my_view(request):
    ...
```

### Template Fragment Caching

```{% load cache %}
{% cache 300 my_cache_key %}
    <!-- Expensive content -->
{% endcache %}
```

### Low-Level Cache API

```python
from django.core.cache import cache

cache.set('my_key', 'my_value', timeout=3600)
value = cache.get('my_key')
cache.delete('my_key')

# Multiple keys
cache.set_many({'a': 1, 'b': 2}, timeout=300)
cache.get_many(['a', 'b'])
```

### Redis Cache Backend

```python
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/0',
    }
}
```

---

## Testing Optimization

### HTMX Error Branch Coverage

Test HTMX-specific error paths that regular tests miss:

```python
from django.test import Client

def test_htmx_form_validation_error(client):
    """HTMX requests need different error handling."""
    response = client.post(
        '/partial-form/',
        {'field': 'invalid'},
        HTTP_HX_REQUEST='true',  # HTMX header
    )
    # HTMX returns partial HTML, not redirect
    assert response.status_code == 200
    assert b'error-message' in response.content
    # No full page redirect for HTMX requests


def test_htmx_identity_fields_untrusted(client):
    """Never trust POSTed identity fields with HTMX."""
    # User logged in as user_id=5
    client.force_login(User.objects.get(id=5))
    
    # Malicious HTMX form tries to change user_id
    response = client.post(
        '/update-profile/',
        {'user_id': 999, 'name': 'Hacker'},  # user_id in POST!
        HTTP_HX_REQUEST='true',
    )
    # Should ignore user_id from POST, use request.user
    assert User.objects.get(id=5).name == 'Hacker'
    assert User.objects.get(id=999).name != 'Hacker'
```

### Formset Tests with Real Tuple Shape

Django admin formsets return specific tuple shapes - test with real data:

```python
from django.contrib import admin
from django.test import TestCase
from myapp.models import Parent, Child

class ParentAdminTest(TestCase):
    def setUp(self):
        self.parent = Parent.objects.create(name='Parent')
        self.child1 = Child.objects.create(parent=self.parent, name='Child 1')
        self.child2 = Child.objects.create(parent=self.parent, name='Child 2')
    
    def test_save_formset_tuple_shape(self):
        """save_formset receives [(obj, changed_data)] not bare lists."""
        admin_instance = admin.site._registry[Parent]
        
        # Mock POST with changed child
        data = {
            'child_set-0-id': self.child1.id,
            'child_set-0-name': 'Updated Child 1',  # Changed
            'child_set-1-id': self.child2.id,
            'child_set-1-name': 'Child 2',  # Unchanged
            'child_set-TOTAL_FORMS': 2,
            'child_set-INITIAL_FORMS': 2,
        }
        
        # Track what save_formset receives
        changed_objects = []
        
        def mock_save_formset(parent, formset, **kwargs):
            # Shape: [(instance, {field: old_value}), ...]
            changed_objects.extend(formset.changed_objects)
        
        # Patch and submit
        original_save = admin_instance.save_formset
        admin_instance.save_formset = mock_save_formset
        
        try:
            self.client.post('/admin/myapp/parent/{}/change/'.format(self.parent.id), data)
        finally:
            admin_instance.save_formset = original_save
        
        # Verify shape
        assert len(changed_objects) == 1
        obj, changed_data = changed_objects[0]
        assert obj.id == self.child1.id
        assert 'name' in changed_data
        assert changed_data['name'] == 'Updated Child 1'
```

### Coverage-Audit Cross-Reference

Verify test coverage matches actual code paths:

```bash
# Run coverage and check branches
pytest --cov=myapp --cov-report=html

# Check specific error branches
pytest -k "test_htmx" --cov=myapp.views --cov-report=term-missing

# Cross-reference with TODOs
grep -r "TODO\|FIXME" myapp/ | grep -v test
```

### Fast Password Hashing for Tests

```python
# settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',  # 70% faster
]
```

### Parallel Testing

```bash
python manage.py test --parallel
```

### Capture on_commit Callbacks in Tests

```python
from django.test import TestCase

class ContactTests(TestCase):
    def test_post(self):
        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            response = self.client.post("/contact/", {"message": "Test"})
        
        self.assertEqual(len(callbacks), 1)  # Verify callback was enqueued
```

### In-Memory SQLite for Tests

```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'file::memory:',
    }
}
```

### Assert Query Count

```python
def test_something(self):
    with self.assertNumQueries(5):
        process_data()
```

---

## Migrations

### Post-Rename Dead-Reference Audit Checklist

After renaming fields, audit for dead references:

```bash
# 1. Search for old field name in code
grep -r "old_field_name" --include="*.py" . | grep -v migration | grep -v __pycache__

# 2. Search in templates
grep -r "old_field_name" --include="*.html" .

# 3. Search in admin configurations
grep -r "list_display.*old_field_name" --include="*.py" .

# 4. Search in forms
grep -r "fields.*=.*\['old_field_name'" --include="*.py" .

# 5. Search in serializers
grep -r "old_field_name" --include="*.py" serializers.py

# 6. Check for hardcoded field names in queries
grep -r "filter.*old_field_name" --include="*.py" .
```

**Checklist**:
- [ ] Models.py - field references
- [ ] Admin.py - list_display, list_filter, search_fields
- [ ] Forms.py - field definitions
- [ ] Serializers.py - field serialization
- [ ] Templates.py - template variable references
- [ ] Views.py - query filters, order_by
- [ ] Tests.py - test data assertions
- [ ] API documentation - Swagger/OpenAPI specs
- [ ] External integrations - webhooks, API consumers

### Add Unique Constraints Before Relying on Upserts

Ensure upserts work correctly with unique constraints:

```python
from django.db import migrations, models

class Migration(migrations.Migration):
    
    dependencies = [
        ('myapp', '0001_initial'),
    ]
    
    operations = [
        # 1. Add unique constraint FIRST
        migrations.AddConstraint(
            model_name='externalresource',
            constraint=models.UniqueConstraint(
                fields=['external_id'],
                name='unique_external_id'
            ),
        ),
        
        # 2. Then data migration to dedupe
        migrations.RunPython(
            deduplicate_external_resources,
            reverse_code=migrations.RunPython.noop
        ),
        
        # 3. Now update_or_create will work reliably
        # (no code change needed - just ensure this migration runs first)
    ]

def deduplicate_external_resources(apps, schema_editor):
    ExternalResource = apps.get_model('myapp', 'ExternalResource')
    
    # Group by external_id
    from django.db.models import Count
    duplicates = ExternalResource.objects.values(
        'external_id'
    ).annotate(count=Count('id')).filter(count__gt=1)
    
    for dup in duplicates:
        # Keep oldest, delete rest
        ids = list(ExternalResource.objects.filter(
            external_id=dup['external_id']
        ).order_by('-created_at').values_list('id', flat=True)[1:])
        
        ExternalResource.objects.filter(id__in=ids).delete()
```

### Squashing Migrations

```bash
# Squash migrations 0002 to 0006
python manage.py squashmigrations app 0002 0006
```

Then update dependencies in other migrations:
```python
class Migration(migrations.Migration):
    dependencies = [
        ('app', '0007_squashed_0006'),  # Update to squashed migration
    ]
```

### Standalone Django ORM (inspectdb)

Query existing databases without a full project:

```python
# settings.py
import os
from django.conf import settings

settings.configure(
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite"}},
    INSTALLED_APPS=["myapp"],
)

# Generate models
# python manage.py inspectdb > models.py
```

**Critical Model Attribute:**
```python
class Place(models.Model):
    url = models.URLField()
    title = models.CharField(null=True)
    
    class Meta:
        managed = False  # Don't try to create/migrate
        db_table = "moz_places"  # Existing table name
```

---

---

## Django Signals Best Practices

### Defining and Using Signals

```python
# Define custom signals
from django.dispatch import Signal
user_logged_in = Signal(providing_args=['user', 'request'])

# Connect receivers with decorator
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_in

@receiver(user_logged_in)
def log_user_login(sender, user, request, **kwargs):
    ActivityLog.objects.create(
        user=user,
        event_type=ActivityLog.LOGIN,
        context={'ip':

…(truncated)
