Django Custom Signals
In the previous sections, we explored Django's built-in signals, which allow you to execute code in response to certain events in your application. While Django's built-in signals are powerful, you might encounter situations where you need to define your own signals for custom events specific to your application's business logic. This is where custom signals come in.
Understanding Custom Signals
Custom signals in Django allow you to create your own event notification system. They are useful when you want different components of your application to communicate without being tightly coupled.
For example, you might want to:
- Notify other parts of your application when a user completes a specific action
- Trigger email notifications when certain business events occur
- Update related data when a specific change happens
Why Use Custom Signals?
- Loose Coupling: Components can interact without directly referencing each other
- Modularity: Makes your code more modular and easier to maintain
- Extensibility: Makes it easy to add new functionality without modifying existing code
Creating Custom Signals
Creating custom signals in Django is straightforward and involves three main steps:
- Defining the signal
- Sending the signal
- Connecting receivers to the signal
Let's dive into each step.
Step 1: Defining a Custom Signal
First, you need to define your custom signal. It's a common practice to define signals in a signals.py
file within your Django app.
# myapp/signals.py
from django.dispatch import Signal
# Define custom signals
payment_completed = Signal() # Providing default arguments is optional
order_status_changed = Signal(providing_args=["order", "old_status", "new_status"]) # For Django < 3.1
In Django 3.1 and later, the providing_args
parameter is deprecated as signals now dynamically determine their arguments. However, it's still useful as self-documentation.
Step 2: Sending the Signal
Once you've defined a signal, you can "send" it from anywhere in your code when a specific event occurs:
# myapp/views.py
from django.shortcuts import render
from .signals import payment_completed
from .models import Payment
def process_payment(request, payment_id):
payment = Payment.objects.get(id=payment_id)
# Process the payment...
payment.status = 'completed'
payment.save()
# Send the custom signal with the payment object
payment_completed.send(sender=Payment, payment=payment, amount=payment.amount)
return render(request, 'payment_success.html')
The send()
method accepts:
sender
: Typically the class that's sending the signal- Additional keyword arguments: Any data you want to pass to the receivers
Step 3: Connecting Receivers to the Signal
Finally, you need to connect receiver functions to your custom signal. This is done the same way as with built-in signals:
# myapp/receivers.py
from django.dispatch import receiver
from .signals import payment_completed
from .models import PaymentLog
@receiver(payment_completed)
def log_completed_payment(sender, payment, amount, **kwargs):
PaymentLog.objects.create(
payment=payment,
amount=amount,
description=f"Payment {payment.id} completed successfully"
)
print(f"Payment of ${amount} has been completed!")
Remember to make sure your receiver functions are imported somewhere in your app's configuration. A common approach is to import them in your app's apps.py
:
# myapp/apps.py
from django.apps import AppConfig
class MyappConfig(AppConfig):
name = 'myapp'
def ready(self):
import myapp.receivers # Import the receivers to register them
Real-world Example: E-commerce Order Processing
Let's look at a practical example for an e-commerce application. We'll create custom signals for order processing.
Setting Up the Signals
# orders/signals.py
from django.dispatch import Signal
# Define custom signals
order_created = Signal() # Sent when a new order is created
order_status_changed = Signal() # Sent when order status changes
order_delivered = Signal() # Sent when order is delivered
Models and Signal Sending
# orders/models.py
from django.db import models
from django.contrib.auth.models import User
from .signals import order_status_changed, order_delivered
class Order(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('processing', 'Processing'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
('canceled', 'Canceled'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
product = models.CharField(max_length=100)
quantity = models.IntegerField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
if self.pk:
# Get the old status before saving
old_status = Order.objects.get(pk=self.pk).status
# Save the instance
super().save(*args, **kwargs)
# Check if status has changed
if old_status != self.status:
# Send signal for status change
order_status_changed.send(
sender=self.__class__,
order=self,
old_status=old_status,
new_status=self.status
)
# Send specific signal for delivery
if self.status == 'delivered':
order_delivered.send(sender=self.__class__, order=self)
else:
# New order, just save it
super().save(*args, **kwargs)
Signal Receivers
# orders/receivers.py
from django.dispatch import receiver
from .signals import order_created, order_status_changed, order_delivered
from django.core.mail import send_mail
@receiver(order_status_changed)
def notify_status_change(sender, order, old_status, new_status, **kwargs):
"""Send email notification to the user when order status changes"""
subject = f'Order #{order.id} Status Update'
message = f'Your order #{order.id} has been updated from {old_status} to {new_status}.'
send_mail(
subject,
message,
'[email protected]',
[order.user.email],
fail_silently=False,
)
print(f"Notification sent for Order #{order.id} status change")
@receiver(order_delivered)
def update_inventory_after_delivery(sender, order, **kwargs):
"""Update inventory after successful delivery"""
print(f"Updating inventory after Order #{order.id} delivery")
# Here you would update your inventory system
# Inventory.decrease(order.product, order.quantity)
@receiver(order_delivered)
def request_customer_feedback(sender, order, **kwargs):
"""Send feedback request after delivery"""
subject = f'How was your order #{order.id}?'
message = f'Your order has been delivered. We would appreciate your feedback!'
# Send after a delay - this is just an example, you might use a task queue
print(f"Scheduling feedback request for Order #{order.id}")
Apps Configuration
# orders/apps.py
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'orders'
def ready(self):
import orders.receivers # Import the receivers
Usage Example
# orders/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .models import Order
from .signals import order_created
def create_order(request):
if request.method == 'POST':
# Simple order creation for demonstration
order = Order.objects.create(
user=request.user,
product=request.POST.get('product'),
quantity=int(request.POST.get('quantity')),
status='pending'
)
# Send the order_created signal
order_created.send(sender=Order, order=order)
messages.success(request, f'Order #{order.id} created successfully!')
return redirect('order_detail', order_id=order.id)
return render(request, 'orders/create_order.html')
Advanced Signal Techniques
Using Signal Context Manager
Django provides a signal context manager that can be used to temporarily connect a receiver to a signal:
from django.dispatch import Signal
payment_received = Signal()
def handle_payment(sender, amount, **kwargs):
print(f"Payment received: ${amount}")
# Temporarily connect the receiver
with payment_received.connect(handle_payment):
payment_received.send(sender=None, amount=100.00)
# Prints: "Payment received: $100.00"
# The receiver is automatically disconnected outside the context manager
payment_received.send(sender=None, amount=200.00)
# No output, as the handler is no longer connected
Signal Namespaces
You can organize signals into namespaces for better structure:
class PaymentSignals:
payment_received = Signal()
payment_refunded = Signal()
payment_failed = Signal()
# Usage
PaymentSignals.payment_received.send(sender=None, amount=50.00)
Synchronous vs Asynchronous Signals
By default, Django signals are synchronous, meaning the sender waits for all receivers to complete before continuing. For time-consuming operations, consider using asynchronous task queues:
from django.dispatch import receiver
from .signals import order_delivered
from celery import shared_task
@receiver(order_delivered)
def schedule_feedback_email(sender, order, **kwargs):
# Instead of sending directly, schedule an asynchronous task
send_feedback_email_task.delay(order.id, order.user.email)
@shared_task
def send_feedback_email_task(order_id, email):
# This runs asynchronously in a Celery worker
# Heavy operations here won't block the main thread
# ...
print(f"Feedback email sent for order #{order_id}")
Summary
Custom signals in Django provide a powerful mechanism to implement event-driven architecture in your applications. They promote loose coupling between components and make your code more modular and maintainable.
To implement custom signals:
- Define your signals in a
signals.py
file - Send signals when specific events occur in your application
- Connect receiver functions to handle those signals
- Make sure your app's
ready()
method imports the receivers
By using custom signals, you can greatly enhance the architecture of your Django applications, making them more extensible and easier to maintain as they grow in complexity.
Additional Resources and Exercises
Resources
Exercises
-
Exercise 1: Create a custom signal called
user_profile_viewed
that sends a notification when someone views a user's profile. -
Exercise 2: Implement a "notification system" using custom signals. When certain events happen (e.g., new comment, friend request), send appropriate signals.
-
Exercise 3: Extend the e-commerce example above by adding a
payment_failed
signal and implementing appropriate receivers. -
Challenge: Create a system where signals can be enabled/disabled through the admin panel using a simple configuration model.
Remember that while signals are powerful, they can also make code flow harder to follow. Use them judiciously, especially for cross-cutting concerns where tight coupling would be problematic.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)