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.
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. This will directly access the value stored in the model, and thus saves an extra roundtrip to the database.company.id