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

前端 未结 25 1769
鱼传尺愫
鱼传尺愫 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:35

    The optimal solution is probably one that does not include an additional database read operation prior to saving the model instance, nor any further django-library. This is why laffuste's solutions is preferable. In the context of an admin site, one can simply override the save_model-method, and invoke the form's has_changed method there, just as in Sion's answer above. You arrive at something like this, drawing on Sion's example setting but using changed_data to get every possible change:

    class ModelAdmin(admin.ModelAdmin):
       fields=['name','mode']
       def save_model(self, request, obj, form, change):
         form.changed_data #output could be ['name']
         #do somethin the changed name value...
         #call the super method
         super(self,ModelAdmin).save_model(request, obj, form, change)
    
    • Override save_model:

    https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

    • Built-in changed_data-method for a Field:

    https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data

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

    I had this situation before my solution was to override the pre_save() method of the target field class it will be called only if the field has been changed
    useful with FileField example:

    class PDFField(FileField):
        def pre_save(self, model_instance, add):
            # do some operations on your file 
            # if and only if you have changed the filefield
    

    disadvantage:
    not useful if you want to do any (post_save) operation like using the created object in some job (if certain field has changed)

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

    A modification to @ivanperelivskiy's answer:

    @property
    def _dict(self):
        ret = {}
        for field in self._meta.get_fields():
            if isinstance(field, ForeignObjectRel):
                # foreign objects might not have corresponding objects in the database.
                if hasattr(self, field.get_accessor_name()):
                    ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
                else:
                    ret[field.get_accessor_name()] = None
            else:
                ret[field.attname] = getattr(self, field.attname)
        return ret
    

    This uses django 1.10's public method get_fields instead. This makes the code more future proof, but more importantly also includes foreign keys and fields where editable=False.

    For reference, here is the implementation of .fields

    @cached_property
    def fields(self):
        """
        Returns a list of all forward fields on the model and its parents,
        excluding ManyToManyFields.
    
        Private API intended only to be used by Django itself; get_fields()
        combined with filtering of field properties is the public API for
        obtaining this field list.
        """
        # For legacy reasons, the fields property should only contain forward
        # fields that are not private or with a m2m cardinality. Therefore we
        # pass these three filters as filters to the generator.
        # The third lambda is a longwinded way of checking f.related_model - we don't
        # use that property directly because related_model is a cached property,
        # and all the models may not have been loaded yet; we don't want to cache
        # the string reference to the related_model.
        def is_not_an_m2m_field(f):
            return not (f.is_relation and f.many_to_many)
    
        def is_not_a_generic_relation(f):
            return not (f.is_relation and f.one_to_many)
    
        def is_not_a_generic_foreign_key(f):
            return not (
                f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
            )
    
        return make_immutable_fields_list(
            "fields",
            (f for f in self._get_fields(reverse=False)
             if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
        )
    
    0 讨论(0)
  • 2020-11-22 07:35

    The mixin from @ivanlivski is great.

    I've extended it to

    • Ensure it works with Decimal fields.
    • Expose properties to simplify usage

    The updated code is available here: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

    To help people new to Python or Django, I'll give a more complete example. This particular usage is to take a file from a data provider and ensure the records in the database reflect the file.

    My model object:

    class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
        station_name = models.CharField(max_length=200)
        nearby_city = models.CharField(max_length=200)
    
        precipitation = models.DecimalField(max_digits=5, decimal_places=2)
        # <list of many other fields>
    
       def is_float_changed (self,v1, v2):
            ''' Compare two floating values to just two digit precision
            Override Default precision is 5 digits
            '''
            return abs (round (v1 - v2, 2)) > 0.01
    

    The class that loads the file has these methods:

    class UpdateWeather (object)
        # other methods omitted
    
        def update_stations (self, filename):
            # read all existing data 
            all_stations = models.Station.objects.all()
            self._existing_stations = {}
    
            # insert into a collection for referencing while we check if data exists
            for stn in all_stations.iterator():
                self._existing_stations[stn.id] = stn
    
            # read the file. result is array of objects in known column order
            data = read_tabbed_file(filename)
    
            # iterate rows from file and insert or update where needed
            for rownum in range(sh.nrows):
                self._update_row(sh.row(rownum));
    
            # now anything remaining in the collection is no longer active
            # since it was not found in the newest file
            # for now, delete that record
            # there should never be any of these if the file was created properly
            for stn in self._existing_stations.values():
                stn.delete()
                self._num_deleted = self._num_deleted+1
    
    
        def _update_row (self, rowdata):
            stnid = int(rowdata[0].value) 
            name = rowdata[1].value.strip()
    
            # skip the blank names where data source has ids with no data today
            if len(name) < 1:
                return
    
            # fetch rest of fields and do sanity test
            nearby_city = rowdata[2].value.strip()
            precip = rowdata[3].value
    
            if stnid in self._existing_stations:
                stn = self._existing_stations[stnid]
                del self._existing_stations[stnid]
                is_update = True;
            else:
                stn = models.Station()
                is_update = False;
    
            # object is new or old, don't care here            
            stn.id = stnid
            stn.station_name = name;
            stn.nearby_city = nearby_city
            stn.precipitation = precip
    
            # many other fields updated from the file 
    
            if is_update == True:
    
                # we use a model mixin to simplify detection of changes
                # at the cost of extra memory to store the objects            
                if stn.has_changed == True:
                    self._num_updated = self._num_updated + 1;
                    stn.save();
            else:
                self._num_created = self._num_created + 1;
                stn.save()
    
    0 讨论(0)
  • 2020-11-22 07:36

    I use following mixin:

    from django.forms.models import model_to_dict
    
    
    class ModelDiffMixin(object):
        """
        A model mixin that tracks model fields' values and provide some useful api
        to know what fields have been changed.
        """
    
        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.
            """
            super(ModelDiffMixin, self).save(*args, **kwargs)
            self.__initial = self._dict
    
        @property
        def _dict(self):
            return model_to_dict(self, fields=[field.name for field in
                                 self._meta.fields])
    

    Usage:

    >>> p = Place()
    >>> p.has_changed
    False
    >>> p.changed_fields
    []
    >>> p.rank = 42
    >>> p.has_changed
    True
    >>> p.changed_fields
    ['rank']
    >>> p.diff
    {'rank': (0, 42)}
    >>> p.categories = [1, 3, 5]
    >>> p.diff
    {'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
    >>> p.get_field_diff('categories')
    (None, [1, 3, 5])
    >>> p.get_field_diff('rank')
    (0, 42)
    >>>
    

    Note

    Please note that this solution works well in context of current request only. Thus it's suitable primarily for simple cases. In concurrent environment where multiple requests can manipulate the same model instance at the same time, you definitely need a different approach.

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

    Note that field change tracking is available in django-model-utils.

    https://django-model-utils.readthedocs.org/en/latest/index.html

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