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=123 instead)
-
Always name your URLs for reverse lookup
Task 2: Model Best Practices
2.1 Model Design Patterns
Rule: Models should be focused, well-documented, and follow Django conventions.
✅ Good Model Design:
"""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_text for complex fields
-
Use TextChoices/IntegerChoices for choice fields
-
Include timestamps (created_at, updated_at) on most models
-
Use appropriate on_delete for ForeignKey
-
Set related_name and related_query_name on 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 _id suffix sparingly (Django adds it automatically to ForeignKey)
-
Use boolean field names that read like questions: is_active , has_paid , email_verified
-
Use date/time field names with _at or _date suffix: created_at , birth_date
2.3 Meta Class Options
Rule: Use Meta class to configure model behavior and database options.
✅ Good Meta Class:
class Article(models.Model): """Article model with comprehensive Meta configuration."""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# Verbose names for admin
verbose_name = _('article')
verbose_name_plural = _('articles')
# Default ordering
ordering = ['-published_at', '-created_at']
# Get latest by
get_latest_by = 'published_at'
# Database table name (optional, Django auto-generates)
db_table = 'blog_articles'
# Indexes for query optimization
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['author', '-published_at']),
models.Index(fields=['category', '-published_at']),
models.Index(fields=['-published_at'], name='recent_articles_idx'),
]
# Unique together constraints
constraints = [
models.UniqueConstraint(
fields=['author', 'slug'],
name='unique_author_slug',
),
models.CheckConstraint(
check=models.Q(published_at__isnull=True) | models.Q(published_at__gte=models.F('created_at')),
name='published_after_created',
),
]
# Permissions
permissions = [
('can_publish', 'Can publish articles'),
('can_feature', 'Can feature articles'),
]
Common Meta Options:
-
verbose_name / verbose_name_plural : Admin display names
-
ordering : Default query ordering
-
indexes : Database indexes for performance
-
constraints : UniqueConstraint, CheckConstraint for data integrity
-
permissions : Custom permissions
-
db_table : Custom table name (use sparingly)
-
get_latest_by : Field to use for latest()
-
abstract : For abstract base models
-
managed : Whether Django manages database lifecycle
2.4 Managers and QuerySets
Rule: Use custom managers for reusable query logic and QuerySets for chainable queries.
✅ Good Custom Manager and QuerySet:
apps/blog/managers.py :
"""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() and prefetch_related() in manager methods
2.5 Model Methods vs Signals
Rule: Use model methods for object-specific logic, signals for cross-cutting concerns.
✅ Good - Use Model Methods:
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_name for clear query filtering
-
Use related_name='+' to disable reverse relation if not needed
-
Avoid name conflicts across apps using app_label
2.7 Database Indexing Strategies
Rule: Add indexes for fields frequently used in queries, filtering, and ordering.
✅ Good Indexing:
class Article(models.Model): """Article model with strategic indexing."""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True) # Unique creates index automatically
author = models.ForeignKey(User, on_delete=models.CASCADE) # FK creates index
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
status = models.CharField(max_length=20, choices=StatusChoices.choices)
featured = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
view_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Index for filtering by status and ordering
models.Index(fields=['status', '-published_at'], name='status_pub_idx'),
# Index for author's articles
models.Index(fields=['author', '-created_at'], name='author_articles_idx'),
# Index for category browsing
models.Index(fields=['category', '-published_at'], name='cat_pub_idx'),
# Index for featured articles
models.Index(
fields=['-published_at'],
condition=models.Q(featured=True),
name='featured_idx', # Partial index (PostgreSQL)
),
# Compound index for complex queries
models.Index(
fields=['status', 'featured', '-view_count'],
name='popular_published_idx',
),
]
# Text search index (PostgreSQL)
# For full-text search capabilities
# Note: Requires GinIndex from django.contrib.postgres.indexes
When to Add Indexes:
-
Foreign keys (automatic in Django)
-
Fields in WHERE clauses
-
Fields in ORDER BY clauses
-
Fields in JOIN conditions
-
Unique fields (automatic)
-
Fields used in filter() , exclude() , get()
When NOT to Add Indexes:
-
Small tables (< 10,000 rows)
-
Fields that change frequently
-
Fields with low cardinality (few unique values like boolean)
-
Too many indexes slow down writes
Index Analysis:
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:
- 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
- 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), ), ]
- 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 RunPython for data migrations with reverse operations
-
Test migrations on production-like data
-
Use --check in 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 template
-
ListView : List objects
-
DetailView : Display single object
-
CreateView : Create object with form
-
UpdateView : Update object with form
-
DeleteView : Delete object
-
FormView : Display and process form
-
RedirectView : 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 authentication
-
PermissionRequiredMixin : Require specific permission
-
UserPassesTestMixin : Custom test function
-
UserOwnerMixin : 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 SerializerMethodField for computed fields
-
Use source parameter to reference model methods/properties
-
Set read_only=True for computed fields
-
Use write_only=True for 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 authentication
-
IsAdminUser : Require staff status
-
IsAuthenticatedOrReadOnly : Read for all, write for authenticated
-
DjangoModelPermissions : Use Django model permissions
-
DjangoObjectPermissions : 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: templates/django_model_template.py
Template 2: Django REST Framework ViewSet
Located at: templates/drf_viewset_template.py
See: templates/drf_viewset_template.py
Template 3: Django Form
Located at: templates/django_form_template.py
See: 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
-
Django Documentation
-
Django REST Framework Documentation
-
Django Best Practices
Security
-
Django Security
-
OWASP Django Security
Performance
-
Django Database Optimization
-
Django Caching
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