Django: При сохранении, как вы можете проверить, изменилось ли поле? - PullRequest
250 голосов
/ 31 августа 2009

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

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

Ответы [ 23 ]

381 голосов
/ 25 ноября 2009

Хотя уже немного поздно, позвольте мне выкинуть это решение для других, кто сталкивался с этим постом. По сути, вы хотите переопределить метод __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
177 голосов
/ 12 декабря 2012

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

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

Примечание

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

133 голосов
/ 29 октября 2011

Лучший способ - сигнал 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
133 голосов
/ 01 сентября 2009

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

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 = []
52 голосов
/ 03 июля 2015

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

@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!
16 голосов
/ 25 сентября 2014

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

https://django -model-utils.readthedocs.org / ен / последний / index.html

13 голосов
/ 15 июля 2016

Если вы используете форму, вы можете использовать updated_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
5 голосов
/ 19 января 2017

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

5 голосов
/ 16 сентября 2015

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

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

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

4 голосов
/ 13 октября 2013

Вы можете использовать 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...