Mastering Decoupled Communication: A Comprehensive Guide to Custom Signals in Django

Mastering Decoupled Communication: A Comprehensive Guide to Custom Signals in Django

ยท

4 min read

As discussed in the previous article, Signals are a way for different parts of our application to communicate with each other based on any events or triggers. They allow decoupled communication between different components, promoting modularity and flexibility in your codebase. Signals follow the publisher-subscriber pattern, where publishers trigger signals and subscribers respond to those signals.

If you haven't read my previous article on Django Signals, please follow the link - https://musaaib.hashnode.dev/django-signals-demystified-streamlining-model-interactions

In the previous blog, we discussed the pre_save, post_save, pre_delete and post_delete signals, which are the built in Django signals that are triggered on changing a model instance. These signals are primarily related to model interactions. Now, let us move a step ahead in Signals and see how we can create custom signals in Django and utilise them to enhance our application.

Let's take an example of an application wherein we have to send some notifications to the user when something happens that doesn't involve a model interaction. So, when there's no model interaction, there's no chance of utilizing pre_save, post_save, pre_delete and post_delete signals. This is where custom signals come into play. People say, talk is cheap, so let's jump into the code.

#signals.py
from django.dispatch import Signal

# Signal for sending notifications to a user
user_notification = Signal(providing_args=["user", "message"])

# Signal for sending notifications to an admin
admin_notification = Signal(providing_args=["message"])

Here we have just imported the Signal class from django.dispatch module. After that we have created two custom signals user_notification and admin_notification by simply creating two instances of the imported Signal class. As seen in the code above, we have passed providing_args . These include the arguments that need to be passed to signal when it's triggered.

Now, we'll see how to trigger these signals. Let suppose we have two functions, one of which happens to do something and notify the user and other happens to do something and notify the admin.

#views.py

from django.contrib.auth.models import User
from .signals import user_notification, admin_notification

# Function to send user notifications
def some_action(request, user):
    # Perform some action...
    message = "Hey, Djangoer, we've got something for you!"
    user_notification.send(sender=None, user=user, message=message)

# Function to send admin notifications
def admin_action(request):
    # Perform some admin action...
    message = "Your Majesty, we've got something for you!"
    admin_notification.send(sender=None, message=message)

So, we have just imported the built in User class and the signals we just created. Then we have created two view functions, one of which triggers the signal to send user notification and the other triggers the signal to send admin notifications.

So, what do you think now. If we call either of the function, the user will receive a notification? Ummm, I think, maybe yes. Let's try it out.

2 minutes later!!!!

Didn't work? It obviously won't. We haven't registered the receiver functions to receive the signals and do the needful. So, let's do that.

#receivers.py

from django.dispatch import receiver
from django.core.mail import send_mail
from .signals import user_notification, admin_notification

# Receiver for user notifications
@receiver(user_notification)
def handle_user_notification(sender, user, message, **kwargs):
    # Notify the user via email
    send_mail(
        'Notification for our Djangoer',
        message,
        'from@example.com',
        [user.email],
        fail_silently=False,
    )

# Receiver for admin notifications
@receiver(admin_notification)
def handle_admin_notification(sender, message, **kwargs):
    # Notify the admin via email
    send_mail(
        'Notification',
        message,
        'from@example.com',
        ['admin@example.com'],
        fail_silently=False,
    )

What do you say? Would it work now. Let's give it a try, no?

1 minute later!!!

Do you know why didn't it work. It didn't because we haven't registered our Signals anywhere. Django expects us to register them. So, in our apps.py file, we'll register our Signal as follows

#apps.py

from django.apps import AppConfig

class HomeConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'home'

    def ready(self):
        import home.signals  # Import the signals file to register the signals

Now, we are ready to use the custom signals we created. In the previous article, I missed registering the Signals, but in this one, I have made sure to cover that as well.

Throughout this guide, we've explored how to create, send, and receive custom signals, enabling developers to trigger specific actions in response to various events. Whether it's implementing a notification system, performing post-save operations, or facilitating custom workflows, Django signals provide the flexibility to adapt and scale applications without compromising the core logic.

Harnessing the potential of custom signals empowers developers to build robust and flexible Django applications that respond dynamically to changing requirements. As with any powerful tool, it's crucial to wield signals judiciously, ensuring a clear and documented structure to maintain readability and facilitate collaboration within your codebase.

By embracing the capabilities of custom signals in Django, developers can create more modular, scalable, and responsive applications, unlocking a realm of possibilities for innovative and efficient software development.

Go Django!

Keep learning. Keep Djangoing!

Peace, Dot!

ย