django-conventions
npx skills add https://github.com/clostaunau/holiday-card --skill django-conventions
Agent 安装分布
Skill 文档
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_() methods, validate across 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