确定 django post_save 信号中已更改的字段

我使用 django 的 post _ save 信号在保存模型之后执行一些语句。

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

现在我想根据 mode字段的值是否更改来执行一个语句。

@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

我看了一些 SOF 的帖子和一个博客,但是找不到解决这个问题的方法。他们都试图使用 pre _ save 方法或表单,这不是我的用例。Django 文档中的 https://docs.djangoproject.com/es/1.9/ref/signals/#post-save没有提到这样做的直接方法。

下面的链接中的一个答案看起来很有希望,但我不知道如何使用它。我不确定最新的 django 版本是否支持它,因为我使用了 ipdb来调试它,发现 instance变量没有下面答案中提到的属性 has_changed

Django: 保存时,如何检查字段是否已更改?

50136 次浏览

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

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.

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.

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.

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

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