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