59.10 Django Signals, Middleware, and Context Processors
Alright, let’s pull back the curtain on three of Django’s most powerful—and most misused—features. These are the tools that separate a simple CRUD app from a truly engineered one. They’re the duct tape and WD-40 of the framework, letting you hook into Django’s core request/response process without rewriting the whole thing. But with great power comes great responsibility, and I’ve seen some truly horrific abuses of these patterns. Let’s do it right.
The Quiet Gossips: Django Signals
Signals are essentially the application-level version of a pub/sub system. Something happens in your app (a model is saved, a request is started), and it sends a signal. Other pieces of your code can listen for that signal and do something in response. It’s a way to keep disparate parts of your app loosely coupled.
The classic example is creating a user profile when a new user is created. You could override the save() method on the User model, but please, for the love of all that is holy, don’t. You don’t own that model. Signals are the correct, decoupled way to do it.
# In your signals.py module
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import UserProfile
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""Creates a UserProfile whenever a User is created."""
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Saves the UserProfile whenever the User is saved."""
instance.userprofile.save()
Why this works: We’re listening to the post_save signal, but only when the sender is the User model. The created argument tells us if this was a new record or an update. The instance is the actual User object that was just saved.
Pitfall #1: Where you put this code matters. You must import this signals module in your app’s ready() method (in apps.py) to ensure the receiver gets registered when Django starts. Otherwise, it’s just a sad, unused function.
# In your_app/apps.py
from django.apps import AppConfig
class YourAppConfig(AppConfig):
name = 'your_app'
def ready(self):
import your_app.signals # noqa
Pitfall #2: Signals are global. They can make your application’s flow incredibly hard to reason about. You can’t easily look at a user.save() and know what else it will trigger. Use them sparingly, mostly for truly decoupled, non-critical side effects. Never, ever use them for critical business logic where you need a transaction.atomic() block—if the signal handler fails, the main action that triggered the signal has already succeeded.
The Bouncers and Butlers: Middleware
Think of middleware as a series of layers an onion… or an ogre. Each request must pass through every one of your middleware classes on the way in, and then each response passes back through them all on the way out. It’s your chance to inspect, modify, or outright reject requests and responses at a global level.
Want to add a custom header to every response? Middleware. Want to reject requests from a specific IP range? Middleware. Need to measure performance? You get the idea.
Here’s a simple, slightly absurd middleware that adds a snarky header to every response:
# middleware.py
class SnarkyHeaderMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Code to run on the way IN (before the view)
response = self.get_response(request) # This is the view and all other middleware
# Code to run on the way OUT (after the view)
response['X-Developer-Mood'] = 'Caffeinated'
return response
To use it, add it to MIDDLEWARE in your settings. The order is critical! It runs top-down on request and bottom-up on response. Put security-oriented middleware (like Django’s SecurityMiddleware) at the top and yours near the bottom.
The Gotcha: That get_response thing is called the “middleware factory” pattern. It’s a one-time setup call. The __call__ method is called for every single request. This is where the magic happens. Mess this up, and you’ll break your entire site.
The Personal Assistants: Context Processors
Ever gotten sick of typing the same data into every single render() call in your views? “Oh, I need the current site name here, and the user object here, and a list of categories for the nav bar here…” Context processors are your solution. They are functions that automatically add variables to the template context of every view that uses a RequestContext (which, if you’re using render(), you are).
Django comes with built-in ones, like auth, which adds the user object. Let’s make our own that adds a SITE_NAME setting to every context.
# context_processors.py
from django.conf import settings
def site_name(request):
return {'SITE_NAME': settings.SITE_NAME}
Now, add it to the TEMPLATES setting in your settings.py:
# settings.py
TEMPLATES = [
{
...
'OPTIONS': {
'context_processors': [
...
'my_app.context_processors.site_name',
],
},
},
]
Now, in any template, you can just use {{ SITE_NAME }} without ever passing it from the view. It’s fantastic for global, read-only data.
The Fine Print: Don’t be tempted to put heavy database queries in here. This function runs for every single view that renders a template. If you slap a Category.objects.all() in there, you’re now hitting the database on every page load. Cache that stuff aggressively or find a smarter way.
So there you have it. Signals for decoupled events, middleware for global request/response handling, and context processors for global template variables. Use them wisely, and your code will be elegant and powerful. Abuse them, and you’ll create a spaghetti-code monster that’s impossible to debug. The choice, as always, is yours.