Identify the changed fields in django post_save signal

Question:

I’m using django’s post_save signal to execute some statements after saving the model.

class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.BooleanField()


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

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
        # do some stuff
        pass

Now I want to execute a statement based on whether the value of the mode field has changed or not.

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
        # if value of `mode` has changed:
        #  then do this
        # else:
        #  do that
        pass

I looked at a few SOF threads and a blog but couldn’t find a solution to this. All of them were trying to use the pre_save method or form which are not my use case. https://docs.djangoproject.com/es/1.9/ref/signals/#post-save in the django docs doesn’t mention a direct way to do this.

An answer in the link below looks promising but I don’t know how to use it. I’m not sure if the latest django version supports it or not, because I used ipdb to debug this and found that the instance variable has no attribute has_changed as mentioned in the below answer.

Django: When saving, how can you check if a field has changed?

Asked By: Wendy

||

Answers:

Set it up on the __init__ of your model so you’ll have access to it.

def __init__(self, *args, **kwargs):
    super(YourModel, self).__init__(*args, **kwargs)
    self.__original_mode = self.mode

Now you can perform something like:

if instance.mode != instance.__original_mode:
    # do something useful
Answered By: scoopseven

Ussually it’s better to override the save method than using signals.

From Two scoops of django:
"Use signals as a last resort."

I agree with @scoopseven answer about caching the original value on the init, but overriding the save method if it’s possible.

class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.BooleanField()
    __original_mode = None

    def __init__(self, *args, **kwargs):
        super(Mode, self).__init__(*args, **kwargs)
        self.__original_mode = self.mode

    def save(self, force_insert=False, force_update=False, *args, **kwargs):
        if self.mode != self.__original_mode:
            #  then do this
        else:
            #  do that

        super(Mode, self).save(force_insert, force_update, *args, **kwargs)
        self.__original_mode = self.mode

UPDATE IF YOU NEED SIGNALS

Just in case you really need signals because you need a decoupled app or you can’t simply override the save() method, you can use pre_save signal to ‘watch’ previous fields

@receiver(pre_save, sender=Mode)
def check_previous_mode(sender, instance, *args, **kwargs):
    original_mode = None
    if instance.id:
        original_mode = Mode.objects.get(pk=instance.id).mode
    
    if instance.mode != original_mode:
        #  then do this
    else:
        #  do that

The problem with this is that you make changes before, so if save() has a problem you could have some issues later.
So to fix that issue, you can store the original value on the pre_save and use on post_save.

@receiver(pre_save, sender=Mode)
def cache_previous_mode(sender, instance, *args, **kwargs):
    original_mode = None
    if instance.id:
        original_mode = Mode.objects.get(pk=instance.id).mode
    
    instance.__original_mode = original_mode:

@receiver(post_save, sender=Mode)
def post_save_mode_handler(sender, instance, created, **kwargs):
    if instance.__original_mode != instance.original_mode:
        #  then do this
    else:
        #  do that

The problem with signals and this approach also is that you need one more query to check previous values.

Answered By: educolo

If you want to compare state before and after save action, you can use pre_save signal which provide you instance as it should become after database update and in pre_save you can read current state of instance in database and perform some actions based on difference.

from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=MyModel)
def on_change(sender, instance: MyModel, **kwargs):
    if instance.id is None: # new object will be created
        pass # write your code here
    else:
        previous = MyModel.objects.get(id=instance.id)
        if previous.field_a != instance.field_a: # field will be updated
            pass  # write your code here

This is an old question but I’ve come across this situation recently and I accomplished it by doing the following:

    class Mode(models.Model):
    
        def save(self, *args, **kwargs):
            if self.pk:
                # If self.pk is not None then it's an update.
                cls = self.__class__
                old = cls.objects.get(pk=self.pk)
                # This will get the current model state since super().save() isn't called yet.
                new = self  # This gets the newly instantiated Mode object with the new values.
                changed_fields = []
                for field in cls._meta.get_fields():
                    field_name = field.name
                    try:
                        if getattr(old, field_name) != getattr(new, field_name):
                            changed_fields.append(field_name)
                    except Exception as ex:  # Catch field does not exist exception
                        pass
                kwargs['update_fields'] = changed_fields
            super().save(*args, **kwargs)

This is more effective since it catches all updates/saves from apps and django-admin.

Answered By: Redgren Grumbholdt

in post_save method you have kwargs argument that is a dictionary and hold some information. You have update_fields in kwargs that tell you what fields changed. This fields stored as forzenset object. You can check what fields changed like this:

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
    if not created:
        for item in iter(kwargs.get('update_fields')):
            if item == 'field_name' and instance.field_name == "some_value":
               # do something here

But there is an issue in this solution. If your field value for example was 10, and you update this field with 10 again, this field will be in update_fields again.

Answered By: Saber Solooki

I’m late but it can be helpful for others.

We can make custom signal for this.

Using custom signal we can easily do these kind of things:

  1. Post is created or not
  2. Post is modified or not
  3. Post is saved but any field does not changed

   class Post(models.Model):
   # some fields 

Custom signals

**Make signal with arguments **

from django.dispatch import Signal, receiver
# provide arguments for your call back function
post_signal = Signal(providing_args=['sender','instance','change','updatedfields'])

Register signal with call back function

# register your signal with receiver decorator 
@receiver(post_signal)
def post_signalReciever(sender,**kwargs):
    print(kwargs['updatedfields'])
    print(kwargs['change'])

Sending the signal from post-admin

We sending the signals from Post admin and also save object when it actually modified

#sending the signals 
class PostAdmin(admin.ModelAdmin):
   # filters or fields goes here 

   #save method 
   def save_model(self, request, obj, form, change):


    if not change and form.has_changed():  # new  post created
        super(PostAdmin, self).save_model(request, obj, form, change)
        post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
        print('Post created')
    elif change and form.has_changed(): # post is actually modified )
        super(PostAdmin, self).save_model(request, obj, form, change)
        post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
        print('Post modified')
    elif change and not form.has_changed() :
        print('Post not created or not updated only saved ')  

See also:

Django Signals official doc

You can use update_fields in django signals.

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):

    # only update instance
    if not created:

        update_fields = kwargs.get('update_fields') or set()

        # value of `mode` has changed:
        if 'mode' in update_fields:
            # then do this
            pass
        else:
            # do that
            pass
Answered By: J.C

This can be identified using instance._state.adding

if not instance._state.adding:
    # update to existing record
    do smthng

else:
    # new object insert operation
    do smthng
Answered By: krishna kanth