Challenge 3: Middleware vs Signal — Which Fits? — Possible Solution ==================================================================== (a) Rejecting all requests from a blocked list of IP addresses -> MIDDLEWARE This needs to run on EVERY incoming request, before any view even has a chance to execute, and it needs to be able to short-circuit the request entirely (return an error response immediately without calling get_response() at all). Signals don't fit here at all — there's no relevant model save/delete event to hook into; this is purely about intercepting the request/response cycle itself, which is exactly what middleware is for. def blocked_ip_middleware(get_response): BLOCKED_IPS = {"203.0.113.5", "198.51.100.9"} def middleware(request): if request.META.get("REMOTE_ADDR") in BLOCKED_IPS: from django.http import HttpResponseForbidden return HttpResponseForbidden("Access denied.") return get_response(request) return middleware (b) Sending a welcome email after a new user registers -> SIGNAL (specifically, post_save on the User model) This is reacting to an EVENT (a User row being created) rather than intercepting every request — and critically, the code that creates a User (Chapter 1's register() view) shouldn't need to directly import and call an email-sending function itself. A post_save signal with created=True decouples "a user was created" from "here's everything that should happen as a result," which is the same principle behind this chapter's UserProfile auto-creation example. @receiver(post_save, sender=User) def send_welcome_email(sender, instance, created, **kwargs): if created: send_mail( "Welcome!", f"Thanks for signing up, {instance.username}!", "noreply@example.com", [instance.email], ) (c) Adding a Content-Security-Policy header to every response -> MIDDLEWARE Same reasoning as Challenge 1's X-Content-Type-Options header — this needs to apply to every response leaving the application, regardless of which view produced it, with no relevant "event" to hook a signal into. This is purely a response-shaping concern, exactly what middleware is designed for. def csp_header_middleware(get_response): def middleware(request): response = get_response(request) response["Content-Security-Policy"] = "default-src 'self'" return response return middleware WHY THIS WORKS AS A DECISION FRAMEWORK ------------------------------------------ - Ask "does this need to run on EVERY request/response, regardless of which view or model is involved?" — if yes, that's middleware's job. - Ask "is this reacting to something happening to a SPECIFIC model instance (created, updated, deleted), where the code causing that event shouldn't need to know about the reaction?" — if yes, that's a signal's job. - If neither applies cleanly (the reaction is tightly coupled to one specific view or workflow, and there's no real decoupling benefit), neither middleware nor a signal is the right tool — a direct function call or an overridden save() method, as the chapter's gotcha suggests, is often the clearer choice.