# Django Conventions

> Comprehensive Django best practices covering project structure, models (field choices, Meta options, managers, QuerySets, migrations), views (CBVs vs FBVs, generic views), Django REST Framework (serializers, ViewSets, permissions), forms, templates, security (CSRF, XSS, SQL injection), performance (N+1 queries, select_related, prefetch_related, caching), testing, and common anti-patterns. Essential reference for Django code reviews and development.

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

---


# Django Conventions and Best Practices

## Purpose

This skill provides comprehensive Django best practices and conventions to ensure high-quality, secure, and performant Django applications. It serves as a reference guide during code reviews to verify adherence to Django standards and community best practices.

**When to use this skill:**
- Conducting code reviews of Django projects
- Designing Django applications and models
- Writing Django views, serializers, and forms
- Evaluating Django security and performance
- Refactoring Django codebases
- Teaching Django best practices to team members

This skill is designed to be referenced by the `uncle-duke-python` agent during Django code reviews.

## Context

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. This skill documents industry-standard Django practices that emphasize:

- **Convention over Configuration**: Follow Django's conventions for predictability
- **Don't Repeat Yourself (DRY)**: Minimize code duplication
- **Explicit is better than implicit**: Clear, readable code
- **Security by default**: Leverage Django's built-in security features
- **Database efficiency**: Optimize queries and avoid common performance pitfalls
- **Maintainability**: Write code that's easy to understand and modify

## Prerequisites

**Required Knowledge:**
- Python fundamentals and best practices
- Understanding of web development concepts (HTTP, REST, MVC/MTV)
- Basic understanding of Django's MTV (Model-Template-View) architecture
- SQL and database concepts

**Required Tools:**
- Django 3.2+ (LTS recommended)
- Python 3.8+
- Database (PostgreSQL recommended for production)

**Expected Project Structure:**
```
myproject/
├── manage.py
├── myproject/              # Project configuration
│   ├── __init__.py
│   ├── settings/           # Split settings by environment
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── development.py
│   │   ├── production.py
│   │   └── test.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── apps/                   # Django apps
│   ├── users/
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── managers.py
│   │   ├── tests/
│   │   │   ├── test_models.py
│   │   │   ├── test_views.py
│   │   │   └── test_serializers.py
│   │   └── migrations/
│   └── core/
├── static/
├── media/
├── templates/
├── requirements/
│   ├── base.txt
│   ├── development.txt
│   ├── production.txt
│   └── test.txt
└── README.md
```

---

## Instructions

### Task 1: Django Project Structure Best Practices

#### 1.1 Project Layout

**Rule:** Organize Django projects with clear separation between project configuration and apps.

✅ **Good Project Structure:**
```
myproject/
├── manage.py
├── myproject/              # Project settings and configuration
│   ├── settings/
│   │   ├── base.py        # Shared settings
│   │   ├── development.py # Dev-specific settings
│   │   ├── production.py  # Production settings
│   │   └── test.py        # Test settings
│   ├── urls.py            # Root URL configuration
│   ├── wsgi.py
│   └── asgi.py
├── apps/                   # All Django apps
│   ├── users/
│   ├── blog/
│   └── core/              # Shared utilities
├── static/                # Static files
├── media/                 # User-uploaded files
├── templates/             # Shared templates
├── requirements/          # Split requirements
└── docs/                  # Documentation
```

❌ **Bad:**
```
myproject/
├── manage.py
├── settings.py            # All settings in one file
├── users.py               # Apps not properly organized
├── blog.py
└── utils.py               # Mixed concerns
```

**Why:** Clear structure improves maintainability, makes settings management easier, and follows Django community standards.

#### 1.2 App Organization

**Rule:** Each app should be focused on a single domain concept.

✅ **Good App Structure:**
```
users/
├── __init__.py
├── models.py              # User-related models
├── views.py               # User views
├── serializers.py         # DRF serializers
├── urls.py                # App-specific URLs
├── admin.py               # Admin configuration
├── apps.py                # App configuration
├── managers.py            # Custom model managers
├── forms.py               # Forms
├── signals.py             # Signal handlers
├── permissions.py         # Custom permissions
├── utils.py               # App-specific utilities
├── tests/
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_views.py
│   └── factories.py       # Test factories
└── migrations/
```

**App Naming Conventions:**
- Use plural nouns for apps containing models (users, posts, comments)
- Use singular nouns for utility apps (core, common, utils)
- Keep app names short and descriptive
- Use underscores for multi-word names (user_profiles)

#### 1.3 Settings Organization

**Rule:** Split settings by environment for security and flexibility.

✅ **Good Settings Structure:**

**`settings/base.py`:**
```python
"""Base settings shared across all environments."""
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent

# SECURITY WARNING: keep the secret key used in production secret!
# This should be overridden in environment-specific settings
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'dev-only-secret-key')

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Third-party apps
    'rest_framework',
    'django_filters',
    # Local apps
    'apps.users',
    'apps.blog',
    'apps.core',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myproject.urls'

# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True  # Always use timezone-aware datetimes

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
```

**`settings/development.py`:**
```python
"""Development-specific settings."""
from .base import *

DEBUG = True

ALLOWED_HOSTS = ['localhost', '127.0.0.1']

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myproject_dev',
        'USER': 'myproject_user',
        'PASSWORD': 'dev_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# Development-specific apps
INSTALLED_APPS += [
    'debug_toolbar',
    'django_extensions',
]

MIDDLEWARE += [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
]

# Django Debug Toolbar
INTERNAL_IPS = ['127.0.0.1']

# Email backend for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
```

**`settings/production.py`:**
```python
"""Production settings."""
import os
from .base import *

DEBUG = False

# SECURITY WARNING: Update this to your domain
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

# Use environment variables for sensitive data
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOST'],
        'PORT': os.environ.get('DB_PORT', '5432'),
        'CONN_MAX_AGE': 600,  # Connection pooling
    }
}

# Security settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': '/var/log/myproject/django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}
```

#### 1.4 URL Configuration Patterns

**Rule:** Use RESTful URL patterns and include() for app-specific URLs.

✅ **Good:**

**`myproject/urls.py`:**
```python
"""Root URL configuration."""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/users/', include('apps.users.urls')),
    path('api/v1/blog/', include('apps.blog.urls')),
    path('api/v1/', include('apps.core.urls')),
]

# Serve media files in development
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
```

**`apps/users/urls.py`:**
```python
"""User app URL configuration."""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

app_name = 'users'  # URL namespace

router = DefaultRouter()
router.register(r'', views.UserViewSet, basename='user')

urlpatterns = [
    path('', include(router.urls)),
    path('me/', views.CurrentUserView.as_view(), name='current-user'),
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),
]
```

**URL Naming Best Practices:**
- Use lowercase with hyphens: `/api/user-profiles/`
- Version your APIs: `/api/v1/`, `/api/v2/`
- Use plural nouns for resources: `/users/`, `/posts/`
- Use nested routes sparingly: `/users/123/posts/` (consider `/posts/?user=123` instead)
- Always name your URLs for reverse lookup

---

### Task 2: Model Best Practices

#### 2.1 Model Design Patterns

**Rule:** Models should be focused, well-documented, and follow Django conventions.

✅ **Good Model Design:**
```python
"""User models."""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator


class User(AbstractUser):
    """Custom user model extending Django's AbstractUser.

    Adds additional fields for user profiles and implements
    business logic related to user accounts.
    """

    class UserRole(models.TextChoices):
        """User role choices."""
        ADMIN = 'ADMIN', _('Administrator')
        MODERATOR = 'MOD', _('Moderator')
        USER = 'USER', _('Regular User')

    # Additional fields
    email = models.EmailField(_('email address'), unique=True)
    role = models.CharField(
        _('role'),
        max_length=10,
        choices=UserRole.choices,
        default=UserRole.USER,
    )
    bio = models.TextField(_('biography'), blank=True, max_length=500)
    birth_date = models.DateField(_('birth date'), null=True, blank=True)
    avatar = models.ImageField(
        _('avatar'),
        upload_to='avatars/%Y/%m/%d/',
        null=True,
        blank=True,
    )
    email_verified = models.BooleanField(_('email verified'), default=False)
    created_at = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('updated at'), auto_now=True)

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['email']),
            models.Index(fields=['-created_at']),
        ]

    def __str__(self):
        """String representation of user."""
        return f"{self.username} ({self.get_role_display()})"

    def get_full_name(self):
        """Return user's full name or username if not set."""
        full_name = super().get_full_name()
        return full_name if full_name else self.username

    @property
    def is_admin(self):
        """Check if user has admin role."""
        return self.role == self.UserRole.ADMIN

    def verify_email(self):
        """Mark user's email as verified."""
        self.email_verified = True
        self.save(update_fields=['email_verified', 'updated_at'])


class Post(models.Model):
    """Blog post model."""

    class PostStatus(models.TextChoices):
        """Post status choices."""
        DRAFT = 'DRAFT', _('Draft')
        PUBLISHED = 'PUBLISHED', _('Published')
        ARCHIVED = 'ARCHIVED', _('Archived')

    title = models.CharField(_('title'), max_length=200)
    slug = models.SlugField(_('slug'), max_length=200, unique=True)
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='posts',
        related_query_name='post',
        verbose_name=_('author'),
    )
    content = models.TextField(_('content'))
    status = models.CharField(
        _('status'),
        max_length=20,
        choices=PostStatus.choices,
        default=PostStatus.DRAFT,
        db_index=True,
    )
    featured = models.BooleanField(_('featured'), default=False)
    view_count = models.PositiveIntegerField(_('view count'), default=0)
    published_at = models.DateTimeField(_('published at'), null=True, blank=True)
    created_at = models.DateTimeField(_('created at'), auto_now_add=True)
    updated_at = models.DateTimeField(_('updated at'), auto_now=True)

    # Use custom manager
    objects = PostManager()

    class Meta:
        verbose_name = _('post')
        verbose_name_plural = _('posts')
        ordering = ['-published_at', '-created_at']
        indexes = [
            models.Index(fields=['status', '-published_at']),
            models.Index(fields=['author', '-created_at']),
        ]
        constraints = [
            models.CheckConstraint(
                check=models.Q(view_count__gte=0),
                name='post_view_count_non_negative',
            ),
        ]

    def __str__(self):
        """String representation of post."""
        return self.title

    def save(self, *args, **kwargs):
        """Override save to set published_at when status changes to published."""
        if self.status == self.PostStatus.PUBLISHED and not self.published_at:
            from django.utils import timezone
            self.published_at = timezone.now()
        super().save(*args, **kwargs)

    def increment_view_count(self):
        """Increment post view count efficiently."""
        self.__class__.objects.filter(pk=self.pk).update(
            view_count=models.F('view_count') + 1
        )
        # Refresh from database
        self.refresh_from_db(fields=['view_count'])
```

**Key Model Design Principles:**
1. Use verbose field names with gettext_lazy for i18n
2. Add `help_text` for complex fields
3. Use TextChoices/IntegerChoices for choice fields
4. Include timestamps (created_at, updated_at) on most models
5. Use appropriate `on_delete` for ForeignKey
6. Set `related_name` and `related_query_name` on relationships
7. Add database indexes for frequently queried fields
8. Use constraints for data integrity
9. Override `__str__()` for meaningful representations
10. Document the model and complex methods

#### 2.2 Field Choices and Naming

**Rule:** Use TextChoices/IntegerChoices for field choices, follow naming conventions.

✅ **Good:**
```python
class Order(models.Model):
    """Customer order model."""

    class OrderStatus(models.TextChoices):
        """Order status choices using TextChoices."""
        PENDING = 'PENDING', _('Pending Payment')
        PAID = 'PAID', _('Paid')
        PROCESSING = 'PROCESSING', _('Processing')
        SHIPPED = 'SHIPPED', _('Shipped')
        DELIVERED = 'DELIVERED', _('Delivered')
        CANCELLED = 'CANCELLED', _('Cancelled')
        REFUNDED = 'REFUNDED', _('Refunded')

    class PaymentMethod(models.TextChoices):
        """Payment method choices."""
        CREDIT_CARD = 'CC', _('Credit Card')
        DEBIT_CARD = 'DC', _('Debit Card')
        PAYPAL = 'PP', _('PayPal')
        BANK_TRANSFER = 'BT', _('Bank Transfer')

    # Field naming follows snake_case
    order_number = models.CharField(max_length=50, unique=True)
    customer = models.ForeignKey(User, on_delete=models.PROTECT)
    status = models.CharField(
        max_length=20,
        choices=OrderStatus.choices,
        default=OrderStatus.PENDING,
    )
    payment_method = models.CharField(
        max_length=2,
        choices=PaymentMethod.choices,
        null=True,
        blank=True,
    )
    total_amount = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        validators=[MinValueValidator(0)],
    )
    shipping_address = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"Order {self.order_number} - {self.get_status_display()}"
```

❌ **Bad:**
```python
class Order(models.Model):
    """Bad example - avoid this."""

    # Bad: Tuple choices instead of TextChoices
    STATUS_CHOICES = (
        (1, 'Pending'),
        (2, 'Paid'),
        (3, 'Shipped'),
    )

    # Bad: Using integers without clear meaning
    status = models.IntegerField(choices=STATUS_CHOICES)

    # Bad: camelCase instead of snake_case
    orderNumber = models.CharField(max_length=50)

    # Bad: Vague field names
    amt = models.DecimalField(max_digits=10, decimal_places=2)
    addr = models.TextField()
```

**Field Naming Conventions:**
- Use snake_case for field names
- Be explicit and descriptive (avoid abbreviations)
- Use `_id` suffix sparingly (Django adds it automatically to ForeignKey)
- Use boolean field names that read like questions: `is_active`, `has_paid`, `email_verified`
- Use date/time field names with `_at` or `_date` suffix: `created_at`, `birth_date`

#### 2.3 Meta Class Options

**Rule:** Use Meta class to configure model behavior and database options.

✅ **Good Meta Class:**
```python
class Article(models.Model):
    """Article model with comprehensive Meta configuration."""

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
    published_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        # Verbose names for admin
        verbose_name = _('article')
        verbose_name_plural = _('articles')

        # Default ordering
        ordering = ['-published_at', '-created_at']

        # Get latest by
        get_latest_by = 'published_at'

        # Database table name (optional, Django auto-generates)
        db_table = 'blog_articles'

        # Indexes for query optimization
        indexes = [
            models.Index(fields=['slug']),
            models.Index(fields=['author', '-published_at']),
            models.Index(fields=['category', '-published_at']),
            models.Index(fields=['-published_at'], name='recent_articles_idx'),
        ]

        # Unique together constraints
        constraints = [
            models.UniqueConstraint(
                fields=['author', 'slug'],
                name='unique_author_slug',
            ),
            models.CheckConstraint(
                check=models.Q(published_at__isnull=True) | models.Q(published_at__gte=models.F('created_at')),
                name='published_after_created',
            ),
        ]

        # Permissions
        permissions = [
            ('can_publish', 'Can publish articles'),
            ('can_feature', 'Can feature articles'),
        ]
```

**Common Meta Options:**
- `verbose_name` / `verbose_name_plural`: Admin display names
- `ordering`: Default query ordering
- `indexes`: Database indexes for performance
- `constraints`: UniqueConstraint, CheckConstraint for data integrity
- `permissions`: Custom permissions
- `db_table`: Custom table name (use sparingly)
- `get_latest_by`: Field to use for latest()
- `abstract`: For abstract base models
- `managed`: Whether Django manages database lifecycle

#### 2.4 Managers and QuerySets

**Rule:** Use custom managers for reusable query logic and QuerySets for chainable queries.

✅ **Good Custom Manager and QuerySet:**

**`apps/blog/managers.py`:**
```python
"""Custom managers and querysets for blog app."""
from django.db import models
from django.utils import timezone


class PostQuerySet(models.QuerySet):
    """Custom QuerySet for Post model with reusable query methods."""

    def published(self):
        """Return only published posts."""
        return self.filter(
            status=self.model.PostStatus.PUBLISHED,
            published_at__lte=timezone.now(),
        )

    def drafts(self):
        """Return draft posts."""
        return self.filter(status=self.model.PostStatus.DRAFT)

    def by_author(self, author):
        """Return posts by specific author."""
        return self.filter(author=author)

    def featured(self):
        """Return featured posts."""
        return self.filter(featured=True)

    def recent(self, days=30):
        """Return posts from last N days."""
        cutoff_date = timezone.now() - timezone.timedelta(days=days)
        return self.filter(published_at__gte=cutoff_date)

    def with_author_info(self):
        """Optimize query by selecting related author."""
        return self.select_related('author')

    def with_comments_count(self):
        """Annotate with comment count."""
        return self.annotate(
            comments_count=models.Count('comments', distinct=True)
        )

    def popular(self, min_views=100):
        """Return popular posts above view threshold."""
        return self.filter(view_count__gte=min_views).order_by('-view_count')


class PostManager(models.Manager):
    """Custom manager for Post model."""

    def get_queryset(self):
        """Return custom QuerySet."""
        return PostQuerySet(self.model, using=self._db)

    # Proxy QuerySet methods for convenience
    def published(self):
        """Return published posts."""
        return self.get_queryset().published()

    def drafts(self):
        """Return draft posts."""
        return self.get_queryset().drafts()

    def by_author(self, author):
        """Return posts by author."""
        return self.get_queryset().by_author(author)

    def featured(self):
        """Return featured posts."""
        return self.get_queryset().featured()

    def recent(self, days=30):
        """Return recent posts."""
        return self.get_queryset().recent(days)


class PublishedPostManager(models.Manager):
    """Manager that returns only published posts by default."""

    def get_queryset(self):
        """Return only published posts."""
        return super().get_queryset().filter(
            status='PUBLISHED',
            published_at__lte=timezone.now(),
        )
```

**Usage in models.py:**
```python
from .managers import PostManager, PublishedPostManager

class Post(models.Model):
    # ... fields ...

    # Default manager
    objects = PostManager()

    # Additional manager for published posts only
    published = PublishedPostManager()

    class Meta:
        base_manager_name = 'objects'  # Use for related queries
```

**Usage in views:**
```python
# Chainable QuerySet methods
recent_featured_posts = Post.objects.published().featured().recent(days=7)

# Multiple optimizations
popular_posts = (
    Post.objects
    .published()
    .with_author_info()
    .with_comments_count()
    .popular(min_views=500)
)

# Using alternative manager
all_published = Post.published.all()
```

**Manager Best Practices:**
1. Put query logic in QuerySets for chainability
2. Create manager methods that return QuerySets
3. Use descriptive method names
4. Document what each method does
5. Don't put business logic in managers (use models or services)
6. Use `select_related()` and `prefetch_related()` in manager methods

#### 2.5 Model Methods vs Signals

**Rule:** Use model methods for object-specific logic, signals for cross-cutting concerns.

✅ **Good - Use Model Methods:**
```python
class Order(models.Model):
    """Order model with business logic in methods."""

    total_amount = models.DecimalField(max_digits=10, decimal_places=2)
    discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    def calculate_total(self):
        """Calculate order total including tax and discount."""
        subtotal = self.total_amount - self.discount_amount
        return subtotal + self.tax_amount

    def apply_discount(self, discount_code):
        """Apply discount code to order."""
        from .services import DiscountService

        discount = DiscountService.validate_and_get_discount(discount_code, self)
        self.discount_amount = discount.amount
        self.save(update_fields=['discount_amount'])
        return discount

    def mark_as_paid(self):
        """Mark order as paid and trigger fulfillment."""
        self.status = self.OrderStatus.PAID
        self.paid_at = timezone.now()
        self.save(update_fields=['status', 'paid_at'])

        # Trigger fulfillment signal
        from .signals import order_paid
        order_paid.send(sender=self.__class__, order=self)
```

✅ **Good - Use Signals for Cross-Cutting Concerns:**

**`apps/orders/signals.py`:**
```python
"""Order-related signals."""
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver, Signal
from .models import Order

# Custom signal
order_paid = Signal()  # Provides 'order' argument

@receiver(post_save, sender=Order)
def send_order_confirmation_email(sender, instance, created, **kwargs):
    """Send confirmation email when order is created."""
    if created:
        from .tasks import send_order_confirmation_email_task
        send_order_confirmation_email_task.delay(instance.id)

@receiver(order_paid)
def start_order_fulfillment(sender, order, **kwargs):
    """Start fulfillment process when order is paid."""
    from .tasks import start_fulfillment_task
    start_fulfillment_task.delay(order.id)

@receiver(pre_delete, sender=Order)
def log_order_deletion(sender, instance, **kwargs):
    """Log when order is deleted for audit trail."""
    import logging
    logger = logging.getLogger(__name__)
    logger.warning(
        f"Order {instance.order_number} deleted by system",
        extra={'order_id': instance.id}
    )
```

**Connect signals in apps.py:**
```python
from django.apps import AppConfig

class OrdersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.orders'

    def ready(self):
        """Import signals when app is ready."""
        import apps.orders.signals  # noqa
```

**When to Use Each:**

**Model Methods:**
- Object-specific business logic
- Calculations based on model data
- State transitions
- Data validation
- Simple related object queries

**Signals:**
- Send notifications (email, SMS, webhooks)
- Update caches
- Create audit logs
- Trigger background tasks
- Cross-app communication
- Update denormalized data

❌ **Bad - Business Logic in Signals:**
```python
@receiver(post_save, sender=Order)
def update_order_total(sender, instance, **kwargs):
    """DON'T DO THIS - business logic should be in model method."""
    instance.total = instance.calculate_subtotal() + instance.tax
    instance.save()  # Causes infinite loop!
```

#### 2.6 Related Names and related_query_name

**Rule:** Always set explicit related_name for reverse relationships.

✅ **Good:**
```python
class User(models.Model):
    username = models.CharField(max_length=150)

class Post(models.Model):
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='posts',           # user.posts.all()
        related_query_name='post',      # User.objects.filter(post__title='...')
    )
    title = models.CharField(max_length=200)

class Comment(models.Model):
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE,
        related_name='comments',
        related_query_name='comment',
    )
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='comments',
        related_query_name='comment',
    )
    content = models.TextField()

# Usage:
user = User.objects.get(username='john')
user_posts = user.posts.all()  # Uses related_name
user_comments = user.comments.all()

# Query filtering uses related_query_name
users_with_python_posts = User.objects.filter(post__title__contains='Python')
posts_with_comments = Post.objects.filter(comment__isnull=False)
```

❌ **Bad:**
```python
class Post(models.Model):
    # Bad: No related_name, Django generates 'post_set'
    author = models.ForeignKey(User, on_delete=models.CASCADE)

# Unclear usage:
user_posts = user.post_set.all()  # What is post_set?
```

**Related Name Best Practices:**
- Use plural for one-to-many: `related_name='posts'`
- Use singular for one-to-one: `related_name='profile'`
- Use `related_query_name` for clear query filtering
- Use `related_name='+'` to disable reverse relation if not needed
- Avoid name conflicts across apps using `app_label`

#### 2.7 Database Indexing Strategies

**Rule:** Add indexes for fields frequently used in queries, filtering, and ordering.

✅ **Good Indexing:**
```python
class Article(models.Model):
    """Article model with strategic indexing."""

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)  # Unique creates index automatically
    author = models.ForeignKey(User, on_delete=models.CASCADE)  # FK creates index
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    status = models.CharField(max_length=20, choices=StatusChoices.choices)
    featured = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)
    view_count = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # Index for filtering by status and ordering
            models.Index(fields=['status', '-published_at'], name='status_pub_idx'),

            # Index for author's articles
            models.Index(fields=['author', '-created_at'], name='author_articles_idx'),

            # Index for category browsing
            models.Index(fields=['category', '-published_at'], name='cat_pub_idx'),

            # Index for featured articles
            models.Index(
                fields=['-published_at'],
                condition=models.Q(featured=True),
                name='featured_idx',  # Partial index (PostgreSQL)
            ),

            # Compound index for complex queries
            models.Index(
                fields=['status', 'featured', '-view_count'],
                name='popular_published_idx',
            ),
        ]

        # Text search index (PostgreSQL)
        # For full-text search capabilities
        # Note: Requires GinIndex from django.contrib.postgres.indexes
```

**When to Add Indexes:**
- Foreign keys (automatic in Django)
- Fields in `WHERE` clauses
- Fields in `ORDER BY` clauses
- Fields in `JOIN` conditions
- Unique fields (automatic)
- Fields used in `filter()`, `exclude()`, `get()`

**When NOT to Add Indexes:**
- Small tables (< 10,000 rows)
- Fields that change frequently
- Fields with low cardinality (few unique values like boolean)
- Too many indexes slow down writes

**Index Analysis:**
```python
# Check if query uses indexes
from django.db import connection
from django.test.utils import CaptureQueriesContext

with CaptureQueriesContext(connection) as queries:
    articles = Article.objects.filter(
        status='PUBLISHED',
        featured=True
    ).order_by('-view_count')[:10]
    list(articles)  # Force evaluation

for query in queries:
    print(query['sql'])
    # Check EXPLAIN output in database
```

#### 2.8 Migration Best Practices

**Rule:** Create focused, reviewable migrations and handle data migrations separately.

✅ **Good Migration Practices:**

**1. Small, Focused Migrations:**
```bash
# Create separate migrations for different changes
python manage.py makemigrations --name add_email_verified_field
python manage.py makemigrations --name add_user_role_choices
```

**2. Data Migration Example:**
```python
# Generated migration file: 0003_populate_user_roles.py
from django.db import migrations

def populate_user_roles(apps, schema_editor):
    """Populate user roles based on is_staff and is_superuser."""
    User = apps.get_model('users', 'User')

    # Update in batches for large datasets
    User.objects.filter(is_superuser=True).update(role='ADMIN')
    User.objects.filter(is_staff=True, is_superuser=False).update(role='MOD')
    User.objects.filter(is_staff=False, is_superuser=False).update(role='USER')

def reverse_populate_user_roles(apps, schema_editor):
    """Reverse operation."""
    User = apps.get_model('users', 'User')
    User.objects.all().update(role='USER')

class Migration(migrations.Migration):
    dependencies = [
        ('users', '0002_user_role'),
    ]

    operations = [
        migrations.RunPython(
            populate_user_roles,
            reverse_code=reverse_populate_user_roles,
        ),
    ]
```

**3. Safe Schema Changes:**
```python
# Safe: Adding nullable field
class Migration(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='user',
            name='phone_number',
            field=models.CharField(max_length=20, null=True, blank=True),
        ),
    ]

# Safe: Adding field with default
class Migration(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='post',
            name='view_count',
            field=models.PositiveIntegerField(default=0),
        ),
    ]
```

**4. Multi-Step Migrations for Removing Fields:**
```python
# Step 1: Make field nullable (deploy)
field=models.CharField(max_length=100, null=True, blank=True)

# Step 2: Remove from code, create migration (deploy)
# Step 3: Drop column (deploy)
```

**Migration Best Practices:**
1. Review generated migrations before committing
2. Use descriptive migration names (`--name`)
3. Never edit applied migrations in production
4. Use `RunPython` for data migrations with reverse operations
5. Test migrations on production-like data
6. Use `--check` in CI to detect missing migrations
7. Squash old migrations when they pile up
8. Handle large datasets with batching in data migrations
9. Add indexes in separate migrations (can be slow)
10. Use database transactions (default in Django)

---

### Task 3: Views and URLs Best Practices

#### 3.1 Class-Based Views (CBVs) vs Function-Based Views (FBVs)

**Rule:** Use CBVs for CRUD operations, FBVs for simple or unique logic.

✅ **Good - Use CBVs for Standard CRUD:**
```python
"""Blog views using class-based views."""
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm


class PostListView(ListView):
    """List all published posts."""

    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 20

    def get_queryset(self):
        """Return only published posts with author info."""
        return Post.objects.published().with_author_info()

    def get_context_data(self, **kwargs):
        """Add additional context data."""
        context = super().get_context_data(**kwargs)
        context['featured_posts'] = Post.objects.featured()[:5]
        return context


class PostDetailView(DetailView):
    """Display single post."""

    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

    def get_object(self, queryset=None):
        """Get post and increment view count."""
        post = super().get_object(queryset)
        post.increment_view_count()
        return post


class PostCreateView(LoginRequiredMixin, CreateView):
    """Create new post."""

    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    success_url = reverse_lazy('blog:post-list')

    def form_valid(self, form):
        """Set author to current user."""
        form.instance.author = self.request.user
        return super().form_valid(form)


class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    """Update existing post."""

    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'

    def test_func(self):
        """Check if user is author or admin."""
        post = self.get_object()
        return self.request.user == post.author or self.request.user.is_staff

    def get_success_url(self):
        """Redirect to post detail."""
        return self.object.get_absolute_url()


class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    """Delete post."""

    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('blog:post-list')

    def test_func(self):
        """Check if user is author or admin."""
        post = self.get_object()
        return self.request.user == post.author or self.request.user.is_staff
```

✅ **Good - Use FBVs for Unique Logic:**
```python
"""Blog views using function-based views for custom logic."""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import Post


@login_required
@require_POST
def toggle_post_featured(request, pk):
    """Toggle post featured status (unique logic, FBV appropriate)."""
    post = get_object_or_404(Post, pk=pk)

    # Check permissions
    if not request.user.is_staff:
        return JsonResponse({'error': 'Permission denied'}, status=403)

    # Toggle featured status
    post.featured = not post.featured
    post.save(update_fields=['featured'])

    return JsonResponse({
        'success': True,
        'featured': post.featured,
    })


def search_posts(request):
    """Search posts (custom search logic, FBV appropriate)."""
    query = request.GET.get('q', '')
    category = request.GET.get('category', '')

    posts = Post.objects.published()

    if query:
        posts = posts.filter(
            models.Q(title__icontains=query) |
            models.Q(content__icontains=query)
        )

    if category:
        posts = posts.filter(category__slug=category)

    context = {
        'posts': posts,
        'query': query,
        'category': category,
    }
    return render(request, 'blog/search_results.html', context)
```

**When to Use Each:**

**Use CBVs when:**
- Standard CRUD operations
- Need inheritance and mixins
- Working with forms
- Need consistent structure across views

**Use FBVs when:**
- Simple logic
- Unique business logic that doesn't fit CBV pattern
- API endpoints with custom logic
- Complex multi-step flows
- Cleare

…(truncated)
