| name | django-conventions |
| description | 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. |
| allowed-tools | Read, Write, Grep, Glob |
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:
"""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:
"""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:
"""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:
"""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:
"""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=123instead) - 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:
"""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:
- Use verbose field names with gettext_lazy for i18n
- Add
help_textfor complex fields - Use TextChoices/IntegerChoices for choice fields
- Include timestamps (created_at, updated_at) on most models
- Use appropriate
on_deletefor ForeignKey - Set
related_nameandrelated_query_nameon relationships - Add database indexes for frequently queried fields
- Use constraints for data integrity
- Override
__str__()for meaningful representations - Document the model and complex methods
2.2 Field Choices and Naming
Rule: Use TextChoices/IntegerChoices for field choices, follow naming conventions.
✅ Good:
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:
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
_idsuffix 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
_ator_datesuffix:created_at,birth_date
2.3 Meta Class Options
Rule: Use Meta class to configure model behavior and database options.
✅ Good Meta Class:
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 namesordering: Default query orderingindexes: Database indexes for performanceconstraints: UniqueConstraint, CheckConstraint for data integritypermissions: Custom permissionsdb_table: Custom table name (use sparingly)get_latest_by: Field to use for latest()abstract: For abstract base modelsmanaged: 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:
"""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:
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:
# 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:
- Put query logic in QuerySets for chainability
- Create manager methods that return QuerySets
- Use descriptive method names
- Document what each method does
- Don't put business logic in managers (use models or services)
- Use
select_related()andprefetch_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:
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:
"""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:
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:
@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:
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:
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_namefor 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:
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
WHEREclauses - Fields in
ORDER BYclauses - Fields in
JOINconditions - 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:
# 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:
# 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:
# 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:
# 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:
# 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:
- Review generated migrations before committing
- Use descriptive migration names (
--name) - Never edit applied migrations in production
- Use
RunPythonfor data migrations with reverse operations - Test migrations on production-like data
- Use
--checkin CI to detect missing migrations - Squash old migrations when they pile up
- Handle large datasets with batching in data migrations
- Add indexes in separate migrations (can be slow)
- 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:
"""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:
"""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
- Clearer and more readable as function
3.2 Generic Views
Rule: Use Django's generic views for common patterns.
✅ Good - Using Generic Views:
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView,
TemplateView, RedirectView, FormView
)
class HomePageView(TemplateView):
"""Homepage using TemplateView."""
template_name = 'home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['recent_posts'] = Post.objects.published().recent()[:5]
context['featured_posts'] = Post.objects.featured()[:3]
return context
class ContactFormView(FormView):
"""Contact form using FormView."""
template_name = 'contact.html'
form_class = ContactForm
success_url = reverse_lazy('contact-success')
def form_valid(self, form):
"""Send email when form is valid."""
form.send_email()
return super().form_valid(form)
class RedirectToLatestPostView(RedirectView):
"""Redirect to latest published post."""
permanent = False
def get_redirect_url(self, *args, **kwargs):
latest_post = Post.objects.published().latest('published_at')
return latest_post.get_absolute_url()
Common Generic Views:
TemplateView: Render a templateListView: List objectsDetailView: Display single objectCreateView: Create object with formUpdateView: Update object with formDeleteView: Delete objectFormView: Display and process formRedirectView: Redirect to URL
3.3 View Permissions and Mixins
Rule: Use mixins for reusable view behavior and permissions.
✅ Good - Using Mixins:
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
UserPassesTestMixin,
)
from django.views.generic import ListView, UpdateView
class AuthorRequiredMixin(UserPassesTestMixin):
"""Mixin to require user to be object author."""
def test_func(self):
"""Check if user is object author."""
obj = self.get_object()
return obj.author == self.request.user
class AdminOrAuthorMixin(UserPassesTestMixin):
"""Mixin to require user to be admin or author."""
def test_func(self):
"""Check if user is admin or author."""
obj = self.get_object()
return (
self.request.user.is_staff or
obj.author == self.request.user
)
class MyPostsView(LoginRequiredMixin, ListView):
"""List current user's posts (requires login)."""
model = Post
template_name = 'blog/my_posts.html'
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
class PostPublishView(PermissionRequiredMixin, UpdateView):
"""Publish post (requires permission)."""
model = Post
fields = ['status']
permission_required = 'blog.can_publish'
def form_valid(self, form):
form.instance.status = 'PUBLISHED'
return super().form_valid(form)
class PostEditView(AdminOrAuthorMixin, UpdateView):
"""Edit post (requires author or admin)."""
model = Post
form_class = PostForm
template_name = 'blog/post_edit.html'
Common Mixins:
LoginRequiredMixin: Require authenticationPermissionRequiredMixin: Require specific permissionUserPassesTestMixin: Custom test functionUserOwnerMixin: Custom mixin for object ownership
Mixin Order Matters:
# Correct order: Left to right
class MyView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# Mixins process left to right
pass
# Wrong: View class should be last
class MyView(UpdateView, LoginRequiredMixin): # Don't do this
pass
3.4 Handling Forms in Views
Rule: Use Django forms for validation, use form_valid() for custom processing.
✅ Good Form Handling:
from django.views.generic.edit import CreateView, UpdateView
from .forms import PostForm, CommentForm
class PostCreateView(LoginRequiredMixin, CreateView):
"""Create post with form."""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_form_kwargs(self):
"""Pass user to form."""
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
"""Process valid form."""
form.instance.author = self.request.user
response = super().form_valid(form)
# Send notification
from .tasks import send_post_created_notification
send_post_created_notification.delay(self.object.id)
messages.success(self.request, 'Post created successfully!')
return response
def form_invalid(self, form):
"""Handle invalid form."""
messages.error(self.request, 'Please correct the errors below.')
return super().form_invalid(form)
def get_success_url(self):
"""Redirect to post detail."""
return self.object.get_absolute_url()
class CommentCreateView(LoginRequiredMixin, CreateView):
"""Create comment with AJAX support."""
model = Comment
form_class = CommentForm
template_name = 'blog/comment_form.html'
def form_valid(self, form):
"""Process valid comment form."""
form.instance.author = self.request.user
form.instance.post_id = self.kwargs['post_pk']
response = super().form_valid(form)
# Return JSON for AJAX requests
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'comment_id': self.object.id,
'author': self.object.author.username,
'content': self.object.content,
})
return response
Task 4: Django REST Framework (DRF) Best Practices
4.1 Serializer Patterns
Rule: Use ModelSerializer for models, add validation and custom fields as needed.
✅ Good Serializer Design:
apps/blog/serializers.py:
"""Blog app serializers."""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Post, Comment, Category
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
"""User serializer with limited fields."""
full_name = serializers.CharField(source='get_full_name', read_only=True)
post_count = serializers.IntegerField(source='posts.count', read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'email', 'full_name', 'post_count', 'date_joined']
read_only_fields = ['date_joined']
class CategorySerializer(serializers.ModelSerializer):
"""Category serializer."""
post_count = serializers.IntegerField(source='posts.count', read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'post_count']
read_only_fields = ['slug']
class CommentSerializer(serializers.ModelSerializer):
"""Comment serializer with nested author."""
author = UserSerializer(read_only=True)
author_id = serializers.IntegerField(write_only=True, required=False)
class Meta:
model = Comment
fields = [
'id', 'post', 'author', 'author_id', 'content',
'created_at', 'updated_at'
]
read_only_fields = ['created_at', 'updated_at']
def validate_content(self, value):
"""Validate comment content."""
if len(value) < 10:
raise serializers.ValidationError(
"Comment must be at least 10 characters long."
)
return value
class PostListSerializer(serializers.ModelSerializer):
"""Post serializer for list view (minimal fields)."""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comment_count = serializers.IntegerField(read_only=True)
excerpt = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'author', 'category',
'excerpt', 'status', 'featured', 'view_count',
'comment_count', 'published_at', 'created_at'
]
def get_excerpt(self, obj):
"""Return first 200 characters of content."""
return obj.content[:200] + '...' if len(obj.content) > 200 else obj.content
class PostDetailSerializer(serializers.ModelSerializer):
"""Post serializer for detail view (full fields)."""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
comment_count = serializers.IntegerField(source='comments.count', read_only=True)
# Write-only fields
author_id = serializers.IntegerField(write_only=True, required=False)
category_id = serializers.IntegerField(write_only=True, required=False)
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'author', 'author_id',
'category', 'category_id', 'content', 'status',
'featured', 'view_count', 'comments', 'comment_count',
'published_at', 'created_at', 'updated_at'
]
read_only_fields = ['slug', 'view_count', 'created_at', 'updated_at']
def validate(self, attrs):
"""Validate post data."""
# Set author from request if not provided
if 'author_id' not in attrs:
attrs['author_id'] = self.context['request'].user.id
# Validate published posts have category
if attrs.get('status') == 'PUBLISHED' and not attrs.get('category_id'):
raise serializers.ValidationError({
'category': 'Published posts must have a category.'
})
return attrs
def create(self, validated_data):
"""Create post and set published_at if published."""
if validated_data.get('status') == 'PUBLISHED':
from django.utils import timezone
validated_data['published_at'] = timezone.now()
return super().create(validated_data)
class PostWriteSerializer(serializers.ModelSerializer):
"""Post serializer for create/update (separate from read)."""
class Meta:
model = Post
fields = [
'title', 'slug', 'category', 'content',
'status', 'featured'
]
def validate_slug(self, value):
"""Validate slug uniqueness."""
# Exclude current instance in update
queryset = Post.objects.filter(slug=value)
if self.instance:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise serializers.ValidationError("Post with this slug already exists.")
return value
Serializer Best Practices:
- Use separate serializers for list/detail/write when fields differ significantly
- Use
SerializerMethodFieldfor computed fields - Use
sourceparameter to reference model methods/properties - Set
read_only=Truefor computed fields - Use
write_only=Truefor password/sensitive input fields - Validate in
validate_<field>()for single field,validate()for multi-field - Keep business logic in models/services, not serializers
- Use nested serializers for related objects in read, IDs in write
4.2 ViewSets vs APIView
Rule: Use ViewSets for standard CRUD APIs, APIView for custom endpoints.
✅ Good - Using ViewSets:
"""Blog API views using ViewSets."""
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Comment
from .serializers import (
PostListSerializer, PostDetailSerializer,
PostWriteSerializer, CommentSerializer
)
from .permissions import IsAuthorOrReadOnly
class PostViewSet(viewsets.ModelViewSet):
"""ViewSet for Post model with CRUD operations."""
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'category', 'featured']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'published_at', 'view_count']
ordering = ['-published_at']
def get_queryset(self):
"""Return appropriate queryset based on action."""
queryset = Post.objects.all()
if self.action == 'list':
# Optimize list query
queryset = queryset.select_related('author', 'category')
queryset = queryset.annotate(
comment_count=models.Count('comments')
)
# Filter by published status for non-staff users
if not self.request.user.is_staff:
queryset = queryset.filter(status='PUBLISHED')
elif self.action == 'retrieve':
# Optimize detail query
queryset = queryset.select_related('author', 'category')
queryset = queryset.prefetch_related('comments__author')
return queryset
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'list':
return PostListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return PostWriteSerializer
return PostDetailSerializer
def perform_create(self, serializer):
"""Set author when creating post."""
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Custom action to publish a post."""
post = self.get_object()
if post.status == 'PUBLISHED':
return Response(
{'detail': 'Post is already published.'},
status=status.HTTP_400_BAD_REQUEST
)
post.status = 'PUBLISHED'
from django.utils import timezone
post.published_at = timezone.now()
post.save()
serializer = self.get_serializer(post)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def comments(self, request, pk=None):
"""Get comments for a post."""
post = self.get_object()
comments = post.comments.select_related('author')
page = self.paginate_queryset(comments)
if page is not None:
serializer = CommentSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def featured(self, request):
"""Get featured posts."""
queryset = self.get_queryset().filter(featured=True)[:10]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class CommentViewSet(viewsets.ModelViewSet):
"""ViewSet for Comment model."""
queryset = Comment.objects.select_related('author', 'post')
serializer_class = CommentSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
def perform_create(self, serializer):
"""Set author when creating comment."""
serializer.save(author=self.request.user)
✅ Good - Using APIView for Custom Logic:
"""Custom API views using APIView."""
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.db.models import Count, Avg
from .models import Post
class PostStatisticsAPIView(APIView):
"""Custom endpoint for post statistics."""
def get(self, request):
"""Return post statistics."""
stats = Post.objects.aggregate(
total_posts=Count('id'),
total_views=models.Sum('view_count'),
avg_views=Avg('view_count'),
published_posts=Count('id', filter=models.Q(status='PUBLISHED')),
)
return Response(stats)
class BulkPublishAPIView(APIView):
"""Bulk publish posts."""
permission_classes = [IsAdminUser]
def post(self, request):
"""Publish multiple posts."""
post_ids = request.data.get('post_ids', [])
if not post_ids:
return Response(
{'detail': 'No post IDs provided.'},
status=status.HTTP_400_BAD_REQUEST
)
from django.utils import timezone
updated_count = Post.objects.filter(
id__in=post_ids,
status='DRAFT'
).update(
status='PUBLISHED',
published_at=timezone.now()
)
return Response({
'detail': f'{updated_count} posts published.',
'count': updated_count
})
When to Use Each:
Use ViewSets when:
- Standard CRUD operations
- Working with a single model
- Need router URL generation
- Want consistent API structure
Use APIView when:
- Custom business logic
- Multiple models in one endpoint
- Non-CRUD operations
- Complex request/response handling
4.3 Permission Classes
Rule: Use DRF permission classes for access control.
✅ Good Custom Permissions:
apps/blog/permissions.py:
"""Custom permission classes for blog app."""
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""Permission to only allow authors to edit their objects."""
def has_object_permission(self, request, view, obj):
"""Check object-level permission."""
# Read permissions for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for author
return obj.author == request.user
class IsAdminOrAuthor(permissions.BasePermission):
"""Permission for admin or author."""
def has_object_permission(self, request, view, obj):
"""Check if user is admin or author."""
return request.user.is_staff or obj.author == request.user
class CanPublishPost(permissions.BasePermission):
"""Permission to publish posts."""
message = "You don't have permission to publish posts."
def has_permission(self, request, view):
"""Check if user has publish permission."""
return request.user.has_perm('blog.can_publish')
class IsEmailVerified(permissions.BasePermission):
"""Permission requiring verified email."""
message = "Email must be verified to perform this action."
def has_permission(self, request, view):
"""Check if user's email is verified."""
return request.user.is_authenticated and request.user.email_verified
Using Permissions in Views:
class PostViewSet(viewsets.ModelViewSet):
"""Post ViewSet with multiple permission classes."""
def get_permissions(self):
"""Return appropriate permissions based on action."""
if self.action in ['create', 'update', 'partial_update', 'destroy']:
permission_classes = [IsAuthenticated, IsEmailVerified, IsAuthorOrReadOnly]
elif self.action == 'publish':
permission_classes = [IsAuthenticated, CanPublishPost]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
Common Permission Classes:
AllowAny: Allow all (default)IsAuthenticated: Require authenticationIsAdminUser: Require staff statusIsAuthenticatedOrReadOnly: Read for all, write for authenticatedDjangoModelPermissions: Use Django model permissionsDjangoObjectPermissions: Object-level permissions
4.4 Authentication Patterns
Rule: Use token-based authentication for APIs, sessions for web.
✅ Good Authentication Setup:
settings/base.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
JWT Authentication (using djangorestframework-simplejwt):
# settings/base.py
from datetime import timedelta
INSTALLED_APPS += [
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
}
Authentication Views:
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
4.5 Pagination Best Practices
Rule: Always paginate list endpoints, provide pagination controls.
✅ Good Pagination:
apps/core/pagination.py:
"""Custom pagination classes."""
from rest_framework.pagination import PageNumberPagination, CursorPagination
from rest_framework.response import Response
class StandardResultsSetPagination(PageNumberPagination):
"""Standard pagination with page numbers."""
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
"""Custom paginated response."""
return Response({
'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'page_size': self.page_size,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
'results': data,
})
class LargeResultsSetPagination(PageNumberPagination):
"""Pagination for large datasets."""
page_size = 100
page_size_query_param = 'page_size'
max_page_size = 1000
class PostCursorPagination(CursorPagination):
"""Cursor pagination for posts (better for infinite scroll)."""
page_size = 20
ordering = '-created_at'
cursor_query_param = 'cursor'
Using in ViewSets:
class PostViewSet(viewsets.ModelViewSet):
"""Post ViewSet with pagination."""
pagination_class = StandardResultsSetPagination
# Override for specific actions
def get_paginated_response(self, data):
"""Add custom metadata to paginated response."""
response = super().get_paginated_response(data)
response.data['meta'] = {
'total_featured': Post.objects.filter(featured=True).count(),
}
return response
4.6 Filtering and Searching
Rule: Use django-filter for complex filtering, DRF filters for search/ordering.
✅ Good Filtering Setup:
apps/blog/filters.py:
"""Custom filters for blog app."""
from django_filters import rest_framework as filters
from .models import Post
class PostFilter(filters.FilterSet):
"""Custom filter for Post model."""
title = filters.CharFilter(lookup_expr='icontains')
author_username = filters.CharFilter(field_name='author__username', lookup_expr='iexact')
category = filters.CharFilter(field_name='category__slug')
published_after = filters.DateTimeFilter(field_name='published_at', lookup_expr='gte')
published_before = filters.DateTimeFilter(field_name='published_at', lookup_expr='lte')
min_views = filters.NumberFilter(field_name='view_count', lookup_expr='gte')
featured = filters.BooleanFilter()
status = filters.ChoiceFilter(choices=Post.PostStatus.choices)
class Meta:
model = Post
fields = ['title', 'author_username', 'category', 'featured', 'status']
class PostViewSet(viewsets.ModelViewSet):
"""Post ViewSet with filtering."""
queryset = Post.objects.all()
serializer_class = PostSerializer
filterset_class = PostFilter
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
search_fields = ['title', 'content', 'author__username']
ordering_fields = ['created_at', 'published_at', 'view_count', 'title']
ordering = ['-published_at']
Usage:
# Filter examples:
GET /api/posts/?category=django&featured=true
GET /api/posts/?published_after=2024-01-01&min_views=100
GET /api/posts/?search=django&ordering=-view_count
GET /api/posts/?author_username=john
4.7 API Versioning
Rule: Version your API from the start, use URL path versioning.
✅ Good API Versioning:
settings/base.py:
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
'VERSION_PARAM': 'version',
}
urls.py:
from django.urls import path, include
urlpatterns = [
path('api/v1/', include('apps.api.v1.urls')),
path('api/v2/', include('apps.api.v2.urls')),
]
Version-specific serializers:
# apps/api/v1/serializers.py
class PostSerializerV1(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author']
# apps/api/v2/serializers.py
class PostSerializerV2(serializers.ModelSerializer):
# V2 adds new fields
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'slug', 'category']
Task 5: Forms and ModelForms Best Practices
5.1 Form Field Validation
Rule: Validate fields with clean_
✅ Good Form Validation:
apps/users/forms.py:
"""User forms with validation."""
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import ValidationError
import re
User = get_user_model()
class UserRegistrationForm(UserCreationForm):
"""User registration form with custom validation."""
email = forms.EmailField(
required=True,
help_text='Enter a valid email address.',
)
agree_to_terms = forms.BooleanField(
required=True,
label='I agree to the terms and conditions',
)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2', 'agree_to_terms']
def clean_username(self):
"""Validate username."""
username = self.cleaned_data.get('username')
# Check length
if len(username) < 3:
raise ValidationError('Username must be at least 3 characters.')
# Check format (alphanumeric and underscores only)
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValidationError(
'Username can only contain letters, numbers, and underscores.'
)
# Check uniqueness
if User.objects.filter(username__iexact=username).exists():
raise ValidationError('This username is already taken.')
return username.lower()
def clean_email(self):
"""Validate email."""
email = self.cleaned_data.get('email')
# Check uniqueness
if User.objects.filter(email__iexact=email).exists():
raise ValidationError('This email is already registered.')
# Check domain (example: block certain domains)
domain = email.split('@')[1]
blocked_domains = ['tempmail.com', 'throwaway.email']
if domain in blocked_domains:
raise ValidationError('Email from this domain is not allowed.')
return email.lower()
def clean(self):
"""Cross-field validation."""
cleaned_data = super().clean()
password1 = cleaned_data.get('password1')
password2 = cleaned_data.get('password2')
username = cleaned_data.get('username')
# Check password doesn't contain username
if password1 and username and username.lower() in password1.lower():
raise ValidationError('Password cannot contain your username.')
return cleaned_data
def save(self, commit=True):
"""Save user with additional processing."""
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
# Send verification email
from .tasks import send_verification_email
send_verification_email.delay(user.id)
return user
class PostForm(forms.ModelForm):
"""Post form with validation."""
class Meta:
model = Post
fields = ['title', 'slug', 'category', 'content', 'status', 'featured']
widgets = {
'content': forms.Textarea(attrs={'rows': 10}),
'slug': forms.TextInput(attrs={'placeholder': 'auto-generated-from-title'}),
}
def __init__(self, *args, **kwargs):
"""Initialize form with custom behavior."""
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# Make slug optional on create (auto-generate from title)
if not self.instance.pk:
self.fields['slug'].required = False
# Limit category choices based on user
if self.user and not self.user.is_staff:
self.fields['category'].queryset = Category.objects.filter(public=True)
def clean_slug(self):
"""Validate and auto-generate slug."""
slug = self.cleaned_data.get('slug')
title = self.cleaned_data.get('title')
# Auto-generate slug if not provided
if not slug and title:
from django.utils.text import slugify
slug = slugify(title)
# Check uniqueness (excluding current instance)
queryset = Post.objects.filter(slug=slug)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise ValidationError('Post with this slug already exists.')
return slug
def clean_content(self):
"""Validate content."""
content = self.cleaned_data.get('content')
# Minimum length
if len(content) < 100:
raise ValidationError('Post content must be at least 100 characters.')
return content
def clean(self):
"""Cross-field validation."""
cleaned_data = super().clean()
status = cleaned_data.get('status')
category = cleaned_data.get('category')
# Published posts must have category
if status == 'PUBLISHED' and not category:
raise ValidationError({
'category': 'Published posts must have a category.'
})
return cleaned_data
5.2 ModelForm Usage
Rule: Use ModelForm for database-backed forms, Form for non-model forms.
✅ Good ModelForm:
class CommentForm(forms.ModelForm):
"""ModelForm for Comment model."""
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'rows': 4,
'placeholder': 'Write your comment here...',
'class': 'form-control',
}),
}
labels = {
'content': 'Your Comment',
}
help_texts = {
'content': 'Be respectful and constructive.',
}
def __init__(self, *args, **kwargs):
"""Initialize form."""
self.post = kwargs.pop('post', None)
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def save(self, commit=True):
"""Save comment with post and user."""
comment = super().save(commit=False)
if self.post:
comment.post = self.post
if self.user:
comment.author = self.user
if commit:
comment.save()
return comment
✅ Good Form (non-model):
class ContactForm(forms.Form):
"""Contact form not backed by model."""
name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={'placeholder': 'Your Name'}),
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={'placeholder': 'your@email.com'}),
)
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={'placeholder': 'Subject'}),
)
message = forms.CharField(
widget=forms.Textarea(attrs={'rows': 6, 'placeholder': 'Your message...'}),
)
def send_email(self):
"""Send contact email."""
from django.core.mail import send_mail
send_mail(
subject=f"Contact Form: {self.cleaned_data['subject']}",
message=self.cleaned_data['message'],
from_email=self.cleaned_data['email'],
recipient_list=['contact@example.com'],
)
5.3 Formsets
Rule: Use formsets for handling multiple forms.
✅ Good Formset Usage:
from django.forms import modelformset_factory, inlineformset_factory
# ModelFormSet for editing multiple objects
PostFormSet = modelformset_factory(
Post,
fields=['title', 'status', 'featured'],
extra=0, # No extra empty forms
can_delete=True,
)
# InlineFormSet for related objects
CommentFormSet = inlineformset_factory(
Post,
Comment,
fields=['content'],
extra=1,
can_delete=True,
)
# In view:
def edit_post_with_comments(request, pk):
"""Edit post and its comments."""
post = get_object_or_404(Post, pk=pk)
if request.method == 'POST':
post_form = PostForm(request.POST, instance=post)
comment_formset = CommentFormSet(request.POST, instance=post)
if post_form.is_valid() and comment_formset.is_valid():
post_form.save()
comment_formset.save()
return redirect('post-detail', pk=post.pk)
else:
post_form = PostForm(instance=post)
comment_formset = CommentFormSet(instance=post)
return render(request, 'post_edit.html', {
'form': post_form,
'formset': comment_formset,
})
Task 6: Security Best Practices
6.1 CSRF Protection
Rule: Always use CSRF protection, never disable it in production.
✅ Good CSRF Usage:
<!-- In templates -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit</button>
</form>
# In AJAX requests
from django.middleware.csrf import get_token
def my_view(request):
"""View that provides CSRF token for AJAX."""
return JsonResponse({
'csrfToken': get_token(request),
})
// In JavaScript
fetch('/api/endpoint/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify(data),
});
❌ Bad - Never Do This:
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt # DON'T DO THIS in production
def my_view(request):
pass
6.2 SQL Injection Prevention
Rule: Always use Django ORM or parameterized queries, never string concatenation.
✅ Good - Safe from SQL Injection:
# Using ORM (automatically escaped)
users = User.objects.filter(username=user_input)
# Using ORM Q objects
from django.db.models import Q
users = User.objects.filter(
Q(username__icontains=search_term) |
Q(email__icontains=search_term)
)
# If you must use raw SQL, use parameterization
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM users WHERE username = %s",
[user_input] # Parameterized - SAFE
)
❌ Bad - SQL Injection Vulnerability:
# DON'T DO THIS - vulnerable to SQL injection
query = f"SELECT * FROM users WHERE username = '{user_input}'"
cursor.execute(query)
# DON'T DO THIS
User.objects.raw(f"SELECT * FROM users WHERE username = '{user_input}'")
6.3 XSS Prevention
Rule: Use Django's auto-escaping, mark safe only when necessary.
✅ Good - XSS Protected:
<!-- Django auto-escapes by default -->
<p>{{ user_comment }}</p> <!-- Automatically escaped -->
<!-- For trusted HTML -->
<div>{{ trusted_html|safe }}</div>
<!-- In Python code -->
from django.utils.html import escape, format_html
# Escape user input
safe_text = escape(user_input)
# Use format_html for building HTML
html = format_html(
'<a href="{}">Link</a>',
user_url # Automatically escaped
)
❌ Bad - XSS Vulnerability:
<!-- DON'T DO THIS -->
<p>{{ user_comment|safe }}</p> <!-- Marks untrusted input as safe -->
<!-- In Python -->
# DON'T DO THIS
html = f'<p>{user_input}</p>' # Not escaped
6.4 SECRET_KEY and Sensitive Settings
Rule: Never hardcode secrets, use environment variables.
✅ Good:
# settings/production.py
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DATABASE_PASSWORD = os.environ['DB_PASSWORD']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_KEY']
# Using python-decouple
from decouple import config
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
❌ Bad:
# DON'T DO THIS
SECRET_KEY = 'my-secret-key-123' # Hardcoded secret
DATABASE_PASSWORD = 'password123' # In source code
6.5 DEBUG Settings
Rule: Never set DEBUG=True in production.
✅ Good:
# settings/production.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# Custom error pages
ADMINS = [('Admin', 'admin@yourdomain.com')]
MANAGERS = ADMINS
6.6 Security Middleware
Rule: Enable all security middleware in production.
✅ Good Production Security:
# settings/production.py
# Security middleware
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',
]
# HTTPS settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# HSTS settings
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Other security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
Task 7: Performance Optimization
7.1 N+1 Query Prevention
Rule: Use select_related() and prefetch_related() to avoid N+1 queries.
✅ Good - Optimized Queries:
# N+1 Problem Example (BAD):
posts = Post.objects.all()
for post in posts:
print(post.author.username) # Triggers query for each post!
# Solution 1: select_related for ForeignKey/OneToOne
posts = Post.objects.select_related('author', 'category').all()
for post in posts:
print(post.author.username) # No additional queries!
# Solution 2: prefetch_related for ManyToMany/Reverse FK
posts = Post.objects.prefetch_related('comments').all()
for post in posts:
print(post.comments.count()) # No additional queries!
# Complex prefetch with filtering
from django.db.models import Prefetch
posts = Post.objects.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects.select_related('author').filter(approved=True),
to_attr='approved_comments'
)
).all()
for post in posts:
for comment in post.approved_comments:
print(comment.author.username) # All in 3 queries total!
When to Use Each:
select_related(): For ForeignKey and OneToOneField (SQL JOIN)prefetch_related(): For ManyToManyField and reverse ForeignKey (separate query + Python join)
7.2 Query Optimization with only() and defer()
Rule: Use only() to fetch specific fields, defer() to exclude fields.
✅ Good:
# Only fetch needed fields
users = User.objects.only('id', 'username', 'email')
# Defer large fields
posts = Post.objects.defer('content') # Don't load content field
# Combine with select_related
posts = (
Post.objects
.select_related('author')
.only('id', 'title', 'author__username', 'created_at')
)
7.3 Database Query Analysis
Rule: Monitor and analyze queries, use django-debug-toolbar.
✅ Good - Query Analysis:
# In development, use django-debug-toolbar
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
# Log queries in development
LOGGING = {
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
# Check query count in tests
from django.test.utils import override_settings
from django.db import connection
from django.test.utils import CaptureQueriesContext
with CaptureQueriesContext(connection) as queries:
# Your code here
list(Post.objects.all())
print(f"Number of queries: {len(queries)}")
for query in queries:
print(query['sql'])
7.4 Caching Strategies
Rule: Cache expensive queries and computations.
✅ Good Caching:
# Cache decorator for views
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # Cache for 15 minutes
def post_list(request):
posts = Post.objects.published().with_author_info()
return render(request, 'posts.html', {'posts': posts})
# Low-level cache API
from django.core.cache import cache
def get_popular_posts():
"""Get popular posts with caching."""
cache_key = 'popular_posts'
posts = cache.get(cache_key)
if posts is None:
posts = list(Post.objects.popular()[:10])
cache.set(cache_key, posts, 60 * 60) # Cache for 1 hour
return posts
# Cache expensive computations
from django.utils.functional import cached_property
class Post(models.Model):
# ... fields ...
@cached_property
def word_count(self):
"""Calculate word count (cached on instance)."""
return len(self.content.split())
# Template fragment caching
{% load cache %}
{% cache 500 sidebar request.user.username %}
<!-- Expensive sidebar generation -->
{% endcache %}
Cache Settings:
# Redis cache (recommended for production)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'myproject',
'TIMEOUT': 300, # 5 minutes default
}
}
# Memcached cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
7.5 Database Connection Pooling
Rule: Use connection pooling in production for better performance.
✅ Good:
# settings/production.py
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 (10 minutes)
'OPTIONS': {
'connect_timeout': 10,
},
}
}
Task 8: Testing Django Applications
Rule: Write comprehensive tests for models, views, and APIs.
✅ Good Django Tests:
apps/blog/tests/test_models.py:
"""Tests for blog models."""
from django.test import TestCase
from django.contrib.auth import get_user_model
from apps.blog.models import Post, Category
User = get_user_model()
class PostModelTest(TestCase):
"""Tests for Post model."""
def setUp(self):
"""Set up test data."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123',
)
self.category = Category.objects.create(
name='Django',
slug='django',
)
def test_create_post(self):
"""Test creating a post."""
post = Post.objects.create(
title='Test Post',
slug='test-post',
author=self.user,
category=self.category,
content='Test content',
)
self.assertEqual(post.title, 'Test Post')
self.assertEqual(post.author, self.user)
self.assertEqual(str(post), 'Test Post')
def test_published_posts_manager(self):
"""Test published posts manager."""
# Create published post
published = Post.objects.create(
title='Published Post',
slug='published-post',
author=self.user,
content='Content',
status='PUBLISHED',
)
# Create draft post
draft = Post.objects.create(
title='Draft Post',
slug='draft-post',
author=self.user,
content='Content',
status='DRAFT',
)
# Test manager
published_posts = Post.objects.published()
self.assertIn(published, published_posts)
self.assertNotIn(draft, published_posts)
apps/blog/tests/test_views.py:
"""Tests for blog views."""
from django.test import TestCase, Client
from django.urls import reverse
from apps.blog.models import Post
class PostListViewTest(TestCase):
"""Tests for post list view."""
def setUp(self):
"""Set up test client and data."""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123',
)
def test_post_list_view(self):
"""Test post list view returns 200."""
response = self.client.get(reverse('blog:post-list'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_list.html')
def test_post_create_requires_login(self):
"""Test that creating post requires login."""
response = self.client.get(reverse('blog:post-create'))
self.assertEqual(response.status_code, 302) # Redirect to login
self.assertIn('/login/', response.url)
apps/blog/tests/test_api.py:
"""Tests for blog API."""
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
class PostAPITest(APITestCase):
"""Tests for Post API endpoints."""
def setUp(self):
"""Set up test data."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123',
)
def test_list_posts(self):
"""Test listing posts."""
url = reverse('api:post-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_post_requires_authentication(self):
"""Test that creating post requires authentication."""
url = reverse('api:post-list')
data = {'title': 'Test Post', 'content': 'Test content'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_create_post_authenticated(self):
"""Test creating post when authenticated."""
self.client.force_authenticate(user=self.user)
url = reverse('api:post-list')
data = {
'title': 'Test Post',
'content': 'Test content' * 20, # Meet minimum length
'status': 'DRAFT',
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 1)
self.assertEqual(Post.objects.first().author, self.user)
Task 9: Common Django Anti-Patterns
9.1 Anti-Pattern: Using filter().first() instead of get()
❌ Bad:
# Don't do this - less clear intent
user = User.objects.filter(id=user_id).first()
if user is None:
# Handle not found
pass
✅ Good:
# Use get() and handle DoesNotExist
from django.core.exceptions import ObjectDoesNotExist
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
# Handle not found
pass
# Or use get_object_or_404 in views
from django.shortcuts.get_object_or_404
user = get_object_or_404(User, id=user_id)
9.2 Anti-Pattern: Not Using select_related/prefetch_related
❌ Bad - N+1 Queries:
# This generates N+1 queries (1 for posts + 1 per post for author)
posts = Post.objects.all()
for post in posts:
print(f"{post.title} by {post.author.username}")
✅ Good:
# This generates 1 query (JOIN)
posts = Post.objects.select_related('author').all()
for post in posts:
print(f"{post.title} by {post.author.username}")
9.3 Anti-Pattern: Using Raw SQL When ORM Would Work
❌ Bad:
# Don't use raw SQL for simple queries
from django.db import connection
cursor = connection.cursor()
cursor.execute("SELECT * FROM users WHERE username = %s", [username])
user = cursor.fetchone()
✅ Good:
# Use the ORM
user = User.objects.get(username=username)
When Raw SQL IS Appropriate:
- Complex queries that ORM can't express efficiently
- Database-specific features (window functions, CTEs)
- Performance-critical queries where you need full control
9.4 Anti-Pattern: Not Using Transactions
❌ Bad:
# Multiple saves without transaction
def transfer_money(from_account, to_account, amount):
from_account.balance -= amount
from_account.save()
to_account.balance += amount
to_account.save() # If this fails, first save succeeded!
✅ Good:
from django.db import transaction
@transaction.atomic
def transfer_money(from_account, to_account, amount):
"""Transfer money atomically."""
from_account.balance -= amount
from_account.save()
to_account.balance += amount
to_account.save()
# Both saves succeed or both fail
# Or use context manager
def transfer_money(from_account, to_account, amount):
"""Transfer money atomically."""
with transaction.atomic():
from_account.balance -= amount
from_account.save()
to_account.balance += amount
to_account.save()
9.5 Anti-Pattern: Storing Secrets in Code
❌ Bad:
SECRET_KEY = 'django-insecure-hard-coded-key'
AWS_SECRET_KEY = 'AKIAIOSFODNN7EXAMPLE'
DATABASE_PASSWORD = 'mypassword123'
✅ Good:
import os
from decouple import config
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
AWS_SECRET_KEY = config('AWS_SECRET_KEY')
DATABASE_PASSWORD = config('DB_PASSWORD')
9.6 Anti-Pattern: Not Using Migrations
❌ Bad:
# Manually creating database tables
# Editing database schema directly
# Not committing migration files
✅ Good:
# Always create migrations for model changes
python manage.py makemigrations
python manage.py migrate
# Commit migration files to version control
git add apps/*/migrations/*.py
git commit -m "Add user profile model"
9.7 Anti-Pattern: Circular Imports
❌ Bad:
# apps/users/models.py
from apps.blog.models import Post # Circular import!
class User(models.Model):
favorite_post = models.ForeignKey(Post, ...)
# apps/blog/models.py
from apps.users.models import User # Circular import!
class Post(models.Model):
author = models.ForeignKey(User, ...)
✅ Good:
# apps/users/models.py
from django.db import models
class User(models.Model):
favorite_post = models.ForeignKey(
'blog.Post', # String reference
on_delete=models.SET_NULL,
null=True,
)
# apps/blog/models.py
from django.conf import settings
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # Reference to user model
on_delete=models.CASCADE,
)
Best Practices Summary
Django Project Checklist
- Project structure follows Django conventions
- Settings split by environment (base, dev, prod, test)
- SECRET_KEY and sensitive data in environment variables
- DEBUG=False in production
- All security middleware enabled in production
- ALLOWED_HOSTS configured correctly
- Database connection pooling enabled (CONN_MAX_AGE)
- Static/media files properly configured
Model Best Practices
- Models use verbose field names with gettext_lazy
- TextChoices/IntegerChoices for choice fields
- Explicit related_name on ForeignKey/ManyToMany
- Appropriate on_delete handlers
- Database indexes on frequently queried fields
- Timestamps (created_at, updated_at) on models
- Custom managers for reusable query logic
- Model methods for business logic
- Signals for cross-cutting concerns
View Best Practices
- CBVs for standard CRUD, FBVs for custom logic
- Mixins for reusable view behavior
- Proper permission checks (LoginRequired, etc.)
- select_related/prefetch_related to avoid N+1
- Pagination on list views
- Form validation in forms, not views
DRF Best Practices
- Separate serializers for list/detail/write
- Permission classes for access control
- Token/JWT authentication configured
- Pagination enabled on list endpoints
- Filtering and search configured
- API versioning implemented
- Proper error handling and responses
Security Checklist
- CSRF protection enabled
- Using Django ORM (not raw SQL concatenation)
- Auto-escaping enabled in templates
- SECRET_KEY in environment variable
- DEBUG=False in production
- Security middleware enabled
- HTTPS enforced (SECURE_SSL_REDIRECT)
- Password validators configured
Performance Checklist
- select_related/prefetch_related used appropriately
- Database indexes on frequently queried fields
- Query optimization (only/defer when appropriate)
- Caching strategy implemented
- Database connection pooling configured
- django-debug-toolbar installed in development
- Query monitoring in place
Testing Checklist
- Tests for all models
- Tests for all views/API endpoints
- Tests for forms and validation
- Tests for permissions
- Test coverage > 80%
- Integration tests for critical paths
Templates
Template 1: Django Model
Located at: templates/django_model_template.py
See: /Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/django-conventions/templates/django_model_template.py
Template 2: Django REST Framework ViewSet
Located at: templates/drf_viewset_template.py
See: /Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/django-conventions/templates/drf_viewset_template.py
Template 3: Django Form
Located at: templates/django_form_template.py
See: /Users/clo/developer/gh-clostaunau/holiday-card/.claude/skills/django-conventions/templates/django_form_template.py
Related Skills
- python-testing-standards: Python testing best practices (referenced for Django tests)
- python-type-hints-guide: Python type hints (applicable to Django)
- uncle-duke-python: Python code review agent that uses this skill
References
Official Documentation
Security
Performance
Books and Guides
- Two Scoops of Django (Best Practices Book)
- Django for APIs (Django REST Framework)
- High Performance Django
Version: 1.0 Last Updated: 2025-12-24 Maintainer: Development Team