Django: при сохранении, как вы можете проверить, изменилось ли поле?

В моей модели у меня есть :

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
 used when the alias is made")
    image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")


    def save(self, *args, **kw):
        if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
            try :
                data = utils.fetch(self.remote_image)
                image = StringIO.StringIO(data)
                image = Image.open(image)
                buf = StringIO.StringIO()
                image.save(buf, format='PNG')
                self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
            except IOError :
                pass

, которое отлично работает в первый раз remote_image изменения.

как я могу получить новое изображение, когда кто-то изменил remote_image на псевдоним? А во-вторых, есть ли лучший способ кэшировать удаленное изображение?

22 ответов


хотя это немного поздно, позвольте мне выбросить это решение для других, которые сталкиваются с этим сообщением. По сути, вы хотите переопределить __init__ метод models.Model чтобы сохранить копию исходного значения. Это делает его таким, что вам не нужно делать другой поиск БД (что всегда хорошо).

class Person(models.Model):
  name = models.CharField()

  __original_name = None

  def __init__(self, *args, **kwargs):
    super(Person, self).__init__(*args, **kwargs)
    self.__original_name = self.name

  def save(self, force_insert=False, force_update=False, *args, **kwargs):
    if self.name != self.__original_name:
      # name changed - do something here

    super(Person, self).save(force_insert, force_update, *args, **kwargs)
    self.__original_name = self.name

Я использую следующий 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])

использование:

>>> 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)
>>>

Примечание

обратите внимание, что это решение хорошо работает только в контексте текущего запроса. Таким образом, он подходит в первую очередь для простых случаев. В параллельной среде, где несколько запросов могут одновременно управлять одним и тем же экземпляром модели, вам определенно нужен другой подход.


и теперь для прямого ответа: один из способов проверить, изменилось ли значение поля, - это получить исходные данные из базы данных перед сохранением экземпляра. Рассмотрим следующий пример:

class MyModel(models.Model):
    f1 = models.CharField(max_length=1)

    def save(self, *args, **kw):
        if self.pk is not None:
            orig = MyModel.objects.get(pk=self.pk)
            if orig.f1 != self.f1:
                print 'f1 changed'
        super(MyModel, self).save(*args, **kw)

то же самое относится и к работе с формой. Вы можете обнаружить его в методе clean или save модели:

class MyModelForm(forms.ModelForm):

    def clean(self):
        cleaned_data = super(ProjectForm, self).clean()
        #if self.has_changed():  # new instance or existing updated (form has data to save)
        if self.instance.pk is not None:  # new instance only
            if self.instance.f1 != cleaned_data['f1']:
                print 'f1 changed'
        return cleaned_data

    class Meta:
        model = MyModel
        exclude = []

лучший способ-это pre_save сигнал. Возможно, это не было вариантом еще в ' 09, когда этот вопрос был задан и ответил, но любой, кто видит это сегодня, должен сделать это так:

@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

Так как Django 1.8 выпущен, вы можете использовать from_db classmethod для кэширования старого значения remote_image. Потом в сохранить метод вы можете сравнить старое и новое значение поля, чтобы проверить, изменилось ли значение.

@classmethod
def from_db(cls, db, field_names, values):
    new = super(Alias, cls).from_db(db, field_names, values)
    # cache value went from the base
    new._loaded_remote_image = values[field_names.index('remote_image')]
    return new

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    if (self._state.adding and self.remote_image) or \
        (not self._state.adding and self._loaded_remote_image != self.remote_image):
        # If it is first save and there is no cached remote_image but there is new one, 
        # or the value of remote_image has changed - do your stuff!

обратите внимание, что отслеживание изменений полей доступно в django-model-utils.

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


Если вы используете форму, вы можете использовать формы changed_data (docs):

class AliasForm(ModelForm):

    def save(self, commit=True):
        if 'remote_image' in self.changed_data:
            # do things
            remote_image = self.cleaned_data['remote_image']
            do_things(remote_image)
        super(AliasForm, self).save(commit)

    class Meta:
        model = Alias

начиная с Django 1.8, есть from_db метод, как упоминает Серж. Фактически, документы Django включают этот конкретный случай использования в качестве примера:

https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading

Ниже приведен пример записи начальных значений полей, загружаемых из базы данных


Я немного опоздала на вечеринку, но я нашел это решение: Джанго Грязные Поля


можно использовать django-model-changes сделать это без дополнительного поиска по базе данных:

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

оптимальным решением, вероятно, является решение, которое не включает дополнительную операцию чтения базы данных до сохранения экземпляра модели, а также любую дальнейшую библиотеку django. Вот почему решения лаффюста предпочтительнее. В контексте сайта администратора можно просто переопределить save_model-метод и вызвать метод has_changed формы там, как и в ответе Sion выше. Вы приходите к чему-то вроде этого, опираясь на пример настройки Sion, но используя "changed_data", чтобы получить все возможное изменить:

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)
  • перезаписать save_model:

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

  • встроенный changed_data-метод для поля:

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


хотя это на самом деле не отвечает на ваш вопрос, я бы пошел об этом по-другому.

просто очистите


у меня была такая ситуация, прежде чем мое решение было переопределить pre_save() метод класса целевого поля будет вызываться только в том случае, если поле было изменено
полезно с FileField пример:

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

недостаток:
не полезно, если вы хотите выполнить какую-либо операцию (post_save), например, использовать созданный объект в каком-либо задании (если определенное поле изменилось)


улучшение @josh ответ для всех полей:

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)

чтобы уточнить, getattr работает, чтобы получить поля, такие как person.name со строками (т. е. getattr(person, "name")


это работает для меня в Django 1.8

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something

еще один поздний ответ, но если вы просто пытаетесь увидеть, был ли новый файл загружен в поле файла, попробуйте следующее: (адаптировано из комментария Кристофера Адамса по ссылке http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ в комментарии Зака здесь)

обновленная ссылка: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed/

def save(self, *args, **kw):
    from django.core.files.uploadedfile import UploadedFile
    if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
        # Handle FileFields as special cases, because the uploaded filename could be
        # the same as the filename that's already there even though there may
        # be different file contents.

        # if a file was just uploaded, the storage model with be UploadedFile
        # Do new file stuff here
        pass

я расширил mixin @livskiy следующим образом:

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

и DictField это:

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)

его можно использовать путем расширять его в ваших моделях поле _dict будет добавлено при синхронизации / миграции, и это поле будет хранить состояние ваших объектов


в качестве расширения ответа SmileyChris вы можете добавить поле datetime в модель для last_updated и установить какой-то предел для максимального возраста, к которому вы позволите ему добраться, прежде чем проверять изменение


миксин от @ivanlivski отличный.

я расширил его до

  • убедиться, что он работает с десятичными полями.
  • Expose свойства для упрощения использования

обновленный код здесь: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

чтобы помочь людям, новым для Python или Django, я приведу более полный пример. Это определенное использование принять a файл от поставщика данных и убедитесь, что записи в базе данных отражают файл.

мой объектной модели:

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

класс, который загружает файл имеет следующие методы:

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()

Как насчет использования решения Дэвида Крамера:

http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/

У меня был успех, используя его так:

@track_data('name')
class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.CharField(max_length=5)

    def save(self, *args, **kwargs):
        if self.has_changed('name'):
            print 'name changed'

    # OR #

    @classmethod
    def post_save(cls, sender, instance, created, **kwargs):
        if instance.has_changed('name'):
            print "Hooray!"

модификация ответа @ivanperelivskiy:

@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

это использует открытый метод django 1.10 get_fields вместо. Это делает код более будущим доказательством, но что более важно, также включает внешние ключи и поля, где editable=False.

Для справки, вот реализация .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))
    )

вот еще один способ сделать это.

class Parameter(models.Model):                                                   

def __init__(self, *args, **kwargs):                                         
    super(Parameter, self).__init__(*args, **kwargs)                         
    self.__original_value = self.value                                       

def clean(self,*args,**kwargs):                                              
    if self.__original_value == self.value:                                  
        print("igual")                                                       
    else:                                                                    
        print("distinto")                                                    

def save(self,*args,**kwargs):                                               
    self.full_clean()                                                        
    return super(Parameter, self).save(*args, **kwargs)                      
    self.__original_value = self.value                                       

key = models.CharField(max_length=24, db_index=True, unique=True)
value = models.CharField(max_length=128)                    

согласно документации: проверка объектов

"второй шаг full_clean() выполняет вызов модели.чистый.)( Этот метод следует переопределить для выполнения пользовательской проверки модели. Этот метод следует использовать для проверки пользовательской модели и при необходимости изменять атрибуты модели. Например, вы можете использовать его для автоматического предоставления значения для поля или для проверки для этого требуется доступ к более чем одному полю:"