How To Use Django Signals

How To Use Django Signals

Introduction

In this technical discourse, I will introduce you to Django Signals, how to use them, and where and when to use them. By the end of this article, you should:

  • have a firm understanding of the Django signals
  • be able to create a function to connect to various signal
  • send email using Django

Prerequisite

For seamless follow-up of this tutorial, I expect you to :

  • have experience with python
  • have a beginner to intermediate experience with Django
  • have an already built Django project.

Definition

English Definition:

Something that incites an action

In the context of Django:

Signals allow certain senders to notify a set of receivers that some action has taken place.

Source : Django Documentation

Primer on Django signal

Signals allow us to perform a set of actions based on an event. The actions could range from sending an email (to welcoming users when they register) to anything.

In a use case where we want to send an email to welcome a user when they register on our website — when a user registers on our web application, it involves saving the user to our database— Saving the user is the event that triggers a signal. Sending an email is the action we will perform due to that signal that will be dispatched.

There are three entities to keep a tab on when using Django signals:

  1. The Signal
  2. The Sender
  3. The Receiver

• The Signal is the event that happened.

• The Sender is where the event happened or what sent the signal.

• The Receiver is the action that will be performed when Django sends the signal because the event occurred.

Types of signals

  1. Model Signal:

    These are signals sent by the Django model system whenever a specific event occurs on a particular model instance.

    They are defined in the django.db.models.signal module.

    The Model Signals include:

    pre_init :

    Whenever we instantiated a model instance, Django calls the model's .__init__() method.

    Django sends the pre_init signal after the .__init__() method starts executing.

    post_init :

    Sent after the .__init__() method finishes executing and the model instance is instantiated.

       book = Book(name = "The living Legend)
    

    The .__ init__() method is called when we instantiate the book model instance

    pre_save: To save a model after we instantiated it, we need to call the .save() method. Django dispatch the pre_save signal at the beginning of the execution of the .save() method and before the .save() method finishes executing.

    post_save: Django dispatch the post_save signal after the .save() method that we called on a model instance finished executing and the instance is saved successfully.

    For instance:

       book = Book(name = "The living Legend)
       book.save()
    

    The pre_save signal is sent before the save method finishes execution. The post_save signal sent after execution.

    pre_delete:

    If we want to delete a model instance, we need to call the .delete() method on the instance.

    Django dispatch the pre_delete signal after the .delete() method starts executing before the model instance is deleted.

    post_delete:

    Django will dispatch the post_delete signal after the delete method we called on a model instance finished executing.

     book.delete()
    
  2. Request-Response Signal:

    Whenever we access a page on a website from our browser, The browser sends a request to the server and the server sends back a Response. The Request-Response Signals are sent during the interaction between the Browser and the server.

    They are Signals specified in the core module at django.core.signal.

    They include :

    request_started: which is dispatched at the start of a request

    request_finished: which is dispatched at the end of a request.

    get_request_exception: dispatched whenever there is an exception during the client-server communication while processing a request

  3. Management Signal :

    Sent by the django-admin command and they include :

    pre_migrate: When we call the migrate command to create a database for our model, Django dispatch the pre_migrate signal.

    post_migrate: Dispatched after successful migration

  4. Test Signals: These signals are sent while running the test suite.

Let's take a broader look at the three entities I said to keep a tab on when using Django Signals:

Senders

Most of the signals enumerated above are triggered numerous times and in numerous places.

For instance, Any model instance we delete on our web application triggers the pre_delete and post_delete signal.

It might not be necessary to listen to every pre_delete and post_delete signal from all our models.

The sender specifies the particular model we want to listen to. If we want our web application to call a receiver function (more about the receiver function in a bit) whenever we delete a user, the sender, in this case, the sender will be the User Model.

The receiver function

The receiver function is the action we want to perform whenever a sender sends a signal.

def welcome_user(sender, **kwargs):
    print(welcome)

The receiver is a Python Function or Method. It takes a sender parameter and a compulsory wildcard keyword argument **kwargs.

Connecting a signal to a receiver function

Django specifies two different approaches to connecting a signal to a receiver function:

• Using the signals' connect method:

In this approach, you call the connect method of each signal and pass the receiver function as shown below for the post_save signal:

post_save.connect(send_welcome_email)

• Using the receiver decorator:

We can also use the receiver decorator in the django.dispatch module.

from django.dispatch import receiver
from django.db.models.signal import post_save

@receiver(post_save)
def send_welcome_email(sender, **kwargs):
    print("Welcome")

In the above snippet:

  1. We passed the post_save signal as an argument to the decorator

  2. We decorated the send_welcome_email with the receiver decorator.

The implementation above will call the receiver function when we save any model instance.

The receiver decorator has a sender argument that limits the source of the signal to a model or list of models.

Whenever we specify the sender, the receiver function will only respond to the signals from the model or a list of models we specified as the sender.

If we want to limit the source of a signal like the post_save signal to the User model, we will specify the sender as the user model:

@receiver(post_save, sender=User)
def send_welcome_email(sender, **kwargs):
    print("Welcome")

In this case, Django calls the receiver function whenever we save a User instance.

How to use various Django signal

To demonstrate the use of various Django signals that I discussed above, I will make use of the User model from django.contrib.auth.models and we will interact with the user model using the python shell

I will demonstrate the use of :

  • the post_save signal by sending a welcome email to a user we will create in seconds.
  • the pre_delete signal by sending an email notification to the user that we deleted his account.

For the other signals, we will print a custom message on the screen indicating which event is happening.

Email configuration

Since some of our to-dos revolve around sending an email message, we will need to add the email configuration that Django requires in the settings.py file before we can send an email from our web application.

The configuration will be specific for the email service provider (Host) which will be google in this case. Therefore Some of the configurations that I will do below are exclusive to the Gmail Simple Mail Transfer Protocol (SMTP) server provided by Google. If you like to use a different email service provider, feel free to do so. You can check the configuration for the provider that you want to use by searching for it on Google.

Google does not allow sending emails from an unsecured application like our application running on localhost unless we are using an app password with 2-step verification.

Follow the following procedure to set an app password and 2 step verification:

  1. log in to your Gmail
  2. Go to Manage Google Account: 20220615_172004

  3. Click on Security in the side pane: 20220615_172006

  4. Turn On 2 Steps Verification

  5. Click on App passwords
  6. Verify yourself by entering your password:

20220615_172706

  1. Click on the select app and select Others

  2. Enter localhost as the name of the app and click on GENERATE:

20220615_172709

  1. Copy and save the generated password:

20220615_172712

This password will allow us to authenticate our Gmail account from our Django application.

Django email setup

  1. Open settings.py

Add the following configuration to the settings.py:

EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "haryourdejijb@gmail.com"
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST_PASSWORD =’tiljfadvxhtcutoe’
EMAIL_PORT = 587

In the above configuration;

  • The EMAIL_HOST is the host that Django will use to send email messages. If you are using Gmail like I am, the host will be the google SMTP host. Set this to google’s SMTP address smtp.gmail.com

  • EMAIL_HOST_USER is the username to use for the SMTP host defined in the EMAIL_HOST. If a sender is not specified when we send an email, The value for this configuration will be used. I will use my Gmail username.

  • EMAIL_HOST_PASSWORD Django uses this in conjunction with the EMAIL_HOST_USER for authentication on the EMAIL_HOST server. I will set this to the app password we generated earlier.

  • EMAIL_TLS and SSL controls whether a secure connection will be used.

  • EMAIL_PORT is the SMTP HOST port. Set this to 587 as shown above.

  • EMAIL_BACKEND handles the actual sending of email with Django and smtp.Backend indicates that email will be sent through an SMTP server.

With the above configuration, we are ready to send emails to Django.

Creating the app

I have a project named BookApp I used in my technical article on Management Command.

I will use the project as a base project here. You are free to use any of your Django projects or create a new project.

From your project directory in the console, run the Django command to create an app called SignalApp or whatever you deem fit:

$ python manage.py createapp SignalApp

post_save signal

When we create a user instance, we need to save it by calling the .save() method. The save method will dispatch the pre_save and the post_save signals as I explained above.

Since we will be sending an email to welcome our user, we need to create a receiverfunction that listens to either of the pre_save and post_save signals that the .save() method will dispatch.

It is more appropriate to listen to the post_savesignal because anything could go wrong that would prevent our user from being saved after the .save() method dispatches the pre_save and before it dispatches the post_save signal. Listening to the post_save signal will ensure that Django sends the email message after the user we created is saved successfully.

To create the receiver function:

  1. Create a signals.py file in the SignalApp directory.

  2. Copy the code below in the file:

from django.db.models.signals import post_save, pre_delete, post_delete
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.core.mail import send_mail
from BookApp import settings

@receiver(post_save, sender=User)
def send_welcome_email(sender, **kwargs):
    if kwargs["created"]:
        subject = "Welcome Message"
        message = f"Hello {kwargs['instance'].username} !" \
                  f"You are welcome to our great website"
        recipient_list = [kwargs["instance"].email]

        send_mail(subject=subject,
                  message=message,
                  recipient_list= recipient_list,
                  fail_silently=False,
                  from_email=settings.EMAIL_HOST_USER
                  )

In the code above:

  1. I made all the necessary imports:

    • The post_save signal, which is the signal that our receiver function will listen to:
      from django.db.models.signals import post_save
      
    • The User model, which is the model we are expecting the signal form:
      from django.contrib.auth.models import User
      
    • The receiver decorator to connect the receiver function to the post_save signal:
      from django.dispatch import receiver
      
    • The send_email function that we will use to send the welcome email.
  2. I decorated the receiver function with the receiver decorator:

     @receiver(post_save, sender=User)
    

    The first argument to the decorator is the signal that the function will listen to which is the post_save signal in this case. The second argument is the Model that sent the signal (The User model).

  3. I created the send_welcome_email function which is the receiver function.

    As I said in the introductory section, the receiver function must take the sender and the wildcard argument **kwargs.

For the post_save signal, The wildcard kwargs argument has the following key-value items:

  • Instance: The model instance that sends the signal.
  • created: A boolean value indicating if the instance was created. Saving a model instance could be because we just created the instance or we updated one of its fields. The created key has a boolean value that indicates if we just created the model instance or if we updated one of the fields of the model instance.

  • updated_fields: This is a list of fields that are updated if the save() method dispatched the post_save signal because we updated the model instance.

  • signal: A signal object

In the function:

Since we only want to send the email message when we create the User Instance, We have to put the send_email function in the if block that checks if we created the object or we updated it.

If kwargs[‘created’]:

The line above checks if the instance is just created.

In the if block:

The Django's send_email function can be used to send an email message to a list of recipients. It takes some arguments including the :

subject: This is the subject of the Email and we set this to “Welcome Message” message: This is the message body we want to send to the new user that we just created.

I used the python f-string to include the username of the new user. This way, I can personalize the message. I got the username of the user instance from the instance key which is part of the kwargs argument. recipient_list:This is a list of the recipient email we want to send the email message.

Irrespective of the number of recipients, the recipient_list must be a list. In our case, we are sending the email to a single user which is the user instance we just created. We can get the email of the user from the instance key in the kwargs argument.

fail_silently = True will ensure that any error message generated while sending the email message is not printed to the screen.

Aside from these arguments, The send_email function takes among other arguments the auth_user and auth_password which are default to the EMAIL_HOST_USER and EMAIL_HOST_PASSWORD we set in the settings.py respectively. These arguments are required to authenticate to the SMTP host server.

With this, we have successfully connected the post_save signal to the receiver function.

To register the receiver function:

  1. Open the app.py file in SignalApp application
  2. Add the following method to the SignalappConfig class:
def ready(self):
    import SignalApp.signals

This makes sure the codes in the signals.py run when we start the Django application

Testing

Let's test if everything works. We will use the python shell to add new users:

  1. Open the python shell:
    $ python manage.py shell
    
  2. Import the User model forfromango.contrib.auth.models
    >>> from django.contrib.auth.models import User
    
  3. Create a User instance:

20220618_232105

The post_save signal is dispatched when we called the new_user.save(), which then calls the receiver function that listens to it ( send_welcome_email). If we check our email now, we should see the welcome message:

Screenshot from 2022-06-18 16-53-12

Holla !!!

Connecting other signals to a receiver function follows the same approach as shown above. Any form of logic can be in the body of the receiver function. It all depends on what action you want Django to do when a signal is dispatched!

Let's see for the pre_delete signal, andpost_delete signal.

post and pre_delete signal

While demonstrating the use of the post_delete and pre_delete signals, we will send an email for the pre_delete signal as well as print a message on the screen, and for the post_delete signal, we will only print a message on the screen.

To connect the pre_delete signal to a receiver_function, edit the signals.py file:

  1. Add the pre_delete signal to the list of imports from `django.db.models.signals:
    from django.db.models.signals import post_save, pre_delete
    
  2. Add the receiver function:

    def print_pre_delete_message(sender, **kwargs):
       subject = "Delete Message"
       message = f"Hello {kwargs['instance'].username} !" \
               f"Your account has been deleted from our great website because" \
               f" you violated one of our policies." \
               f"\n\n" \
               f"Thank you !"
       recipient_list = [kwargs["instance"].email]
       send_mail(subject=subject,
               message=message,
               recipient_list=recipient_list,
               fail_silently=False,
               from_email=settings.EMAIL_HOST_USER
               )
       print('This Message is sent from  the Pre delete signal')
       print(f"{kwargs['instance'].username} has been deleted")
    

    Since Django dispatch the pre_delete signal before deleting the User Instance, The instance is still available in the database and we can still perform any form of logic with the instance.

  3. Connect the signal to the receiver function:

As shown in the signal primer section, There is another way we can connect the signal to the receiver function, We won't use the receiver decorator. We will use the connect method from the pre_delete’ signal `.

pre_delete.connect(print_pre_delete_message, sender=User,)

The connect method takes the receiver function and it takes the sender argument if we want to limit the sender of the signal to a specific model.

That’s all we need to do.

For the post delete signal, we will simply print a message on the screen indicating a post_delete operation is taking place:

  1. Edit the signals.py to add the following:

  2. Add the post_delete signal to the list of imports from django.db.models.signals:

    from django.db.models.signals import post_save, pre_delete, post_delete
    
  3. Add the receiver function:

    def print_post_delete_message(sender, instance, **kwargs):
         print('This Message is sent from  the Post delete signal')
         print(f"{instance.username} has been deleted")
    

    In the case of the post_delete signal, Django dispatch the signal after the User Instance has been deleted. Therefore the instance is not available in the database. Though the instance variable is still available for use, So be wary of the logic you perform with the instance.

  4. Connect the signal to the receiver function:

    post_delete.connect(print_post_delete_message, sender=User,)
    

To test if this works, We will delete the User instance we just created.

In the python shell we opened earlier, call the delete method on the new_user instance:

20220618_214040

Screenshot from 2022-06-18 16-52-50

As seen above. We got the pre_delete message as well as the post_delete message. The order in which the messages are printed indicates the pre_delete signal was dispatched before the post_delete signal. If we check our email, we will also receive the email sent from the receiver of the pre_delete signal.

request-response signals

Django dispatches the request-response signals whenever we view a page. As discussed earlier, Django has the request_started signal and request_finished signal that is dispatched whenever a request to a URL route starts or finishes respectively.

There is no direct way to specify the request the receiver function to these signals will listen to. However, We can tweak the available options to get the receiver to listen to a specific request.

request-started signal

To demonstrate the use of the request_started signal, we will simply print a message to the screen indicating the Django view that sent the request. First, we need to create a simple view and its associated URL route so that we can send an HTTP request :

  1. Open the views.py file in the signal App app.
  2. Add the following code snippet in the view file:

    from django.http.response import HttpResponse
    
    def index(request):
       return HttpResponse("<h1> This is a signal Email </h1>")
    

    In the above code snippet, We imported the HttpResponse from django.http.response and we created an index view function that returns an HTTP Response with an HTML content.

  3. Open the urls.py in the SignalApp.

  4. Add the code snippet below in the urls.py file
from SignalApp.views import index

urlpatterns = [

    path("", index, name= 'index'),
    ...
]

We import the index view we created in the views.py file of the SignalApp app and we added a URL path that maps to the view function in the list of URL patterns.

Now that we have the view and the URL route, let's create the receiver function for the request started signal:

  1. Open the signals.py file in the SignalApp app
  2. Import the request_started signal from django.core.signal module:
    from django.core.signals import request_started
    
  3. Create a receiver function for the request_started signal:
    @receiver(request_started)
    def print_request_started(sender,environ, **kwargs):
       if environ["PATH_INFO"] == "/":
           print("This Message is from the request started signal to the Home page")
    

The request_started signal sends among others, the sender and the environ arguments to the receiver function.

The sender is the handler class that handled the request (not the view function). The environ argument is Metadata about the HTTP request and other environment variables. Since we cannot specifically associate the request_started signal with the specific view, we can however access the PATH_INFO key of the environ argument of the receiver function. This PATH_INFO key stores the path segment of the URL associated with each Django view. We can then use an If - else block to check the value of the PATH_INFO key against the path segment associated with a particular view. In this case, The path segment associated with our URL is empty.

If we navigate to the index page URL, the message we specified in the print function in the if block is printed:

20220618_214133

The request_finish signal works the same way as the request_started signal. I will leave it out for you to try out as an exercise.

Conclusion

The Django signal is a very powerful feature you can leverage while building a web application. In this article, you learned about various Django signals and how to use them. As a bonus, you also learned how to send emails with Django.

Referenced Materials for this write-up: