Skip to content

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#

  1. Use transactions - For related database operations
  2. Cache expensive operations - Improve performance
  3. Use debug toolbar - Only in development
  4. Prefer generic views - Less code, more functionality
  5. Use signals carefully - Can make code harder to follow
  6. Custom user model - Set up early in project
  7. Create management commands - For repetitive tasks
  8. Custom template tags - Reuse template logic

✅ Next Steps#


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!