Django Framework Guide¶
Applies to: Django 4.2+, Django REST Framework, Django Channels Language: Python 3.10+ Type: Full-stack Web Framework
Overview¶
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It follows the "batteries included" philosophy, providing everything needed to build web applications out of the box.
Use Django when: - Building full-stack web applications with templates - Need an admin interface out of the box - Want ORM with migrations included - Building content management systems - Require session-based authentication
Consider alternatives when: - Building pure APIs (consider FastAPI) - Need async-first architecture - Want minimal framework overhead
Project Structure¶
Standard Django Project¶
myproject/
├── manage.py
├── pyproject.toml
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── config/ # Project configuration
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── dev.py
│ │ └── prod.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/ # Django applications
│ ├── users/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── forms.py
│ │ ├── serializers.py # If using DRF
│ │ ├── services.py # Business logic
│ │ └── tests/
│ │ ├── __init__.py
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ └── test_services.py
│ └── core/ # Shared utilities
│ ├── __init__.py
│ ├── models.py # Abstract base models
│ └── mixins.py
├── templates/
│ ├── base.html
│ └── components/
├── static/
│ ├── css/
│ └── js/
└── tests/
└── conftest.py
Settings Configuration¶
Base Settings¶
# config/settings/base.py
from pathlib import Path
from decouple import config, Csv
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = config("SECRET_KEY")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party
"rest_framework",
"corsheaders",
"django_extensions",
# Local apps
"apps.users",
"apps.core",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"corsheaders.middleware.CorsMiddleware",
"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 = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# Database
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": config("DB_NAME"),
"USER": config("DB_USER"),
"PASSWORD": config("DB_PASSWORD"),
"HOST": config("DB_HOST", default="localhost"),
"PORT": config("DB_PORT", default="5432"),
}
}
# Custom user model
AUTH_USER_MODEL = "users.User"
# Static files
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
# Media files
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
},
}
Development Settings¶
# config/settings/dev.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Debug toolbar
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
INTERNAL_IPS = ["127.0.0.1"]
# Email backend for development
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# CORS for local development
CORS_ALLOW_ALL_ORIGINS = True
Models¶
Abstract Base Model¶
# apps/core/models.py
from django.db import models
import uuid
class TimeStampedModel(models.Model):
"""Abstract base model with created/updated timestamps."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDModel(models.Model):
"""Abstract base model with UUID primary key."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class SoftDeleteModel(models.Model):
"""Abstract base model with soft delete."""
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
from django.utils import timezone
self.is_deleted = True
self.deleted_at = timezone.now()
self.save(update_fields=["is_deleted", "deleted_at"])
def hard_delete(self):
super().delete()
Custom User Model¶
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from apps.core.models import TimeStampedModel, UUIDModel
class User(AbstractUser, UUIDModel, TimeStampedModel):
"""Custom user model with UUID and timestamps."""
email = models.EmailField(unique=True)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
bio = models.TextField(max_length=500, blank=True)
# Use email for authentication
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
class Meta:
db_table = "users"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["email"]),
models.Index(fields=["username"]),
]
def __str__(self) -> str:
return self.email
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}".strip() or self.username
class Profile(TimeStampedModel):
"""Extended user profile."""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
phone = models.CharField(max_length=20, blank=True)
address = models.TextField(blank=True)
birth_date = models.DateField(null=True, blank=True)
class Meta:
db_table = "profiles"
def __str__(self) -> str:
return f"Profile of {self.user.email}"
Model with Relationships¶
# apps/products/models.py
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal
from apps.core.models import TimeStampedModel, UUIDModel
class Category(TimeStampedModel):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="children",
)
class Meta:
db_table = "categories"
verbose_name_plural = "categories"
def __str__(self) -> str:
return self.name
class Product(UUIDModel, TimeStampedModel):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
ARCHIVED = "archived", "Archived"
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
description = models.TextField()
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(Decimal("0.01"))],
)
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name="products",
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
)
stock = models.PositiveIntegerField(default=0)
class Meta:
db_table = "products"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["slug"]),
models.Index(fields=["status"]),
models.Index(fields=["category", "status"]),
]
def __str__(self) -> str:
return self.name
@property
def is_available(self) -> bool:
return self.status == self.Status.PUBLISHED and self.stock > 0
Views¶
Class-Based Views¶
# apps/products/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.db.models import Q
from .models import Product
from .forms import ProductForm
class ProductListView(ListView):
model = Product
template_name = "products/list.html"
context_object_name = "products"
paginate_by = 20
def get_queryset(self):
queryset = Product.objects.filter(status=Product.Status.PUBLISHED)
# Search
search = self.request.GET.get("q")
if search:
queryset = queryset.filter(
Q(name__icontains=search) | Q(description__icontains=search)
)
# Category filter
category = self.request.GET.get("category")
if category:
queryset = queryset.filter(category__slug=category)
return queryset.select_related("category")
class ProductDetailView(DetailView):
model = Product
template_name = "products/detail.html"
context_object_name = "product"
slug_url_kwarg = "slug"
def get_queryset(self):
return Product.objects.select_related("category").prefetch_related(
"images", "reviews"
)
class ProductCreateView(LoginRequiredMixin, CreateView):
model = Product
form_class = ProductForm
template_name = "products/form.html"
success_url = reverse_lazy("products:list")
def form_valid(self, form):
form.instance.created_by = self.request.user
return super().form_valid(form)
Django REST Framework Views¶
# apps/products/views_api.py
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer, ProductCreateSerializer
from .filters import ProductFilter
class ProductViewSet(viewsets.ModelViewSet):
"""
ViewSet for Product CRUD operations.
list: Get all published products
retrieve: Get single product by ID
create: Create new product (authenticated)
update: Update product (owner only)
destroy: Delete product (owner only)
"""
queryset = Product.objects.select_related("category").prefetch_related("images")
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = ProductFilter
search_fields = ["name", "description"]
ordering_fields = ["price", "created_at", "name"]
ordering = ["-created_at"]
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update"]:
return ProductCreateSerializer
return ProductSerializer
def get_queryset(self):
queryset = super().get_queryset()
if self.action == "list":
queryset = queryset.filter(status=Product.Status.PUBLISHED)
return queryset
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
product = self.get_object()
product.status = Product.Status.PUBLISHED
product.save(update_fields=["status"])
return Response({"status": "published"})
@action(detail=False, methods=["get"])
def featured(self, request):
featured = self.get_queryset().filter(is_featured=True)[:10]
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
Serializers¶
# apps/products/serializers.py
from rest_framework import serializers
from django.utils.text import slugify
from .models import Product, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "slug"]
class ProductSerializer(serializers.ModelSerializer):
category = CategorySerializer(read_only=True)
is_available = serializers.BooleanField(read_only=True)
class Meta:
model = Product
fields = [
"id",
"name",
"slug",
"description",
"price",
"category",
"status",
"stock",
"is_available",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "created_at", "updated_at"]
class ProductCreateSerializer(serializers.ModelSerializer):
category_id = serializers.UUIDField(write_only=True)
class Meta:
model = Product
fields = [
"name",
"description",
"price",
"category_id",
"stock",
]
def validate_category_id(self, value):
if not Category.objects.filter(id=value).exists():
raise serializers.ValidationError("Category not found")
return value
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be positive")
return value
def create(self, validated_data):
category_id = validated_data.pop("category_id")
validated_data["category_id"] = category_id
validated_data["slug"] = slugify(validated_data["name"])
return super().create(validated_data)
Services (Business Logic)¶
# apps/products/services.py
from django.db import transaction
from django.core.exceptions import ValidationError
from typing import Optional
from .models import Product, Category
class ProductService:
"""Business logic for products."""
@staticmethod
@transaction.atomic
def create_product(
*,
name: str,
description: str,
price: float,
category_id: int,
stock: int = 0,
created_by,
) -> Product:
"""Create a new product with validation."""
# Validate category exists
try:
category = Category.objects.get(id=category_id)
except Category.DoesNotExist:
raise ValidationError("Category not found")
# Check for duplicate name in category
if Product.objects.filter(name=name, category=category).exists():
raise ValidationError("Product with this name already exists in category")
product = Product.objects.create(
name=name,
description=description,
price=price,
category=category,
stock=stock,
created_by=created_by,
)
return product
@staticmethod
def update_stock(product_id: int, quantity: int) -> Product:
"""Update product stock with optimistic locking."""
with transaction.atomic():
product = Product.objects.select_for_update().get(id=product_id)
new_stock = product.stock + quantity
if new_stock < 0:
raise ValidationError("Insufficient stock")
product.stock = new_stock
product.save(update_fields=["stock", "updated_at"])
return product
@staticmethod
def get_products_by_category(
category_slug: str,
limit: Optional[int] = None,
) -> list[Product]:
"""Get published products by category."""
queryset = Product.objects.filter(
category__slug=category_slug,
status=Product.Status.PUBLISHED,
).select_related("category")
if limit:
queryset = queryset[:limit]
return list(queryset)
Forms¶
# apps/products/forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.utils.text import slugify
from .models import Product
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ["name", "description", "price", "category", "stock"]
widgets = {
"description": forms.Textarea(attrs={"rows": 4}),
"price": forms.NumberInput(attrs={"step": "0.01", "min": "0"}),
}
def clean_name(self):
name = self.cleaned_data.get("name")
slug = slugify(name)
# Check for existing slug (excluding current instance)
qs = Product.objects.filter(slug=slug)
if self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError("A product with this name already exists")
return name
def save(self, commit=True):
instance = super().save(commit=False)
instance.slug = slugify(instance.name)
if commit:
instance.save()
return instance
URL Configuration¶
# config/urls.py
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/", include("apps.api.urls")),
path("", include("apps.products.urls")),
path("users/", include("apps.users.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
# apps/api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from apps.products.views_api import ProductViewSet
from apps.users.views_api import UserViewSet
router = DefaultRouter()
router.register(r"products", ProductViewSet)
router.register(r"users", UserViewSet)
urlpatterns = [
path("", include(router.urls)),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
Testing¶
# apps/products/tests/test_models.py
import pytest
from decimal import Decimal
from django.core.exceptions import ValidationError
from apps.products.models import Product, Category
@pytest.mark.django_db
class TestProductModel:
def test_create_product(self, category):
product = Product.objects.create(
name="Test Product",
slug="test-product",
description="Test description",
price=Decimal("19.99"),
category=category,
stock=10,
)
assert product.name == "Test Product"
assert product.price == Decimal("19.99")
assert product.is_available is False # Draft status
def test_product_is_available_when_published_with_stock(self, category):
product = Product.objects.create(
name="Available Product",
slug="available-product",
description="Test",
price=Decimal("10.00"),
category=category,
status=Product.Status.PUBLISHED,
stock=5,
)
assert product.is_available is True
def test_product_not_available_when_out_of_stock(self, category):
product = Product.objects.create(
name="Out of Stock",
slug="out-of-stock",
description="Test",
price=Decimal("10.00"),
category=category,
status=Product.Status.PUBLISHED,
stock=0,
)
assert product.is_available is False
# apps/products/tests/test_views.py
import pytest
from django.urls import reverse
from rest_framework import status
@pytest.mark.django_db
class TestProductAPI:
def test_list_products(self, api_client, published_products):
url = reverse("product-list")
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == len(published_products)
def test_create_product_authenticated(self, authenticated_client, category):
url = reverse("product-list")
data = {
"name": "New Product",
"description": "New description",
"price": "29.99",
"category_id": str(category.id),
"stock": 10,
}
response = authenticated_client.post(url, data)
assert response.status_code == status.HTTP_201_CREATED
assert Product.objects.filter(name="New Product").exists()
def test_create_product_unauthenticated(self, api_client, category):
url = reverse("product-list")
data = {"name": "New Product"}
response = api_client.post(url, data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
# conftest.py
import pytest
from rest_framework.test import APIClient
from decimal import Decimal
from apps.users.models import User
from apps.products.models import Product, Category
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def user(db):
return User.objects.create_user(
username="testuser",
email="test@example.com",
password="testpass123",
)
@pytest.fixture
def authenticated_client(api_client, user):
api_client.force_authenticate(user=user)
return api_client
@pytest.fixture
def category(db):
return Category.objects.create(name="Electronics", slug="electronics")
@pytest.fixture
def published_products(db, category):
products = []
for i in range(5):
products.append(
Product.objects.create(
name=f"Product {i}",
slug=f"product-{i}",
description="Test",
price=Decimal("10.00"),
category=category,
status=Product.Status.PUBLISHED,
stock=10,
)
)
return products
Admin Configuration¶
# apps/products/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Product, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["name", "slug", "parent", "product_count"]
prepopulated_fields = {"slug": ("name",)}
search_fields = ["name"]
def product_count(self, obj):
return obj.products.count()
product_count.short_description = "Products"
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ["name", "category", "price", "stock", "status", "is_available"]
list_filter = ["status", "category", "created_at"]
search_fields = ["name", "description"]
prepopulated_fields = {"slug": ("name",)}
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
fieldsets = (
(None, {"fields": ("name", "slug", "description")}),
("Pricing", {"fields": ("price", "stock")}),
("Classification", {"fields": ("category", "status")}),
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
)
def is_available(self, obj):
if obj.is_available:
return format_html('<span style="color: green;">✓</span>')
return format_html('<span style="color: red;">✗</span>')
is_available.short_description = "Available"
Management Commands¶
# apps/products/management/commands/import_products.py
from django.core.management.base import BaseCommand, CommandError
import csv
from decimal import Decimal
from apps.products.models import Product, Category
class Command(BaseCommand):
help = "Import products from CSV file"
def add_arguments(self, parser):
parser.add_argument("csv_file", type=str, help="Path to CSV file")
parser.add_argument(
"--dry-run",
action="store_true",
help="Preview import without saving",
)
def handle(self, *args, **options):
csv_file = options["csv_file"]
dry_run = options["dry_run"]
try:
with open(csv_file, "r") as f:
reader = csv.DictReader(f)
products = []
for row in reader:
category = Category.objects.get(slug=row["category_slug"])
product = Product(
name=row["name"],
slug=row["slug"],
description=row["description"],
price=Decimal(row["price"]),
category=category,
stock=int(row["stock"]),
)
products.append(product)
if dry_run:
self.stdout.write(f"Would import {len(products)} products")
else:
Product.objects.bulk_create(products)
self.stdout.write(
self.style.SUCCESS(f"Imported {len(products)} products")
)
except FileNotFoundError:
raise CommandError(f"File not found: {csv_file}")
except Category.DoesNotExist:
raise CommandError(f"Category not found: {row['category_slug']}")
Signals¶
# apps/products/signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Product
@receiver(post_save, sender=Product)
def invalidate_product_cache(sender, instance, **kwargs):
"""Invalidate cache when product is saved."""
cache.delete(f"product_{instance.id}")
cache.delete("featured_products")
@receiver(pre_delete, sender=Product)
def cleanup_product_files(sender, instance, **kwargs):
"""Clean up associated files before deletion."""
for image in instance.images.all():
image.file.delete(save=False)
# apps/products/apps.py
from django.apps import AppConfig
class ProductsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.products"
def ready(self):
import apps.products.signals # noqa
Middleware¶
# apps/core/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimingMiddleware:
"""Log request timing."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
duration = time.time() - start_time
logger.info(
f"{request.method} {request.path} - {response.status_code} - {duration:.3f}s"
)
return response
class CurrentUserMiddleware:
"""Make current user available in thread local."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from threading import local
_thread_locals = local()
_thread_locals.user = getattr(request, "user", None)
return self.get_response(request)
Celery Tasks¶
# apps/products/tasks.py
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
from .models import Product
@shared_task
def send_low_stock_alert(product_id: int):
"""Send alert when product stock is low."""
product = Product.objects.get(id=product_id)
if product.stock <= 5:
send_mail(
subject=f"Low stock alert: {product.name}",
message=f"Product {product.name} has only {product.stock} items left.",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[settings.ADMIN_EMAIL],
)
@shared_task
def update_product_statistics():
"""Daily task to update product statistics."""
from django.db.models import Count, Avg
products = Product.objects.annotate(
review_count=Count("reviews"),
avg_rating=Avg("reviews__rating"),
)
for product in products:
product.review_count_cached = product.review_count
product.avg_rating_cached = product.avg_rating or 0
Product.objects.bulk_update(
products, ["review_count_cached", "avg_rating_cached"]
)
Dependencies¶
# requirements/base.txt
Django>=4.2,<5.0
djangorestframework>=3.14
django-cors-headers>=4.0
django-filter>=23.0
djangorestframework-simplejwt>=5.3
python-decouple>=3.8
psycopg2-binary>=2.9
Pillow>=10.0
celery>=5.3
redis>=5.0
whitenoise>=6.6
# requirements/dev.txt
-r base.txt
pytest>=7.4
pytest-django>=4.5
pytest-cov>=4.1
factory-boy>=3.3
django-debug-toolbar>=4.2
django-extensions>=3.2
ipython>=8.0
black>=23.0
ruff>=0.1.0
mypy>=1.5
django-stubs>=4.2
Commands Reference¶
# Development
python manage.py runserver
python manage.py shell_plus # django-extensions
# Migrations
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
# Testing
pytest
pytest -v --cov=apps --cov-report=html
pytest apps/products/ -k "test_create"
# Database
python manage.py dbshell
python manage.py dumpdata products > fixtures/products.json
python manage.py loaddata fixtures/products.json
# Static files
python manage.py collectstatic
# Custom commands
python manage.py import_products data/products.csv --dry-run
# Celery
celery -A config worker -l info
celery -A config beat -l info
Best Practices¶
Do¶
- ✓ Use
select_relatedandprefetch_relatedfor queries - ✓ Create indexes for frequently queried fields
- ✓ Use transactions for multi-step operations
- ✓ Validate at model level and serializer level
- ✓ Write services for business logic
- ✓ Use signals sparingly (prefer explicit calls)
- ✓ Cache expensive queries
- ✓ Use environment variables for configuration
Don't¶
- ❌ Put business logic in views
- ❌ Use raw SQL without parameterization
- ❌ Ignore N+1 query problems
- ❌ Store sensitive data in settings files
- ❌ Use
Model.objects.all()without limits - ❌ Skip migrations in production