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

前端 未结 25 1715
鱼传尺愫
鱼传尺愫 2020-11-22 07:15

In my model I have :

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text=\"A URL that is downloaded and cached          


        
相关标签:
25条回答
  • 2020-11-22 07:37

    Best way is with a pre_save signal. May not have been an option back in '09 when this question was asked and answered, but anyone seeing this today should do it this way:

    @receiver(pre_save, sender=MyModel)
    def do_something_if_changed(sender, instance, **kwargs):
        try:
            obj = sender.objects.get(pk=instance.pk)
        except sender.DoesNotExist:
            pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
        else:
            if not obj.some_field == instance.some_field: # Field has changed
                # do something
    
    0 讨论(0)
  • 2020-11-22 07:38

    I am a bit late to the party but I found this solution also: Django Dirty Fields

    0 讨论(0)
  • 2020-11-22 07:39

    You can use django-model-changes to do this without an additional database lookup:

    from django.dispatch import receiver
    from django_model_changes import ChangesMixin
    
    class Alias(ChangesMixin, MyBaseModel):
       # your model
    
    @receiver(pre_save, sender=Alias)
    def do_something_if_changed(sender, instance, **kwargs):
        if 'remote_image' in instance.changes():
            # do something
    
    0 讨论(0)
  • 2020-11-22 07:39

    There is an attribute __dict__ which have all the fields as the keys and value as the field values. So we can just compare two of them

    Just change the save function of model to the function below

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is not None:
            initial = A.objects.get(pk=self.pk)
            initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
            initial_json.pop('_state'), final_json.pop('_state')
            only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
            print(only_changed_fields)
        super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
    

    Example Usage:

    class A(models.Model):
        name = models.CharField(max_length=200, null=True, blank=True)
        senior = models.CharField(choices=choices, max_length=3)
        timestamp = models.DateTimeField(null=True, blank=True)
    
        def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
            if self.pk is not None:
                initial = A.objects.get(pk=self.pk)
                initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
                initial_json.pop('_state'), final_json.pop('_state')
                only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
                print(only_changed_fields)
            super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
    

    yields output with only those fields that have been changed

    {'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
    
    0 讨论(0)
  • 2020-11-22 07:41

    improving @josh answer for all fields:

    class Person(models.Model):
      name = models.CharField()
    
    def __init__(self, *args, **kwargs):
        super(Person, self).__init__(*args, **kwargs)
        self._original_fields = dict([(field.attname, getattr(self, field.attname))
            for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])
    
    def save(self, *args, **kwargs):
      if self.id:
        for field in self._meta.local_fields:
          if not isinstance(field, models.ForeignKey) and\
            self._original_fields[field.name] != getattr(self, field.name):
            # Do Something    
      super(Person, self).save(*args, **kwargs)
    

    just to clarify, the getattr works to get fields like person.name with strings (i.e. getattr(person, "name")

    0 讨论(0)
  • 2020-11-22 07:41

    I have extended the mixin of @livskiy as follows:

    class ModelDiffMixin(models.Model):
        """
        A model mixin that tracks model fields' values and provide some useful api
        to know what fields have been changed.
        """
        _dict = DictField(editable=False)
        def __init__(self, *args, **kwargs):
            super(ModelDiffMixin, self).__init__(*args, **kwargs)
            self._initial = self._dict
    
        @property
        def diff(self):
            d1 = self._initial
            d2 = self._dict
            diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
            return dict(diffs)
    
        @property
        def has_changed(self):
            return bool(self.diff)
    
        @property
        def changed_fields(self):
            return self.diff.keys()
    
        def get_field_diff(self, field_name):
            """
            Returns a diff for field if it's changed and None otherwise.
            """
            return self.diff.get(field_name, None)
    
        def save(self, *args, **kwargs):
            """
            Saves model and set initial state.
            """
            object_dict = model_to_dict(self,
                   fields=[field.name for field in self._meta.fields])
            for field in object_dict:
                # for FileFields
                if issubclass(object_dict[field].__class__, FieldFile):
                    try:
                        object_dict[field] = object_dict[field].path
                    except :
                        object_dict[field] = object_dict[field].name
    
                # TODO: add other non-serializable field types
            self._dict = object_dict
            super(ModelDiffMixin, self).save(*args, **kwargs)
    
        class Meta:
            abstract = True
    

    and the DictField is:

    class DictField(models.TextField):
        __metaclass__ = models.SubfieldBase
        description = "Stores a python dict"
    
        def __init__(self, *args, **kwargs):
            super(DictField, self).__init__(*args, **kwargs)
    
        def to_python(self, value):
            if not value:
                value = {}
    
            if isinstance(value, dict):
                return value
    
            return json.loads(value)
    
        def get_prep_value(self, value):
            if value is None:
                return value
            return json.dumps(value)
    
        def value_to_string(self, obj):
            value = self._get_val_from_obj(obj)
            return self.get_db_prep_value(value)
    

    it can be used by extending it in your models a _dict field will be added when you sync/migrate and that field will store the state of your objects

    0 讨论(0)
提交回复
热议问题