Identify the changed fields in django post_save signal

PythonDjangoDjango ModelsDjango Signals

Python Problem Overview


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.

https://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed/28094442#28094442

Python Solutions


Solution 1 - Python

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

Solution 2 - Python

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

Solution 3 - Python

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

Solution 4 - Python

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.

Solution 5 - Python

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.

Solution 6 - Python

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

Solution 7 - Python

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

Solution 8 - Python

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionWendyView Question on Stackoverflow
Solution 1 - PythoneducoloView Answer on Stackoverflow
Solution 2 - PythonRyabchenko AlexanderView Answer on Stackoverflow
Solution 3 - PythonscoopsevenView Answer on Stackoverflow
Solution 4 - PythonRedgren GrumbholdtView Answer on Stackoverflow
Solution 5 - PythonSaber SolookiView Answer on Stackoverflow
Solution 6 - PythonMuhammad Faizan FareedView Answer on Stackoverflow
Solution 7 - Pythonkrishna kanthView Answer on Stackoverflow
Solution 8 - PythonJ.CView Answer on Stackoverflow