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
на псевдоним? А во-вторых, есть ли лучший способ кэшировать удаленное изображение?
Ответы
Ответ 1
Хотя это немного поздно, позвольте мне выкинуть это решение для других, которые сталкиваются с этим сообщением. По существу, вы хотите переопределить метод __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
Ответ 2
Я использую следующий 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 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)
>>>
Примечание
Обратите внимание, что это решение работает хорошо только в контексте текущего запроса. Таким образом, он подходит в первую очередь для простых случаев. В параллельной среде, где несколько запросов могут одновременно манипулировать одним и тем же экземпляром модели, вам определенно нужен другой подход.
Ответ 3
Лучший способ - с сигналом 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
Ответ 4
И теперь для прямого ответа: один из способов проверить, изменилось ли значение для поля, - извлечь исходные данные из базы данных перед сохранением экземпляра. Рассмотрим этот пример:
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 = []
Ответ 5
С выпуском Django 1.8 вы можете использовать метод class_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!
Ответ 6
Обратите внимание, что отслеживание изменений полей доступно в django-model-utils.
https://django-model-utils.readthedocs.org/en/latest/index.html
Ответ 7
Если вы используете форму, вы можете использовать Form changed_datastrong > (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
Ответ 8
Начиная с Django 1.8, существует метод from_db
, как упоминает Серж. Фактически, документы Django включают этот конкретный пример использования в качестве примера:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
Ниже приведен пример, показывающий, как записать начальные значения полей, загружаемых из базы данных.
Ответ 9
Я немного опоздал на вечеринку, но я нашел и это решение: Django Dirty Fields
Ответ 10
Вы можете использовать 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
Ответ 11
Еще один поздний ответ, но если вы просто пытаетесь посмотреть, загружен ли новый файл в поле файла, попробуйте следующее: (адаптированный от Кристофера Адамса комментарий по ссылке http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ в комментарии zach здесь)
Обновлено: 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 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
Ответ 12
Это работает для меня в Django 1.8
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
Ответ 13
Оптимальное решение, вероятно, является тем, которое не включает в себя дополнительную операцию чтения базы данных до сохранения экземпляра модели или никакой дополнительной библиотеки django. Вот почему предпочтительнее использовать решения laffuste. В контексте сайта администратора можно просто переопределить метод 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)
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
- Встроенный метод change_data для поля:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
Ответ 14
Пока это не отвечает на ваш вопрос, я бы об этом по-другому.
Просто удалите поле remote_image
после успешного сохранения локальной копии. Затем в вашем методе сохранения вы всегда можете обновлять изображение, когда remote_image
не пуст.
Если вы хотите сохранить ссылку на URL-адрес, вы можете использовать не редактируемое логическое поле для обработки флага кэширования, а не самого remote_image
.
Ответ 15
У меня была такая ситуация, прежде чем мое решение должно было переопределить метод 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) операцию, например, используя созданный объект в некотором задании (если определенное поле изменилось)
Ответ 16
улучшение ответа @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")
Ответ 17
Я расширил 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 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 будет добавлено при синхронизации/переносе, и это поле сохранит состояние ваших объектов.
Ответ 18
Как насчет использования решения David Cramer:
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!"
Ответ 19
Используйте метод has_changed()
, чтобы проверить, были ли изменены данные формы из исходных данных.
Пример:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': '[email protected]',
... 'cc_myself': True}
>>> f = ContactForm(data, initial=data)
>>> f.has_changed()
False
Когда форма отправлена, Django восстанавливает ее, чтобы можно было выполнить сравнение.
>>> f = ContactForm(request.POST, initial=data)
>>> f.has_changed()
>>> "--True if request.POST and data are different"
has_changed() будет True, если данные из request.POST отличаются от того, что было предоставлено в исходном или False в противном случае. Результат вычисляется путем вызова поля Field.has_changed() для каждого поля в форме.
Атрибут Form.changed_data покажет, какие поля были изменены с initial
и request.POST
.
Вы также можете использовать instance=YourModel
вместо inital=data
, если у вас уже есть объект.
Подробнее о здесь.
Ответ 20
Вот еще один способ сделать это.
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(). Этот метод должен быть переопределен для выполнения пользовательской проверки вашей модели. Этот метод должен использоваться для обеспечения пользовательской проверки модели и, при желании, для изменения атрибутов вашей модели. Например, вы можете использовать его для автоматического предоставления значения для поля или для проверки, которая требует доступа к более чем одному полю: "
Ответ 21
как расширение ответа SmileyChris, вы можете добавить поле datetime в модель для last_updated и установить какой-то предел для максимального возраста, который вы позволите ему до этого проверить, прежде чем проверять изменение
Ответ 22
Миксин из @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()
Ответ 23
Модификация ответа @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))
)
Ответ 24
Если вы не заинтересованы в переопределении метода 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()
Ответ 25
Существует атрибут __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'}}