djangodjango-rest-frameworkdjango-channels

How to create a Django decorator that creates a signal for a model?


I want to create a custom decorator in Django that I can put on my model, and it creates a signal with my business logic. The logic is the same for each signal, the only thing changing is the model it is being attached to. I am having trouble understanding how to accomplish this.

Given a model such as:

class Post(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

I ideally would want a functionality with my decorator to put it on my model such as:

@custom_signal_decorator()
class Post(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

And it would create a signal such as this one:

@receiver(post_save, sender=sender)
def notify_created(sender, instance, created, **kwargs):
    channel_layer = channels.layers.get_channel_layer()
    group_name = f'notifications_{instance.company.id}'
    async_to_sync(channel_layer.group_send)(
        group_name,
        {
            "type": "notify_change",
        }
    )

The end goal is tracking changes in real time by sending a notification to the frontend when a new post is created. This all works when creating a signal by hand for each model, but I feel like a decorator would make more sense, to put on models we want to track.

Any kind of input to how I would go about implementing this would be helpful, or another way that you think might work as well without the use of a decorator. Thank you.

I tried creating a custom decorator on my own, but I cannot figure out how to dynamically send the model inside the decorator so that it tracks that model, that's mainly where I don't understand how it works.


Solution

  • You can make a decorator that uses the passed model class to register the signal:

    from django.db.models.signals import post_save
    
    
    def notify_created(sender, instance, created, **kwargs):
        channel_layer = channels.layers.get_channel_layer()
        group_name = f'notifications_{instance.company.id}'
        async_to_sync(channel_layer.group_send)(
            group_name,
            {
                'type': 'notify_change',
            },
        )
    
    
    def custom_signal_decorator(model):
        post_save.connect(notify_created, sender=model)
        return model

    and use it like:

    @custom_signal_decorator
    class Post(models.Model):
        company = models.ForeignKey(Company, on_delete=models.CASCADE)
        title = models.CharField(max_length=100)

    We can check if the model contains a company_id field and thus prevent adding the decorator to a model that is incompatible with the signal:

    def custom_signal_decorator(model):
        model._meta.get_field('company_id')
        post_save.connect(notify_created, sender=model)
        return model

    Note: You can improve the performance of querying the primary key of a related object by using company_id instead of company.id. This will directly access the value stored in the model, and thus saves an extra roundtrip to the database.