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

293

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

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псевдоним? А во-вторых, есть ли лучший способ кэширования удаленного изображения?

Пол Тарьян
источник

Ответы:

424

По сути, вы хотите переопределить __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
мистифицировать
источник
24
вместо перезаписи init я бы использовал post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo
22
Переопределение методов рекомендуется в документации Django: docs.djangoproject.com/en/dev/topics/db/models/…
полковник Спонсз
10
@callum, так что если вы вносите изменения в объект, сохраняете его, затем вносите дополнительные изменения и вызываете save()его снова, он все равно будет работать правильно.
Philfreo
17
@ Джош не будет проблем, если у вас есть несколько серверов приложений, работающих с одной и той же базой данных, поскольку она только отслеживает изменения в памяти
Jens Alm
13
@lajarre, я думаю, что ваш комментарий немного вводит в заблуждение. Документы предполагают, что вы позаботитесь, когда будете это делать. Они не рекомендуют против этого.
Джош
199

Я использую следующий миксин:

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

Заметка

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

iperelivskiy
источник
4
Действительно идеально, и не выполнять дополнительный запрос. Большое спасибо !
Стефан
28
+1 за использование mixin. +1 для дополнительного попадания в БД. +1 за много полезных методов / свойств. Я должен быть в состоянии поднять голос несколько раз.
Джейк
Да. Плюс один за использование Mixin и без лишних ударов.
Дэвид С
2
Mixin хорош, но эта версия имеет проблемы при использовании вместе с .only (). Вызов Model.objects.only ('id') приведет к бесконечной рекурсии, если в Model есть хотя бы 3 поля. Чтобы решить эту проблему, мы должны удалить отложенные поля от сохранения в начальном и изменить свойство _dict немного
gleb.pitsevich
19
Подобно ответу Джоша, этот код будет обманчиво работать на вашем однопроцессном сервере тестирования, но в тот момент, когда вы развернете его на любой сервер многопроцессорной обработки, он даст неверные результаты. Вы не можете знать, изменяете ли вы значение в базе данных, не запрашивая базу данных.
rspeer
154

Лучший способ с 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
Крис Пратт
источник
6
Почему это лучший способ, если метод, описанный выше Джошем, не требует дополнительного попадания в базу данных?
joshcartme
36
1) этот метод является хаком, сигналы в основном предназначены для таких применений, как 2) этот метод требует внесения изменений в вашу модель, этого нет 3) как вы можете прочитать в комментариях к этому ответу, у него есть побочные эффекты, которые может быть потенциально проблематичным, такого решения нет
Крис Пратт
2
Этот способ хорош, если вам нужно только перехватить изменения перед сохранением. Однако это не сработает, если вы хотите немедленно отреагировать на изменение. Я сталкивался с последним сценарием много раз (и сейчас я работаю над одним таким примером).
Джош
5
@Josh: Что вы подразумеваете под «немедленно реагировать на изменения»? Каким образом это не позволяет вам "реагировать"?
Крис Пратт
2
Извините, я забыл о масштабах этого вопроса и имел в виду совершенно другую проблему. Тем не менее, я думаю, что сигналы - хороший способ попасть сюда (теперь, когда они доступны). Тем не менее, я нахожу, что многие люди считают переопределение, кроме как «взломать». Я не верю, что это так. Как следует из этого ответа ( stackoverflow.com/questions/170337/… ), я думаю, что переопределение - это лучшая практика, когда вы не работаете с изменениями, «специфичными для рассматриваемой модели». Тем не менее, я не собираюсь навязывать эту веру никому.
Джош
138

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

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)

То же самое относится и к работе с формой. Вы можете обнаружить это при чистом или сохраненном методе ModelForm:

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 = []
Zgoda
источник
24
Решение Джоша гораздо более дружественно к базам данных. Дополнительный звонок для проверки того, что изменилось, стоит дорого.
дд.
5
Одно дополнительное чтение перед записью не так дорого. Также метод отслеживания изменений не работает, если есть несколько запросов. Хотя это будет страдать от состояния гонки между получением и сохранением.
dalore
1
Перестаньте говорить людям, чтобы они проверяли, pk is not Noneчто это не относится, например, если вы используете UUIDField. Это просто плохой совет.
user3467349
2
@dalore Вы можете избежать состояния гонки, украсив метод сохранения с помощью@transaction.atomic
Фрэнк Папе
2
@dalore, хотя вам нужно убедиться, что уровень изоляции транзакции достаточен. В postgresql значение по умолчанию - зафиксированное чтение, но повторяемое чтение необходимо .
Фрэнк Папе
58

Начиная с выпуска Django 1.8, вы можете использовать метод класса from_db для кэширования старого значения 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!
саржа
источник
1
Спасибо - вот ссылка на документы: docs.djangoproject.com/en/1.8/ref/models/instances/… . Я считаю, что это по-прежнему приводит к вышеупомянутой проблеме, когда база данных может меняться между оценкой и сравнением, но это хорошая новая опция.
trpt4him
1
Вместо того, чтобы искать значения (а это O (n) на основе числа значений), не будет ли это быстрее и понятнее new._loaded_remote_image = new.remote_image?
Далоре
1
К сожалению, я должен отменить мой предыдущий (теперь удаленный) комментарий. При from_dbвызове refresh_from_db, атрибуты экземпляра (то есть загруженные или предыдущие) не обновляются. В результате, я не могу найти причину , почему это лучше , чем , __init__как вам все еще нужно обрабатывать 3 случая: __init__/ from_db, refresh_from_db, и save.
Клейтонд
18

Если вы используете форму, вы можете использовать Измененные_данные формы ( документы ):

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
laffuste
источник
5

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

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

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

Амихай Шрайбер
источник
5

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

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something
jhrs21
источник
4

Вы можете использовать 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
Роберт Кайич
источник
4

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

Обновленная ссылка: 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
Аарон Макмиллин
источник
Это отличное решение для проверки загрузки нового файла. Намного лучше, чем сверять имя с базой данных, потому что имя файла может совпадать. Вы также можете использовать его в pre_saveприемнике. Спасибо, что поделились этим!
DataGreed
1
Вот пример обновления длительности звука в базе данных, когда файл был обновлен с использованием мутагена для чтения аудиоинформации - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed
3

Оптимальным решением, вероятно, является решение, которое не включает в себя дополнительную операцию чтения базы данных перед сохранением экземпляра модели или какую-либо дополнительную библиотеку django. Вот почему решения Лаффуста предпочтительнее. В контексте сайта администратора можно просто переопределить save_model-method и вызвать там has_changedметод формы , как в ответе Сиона выше. Вы 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

user3061675
источник
2

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

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

Если вы хотите сохранить ссылку на URL, вы можете использовать не редактируемое логическое поле для обработки флага кэширования, а не remote_imageсамо поле.

SmileyChris
источник
2

У меня была такая ситуация до того, как я решил переопределить 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) операцию, например, использовать созданный объект в каком-либо задании (если определенное поле изменилось)

MYaser
источник
2

улучшение ответа @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")

Hassek
источник
И это все еще не делает дополнительные запросы БД?
andilabs
Я пытался реализовать твой код. Работает нормально, редактируя поля. Но сейчас у меня проблема с вставкой нового. Я получаю DoesNotExist для моего поля FK в классе. Некоторые советы, как решить это, будут оценены.
andilabs
Я только что обновил код, теперь он пропускает внешние ключи, поэтому вам не нужно извлекать эти файлы с дополнительными запросами (очень дорого), и если объект не существует, он пропустит дополнительную логику.
Хассек
1

Я расширил миксин @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, в котором будет храниться состояние ваших объектов.

оборота MYaser
источник
1

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

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!"
Sion
источник
2
Если вы забыли super (Mode, self) .save (* args, ** kwargs), то вы отключаете функцию сохранения, поэтому не забудьте добавить ее в метод сохранения.
максимум
Ссылка на статью устарела, это новая ссылка: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop
1

Модификация ответа @ 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

get_fieldsВместо этого используется открытый метод django 1.10 . Это делает код более перспективным для будущего, но, что более важно, также включает внешние ключи и поля, где 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))
    )
theicfire
источник
1

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

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 (), - это вызов Model.clean (). Этот метод должен быть переопределен для выполнения пользовательской проверки вашей модели. Этот метод должен использоваться для обеспечения пользовательской проверки модели и, при желании, для изменения атрибутов вашей модели. Например, вы можете использовать его для автоматического предоставления значения для поля или для проверки, которая требует доступа к более чем одному полю: "

Гонсало
источник
1

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

Просто измените функцию сохранения модели на функцию ниже

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)

Пример использования:

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)

выводит только те поля, которые были изменены

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
Нимиш Бансал
источник
1

Очень поздно к игре, но это вариант ответа Криса Пратта, который защищает от условий гонки, жертвуя при этом производительностью, используя transactionблок иselect_for_update()

@receiver(pre_save, sender=MyModel)
@transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.select_for_update().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
baqyoteto
источник
0

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

Jiaaro
источник
0

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

Я расширил это до

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

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

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

Моя модель объекта:

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()
sknutsonsf
источник
0

Если вы не заинтересованы в переопределении saveметода, вы можете сделать

  model_fields = [f.name for f in YourModel._meta.get_fields()]
  valid_data = {
        key: new_data[key]
        for key in model_fields
        if key in new_data.keys()
  }

  for (key, value) in valid_data.items():
        if getattr(instance, key) != value:
           print ('Data has changed')

        setattr(instance, key, value)

 instance.save()
theTypan
источник