Search code examples
pythondjangodjango-modelsdjango-channelsdjango-signals

Django signals - how do I send what was saved in the model using post_save?


Triying to use signals to send through websockets the last record that was saved using .save(). What do I put in data?

#models.py
from django.db import models
from django.db.models.signals import post_save
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

class DataModel(models.Model):
    time = models.DateTimeField()
    value = models.FloatField()

    def __str__(self):
        return str(self.time)

def save_post(sender, instance, **kwargs):
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        "echo_group",
        {"type": "on.message", "data": data},
    )

post_save.connect(save_post, sender=DataModel)

I guess I could just fetch the record with the highest index and send it, but wondering if there's a more elegant solution.


Solution

  • Prelude: deal with channel hydration

    To know what's last modified, you would need a last modified field. This is a very well-known pattern and Django uses the auto_now=True argument to DateTimeFields to help with it. These fields are not editable and use Database triggers wherever possible to achieve the result (which is why they're not editable).

    As said this is common, so I usually use a base model:

    class AuditableBase(models.Model):
        """
        Base class that adds created_at and last_modified fields for audit purposes.
        """
    
        created_at = models.DateTimeField(auto_now_add=True)
        last_modified = models.DateTimeField(auto_now=True)
    
        class Meta:
            abstract = True
    

    Now it's trivial to get the last modified record and hydrate a channel with the last modified, irrespective of whether save occurred:

    class DataModel(AuditableBase):
        time = models.DateTimeField()
        value = models.FloatField()
    
        def __str__(self):
            return str(self.time)
    
    # On channel start:
    latest = DataModel.objects.latest('last_modified')
    

    Signal handler

    However, if we use the post save signal, we already have the object that was just saved, in the "instance" argument. To convert it to json easily, we can use model_to_dict and DjangoJSONEncoder to deal with most problems:

    from django.forms.models import model_to_dict
    from django.core.serializers import DjangoJSONEncoder
    import json
    
    def save_post(sender, instance, **kwargs):
        channel_layer = get_channel_layer()
        data = model_to_dict(instance)
        json_data = json.dumps(data, cls=DjangoJSONEncoder)
        async_to_sync(channel_layer.group_send)(
            "echo_group",
            {"type": "on.message", "data": json_data},
        )
    

    Model_to_dict will convert a model to a dictionary and can be restricted using fields= (explicit include) or exclude= (explicit exclude). DjangoJSONEncoder deals with temporal data, that is not supported by json's default encoder.