Advanced Django Topics#
Complete guide to advanced Django features: transactions, caching, debug toolbar, generic views, and more.
🔄 Database Transactions#
Atomic Transactions#
from django.db import transaction
# Decorator approach
@transaction.atomic
def transfer_money(from_account, to_account, amount):
from_account.balance -= amount
from_account.save()
to_account.balance += amount
to_account.save()
# Context manager approach
def transfer_money(from_account, to_account, amount):
with transaction.atomic():
from_account.balance -= amount
from_account.save()
to_account.balance += amount
to_account.save()
# If any error occurs, all changes are rolled back
Savepoints#
from django.db import transaction
def complex_operation():
with transaction.atomic():
# Outer transaction
obj1 = Model1.objects.create(...)
sid = transaction.savepoint() # Create savepoint
try:
# Inner operation
obj2 = Model2.objects.create(...)
# If this fails, rollback to savepoint
except Exception:
transaction.savepoint_rollback(sid)
raise
transaction.savepoint_commit(sid)
Transaction in Views#
from django.db import transaction
from django.http import JsonResponse
@transaction.atomic
def create_order(request):
# All database operations in this view are atomic
order = Order.objects.create(...)
# If any error, entire transaction rolls back
return JsonResponse({'status': 'success'})
💾 Caching#
Cache Backends#
# myproject/settings.py
# Memory cache (development)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
# Redis cache (production)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
# File-based cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': BASE_DIR / 'cache',
}
}
Cache API#
from django.core.cache import cache
# Set cache
cache.set('key', 'value', timeout=300) # 5 minutes
# Get cache
value = cache.get('key')
value = cache.get('key', 'default') # Default if not found
# Delete cache
cache.delete('key')
# Clear all cache
cache.clear()
# Get or set
value = cache.get_or_set('key', expensive_function, timeout=300)
Per-View Caching#
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
# Function-based view
@cache_page(60 * 15) # Cache for 15 minutes
def my_view(request):
return render(request, 'template.html')
# Class-based view
@method_decorator(cache_page(60 * 15), name='dispatch')
class MyView(ListView):
pass
Template Fragment Caching#
{% load cache %}
{% cache 500 sidebar %}
<!-- Expensive content -->
{% for item in items %}
{{ item }}
{% endfor %}
{% endcache %}
<!-- Cache with key -->
{% cache 500 sidebar request.user.id %}
User-specific content
{% endcache %}
Cache Querysets#
from django.core.cache import cache
def get_popular_posts():
cache_key = 'popular_posts'
posts = cache.get(cache_key)
if posts is None:
posts = Post.objects.filter(is_published=True).order_by('-views')[:10]
cache.set(cache_key, posts, timeout=300) # 5 minutes
return posts
🐛 Django Debug Toolbar#
Installation#
# Install
pip install django-debug-toolbar
Configuration#
# myproject/settings.py
INSTALLED_APPS = [
# ...
'debug_toolbar',
]
MIDDLEWARE = [
# ...
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
# Internal IPs (for development)
INTERNAL_IPS = [
'127.0.0.1',
]
# Debug toolbar configuration
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG,
}
URL Configuration#
# myproject/urls.py
from django.conf import settings
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
Features#
- SQL queries and time
- Templates used
- Request/response headers
- Settings
- Static files
- Cache usage
- Signals
- Profiling
🎯 Generic Views#
ListView#
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = 'myapp/post_list.html'
context_object_name = 'posts'
paginate_by = 10
queryset = Post.objects.filter(is_published=True)
def get_queryset(self):
queryset = super().get_queryset()
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(title__icontains=search)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
return context
DetailView#
from django.views.generic import DetailView
from django.shortcuts import get_object_or_404
class PostDetailView(DetailView):
model = Post
template_name = 'myapp/post_detail.html'
context_object_name = 'post'
queryset = Post.objects.filter(is_published=True)
def get_object(self):
obj = super().get_object()
obj.increment_views() # Custom logic
return obj
CreateView#
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django.contrib import messages
class PostCreateView(CreateView):
model = Post
fields = ['title', 'content', 'category']
template_name = 'myapp/post_form.html'
success_url = reverse_lazy('myapp:post_list')
def form_valid(self, form):
form.instance.author = self.request.user
messages.success(self.request, 'Post created!')
return super().form_valid(form)
UpdateView#
from django.views.generic import UpdateView
class PostUpdateView(UpdateView):
model = Post
fields = ['title', 'content', 'category']
template_name = 'myapp/post_form.html'
def get_success_url(self):
return reverse_lazy('myapp:post_detail', kwargs={'pk': self.object.pk})
DeleteView#
from django.views.generic import DeleteView
class PostDeleteView(DeleteView):
model = Post
template_name = 'myapp/post_confirm_delete.html'
success_url = reverse_lazy('myapp:post_list')
🔔 Signals#
Built-in Signals#
# myapp/signals.py
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from django.dispatch import receiver
from .models import Post
@receiver(pre_save, sender=Post)
def pre_save_post(sender, instance, **kwargs):
# Before saving
if not instance.slug:
instance.slug = instance.title.lower().replace(' ', '-')
@receiver(post_save, sender=Post)
def post_save_post(sender, instance, created, **kwargs):
# After saving
if created:
print(f'New post created: {instance.title}')
else:
print(f'Post updated: {instance.title}')
@receiver(pre_delete, sender=Post)
def pre_delete_post(sender, instance, **kwargs):
# Before deleting
if instance.image:
instance.image.delete(save=False)
Connect Signals#
# myapp/apps.py
from django.apps import AppConfig
class MyappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
import myapp.signals # Import signals
Custom Signals#
# myapp/signals.py
from django.dispatch import Signal
# Define signal
post_published = Signal(providing_args=['post'])
# Send signal
post_published.send(sender=Post, post=post_instance)
# Receive signal
@receiver(post_published, sender=Post)
def handle_post_published(sender, post, **kwargs):
# Send email, update cache, etc.
pass
🔐 Custom Authentication#
Custom User Model#
# myapp/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
phone = models.CharField(max_length=20, blank=True)
bio = models.TextField(blank=True)
def __str__(self):
return self.username
# myproject/settings.py
AUTH_USER_MODEL = 'myapp.User'
Custom Authentication Backend#
# myapp/backends.py
from django.contrib.auth.backends import BaseBackend
from .models import User
class EmailBackend(BaseBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
return None
if user.check_password(password):
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
# myproject/settings.py
AUTHENTICATION_BACKENDS = [
'myapp.backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend',
]
📊 Custom Management Commands#
# myapp/management/commands/populate_data.py
from django.core.management.base import BaseCommand
from myapp.models import Post
class Command(BaseCommand):
help = 'Populate database with sample data'
def add_arguments(self, parser):
parser.add_argument('--count', type=int, default=10, help='Number of posts to create')
def handle(self, *args, **options):
count = options['count']
for i in range(count):
Post.objects.create(
title=f'Post {i+1}',
content=f'Content for post {i+1}',
is_published=True
)
self.stdout.write(self.style.SUCCESS(f'Created {count} posts'))
# Run command
python manage.py populate_data --count=20
🎯 Custom Template Tags#
Simple Tag#
# myapp/templatetags/my_tags.py
from django import template
register = template.Library()
@register.simple_tag
def current_time(format_string):
from datetime import datetime
return datetime.now().strftime(format_string)
{% load my_tags %}
{% current_time "%Y-%m-%d %H:%M" %}
Inclusion Tag#
@register.inclusion_tag('myapp/recent_posts.html')
def show_recent_posts(count=5):
posts = Post.objects.filter(is_published=True)[:count]
return {'posts': posts}
<!-- myapp/templates/myapp/recent_posts.html -->
<ul>
{% for post in posts %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% load my_tags %}
{% show_recent_posts 10 %}
Filter#
@register.filter
def truncate_smart(value, arg):
"""Truncate at word boundary"""
try:
length = int(arg)
except ValueError:
return value
if len(value) <= length:
return value
return value[:length].rsplit(' ', 1)[0] + '...'
{{ post.content|truncate_smart:100 }}
✅ Best Practices#
- ✅ Use transactions - For related database operations
- ✅ Cache expensive operations - Improve performance
- ✅ Use debug toolbar - Only in development
- ✅ Prefer generic views - Less code, more functionality
- ✅ Use signals carefully - Can make code harder to follow
- ✅ Custom user model - Set up early in project
- ✅ Create management commands - For repetitive tasks
- ✅ Custom template tags - Reuse template logic
✅ Next Steps#
- Review ORM Queries for efficient database access
- Review Settings & Production for production setup
- Explore Django documentation for more advanced features
Pro Tip: Use transactions for operations that must succeed or fail together. Use caching for expensive queries or computations. Debug toolbar is invaluable for development - it shows all SQL queries and helps identify N+1 problems!