私のモデルでは:
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
を変更したときに新しい画像を取得するにはどうすればよいですか?次に、リモート画像をキャッシュするより良い方法はありますか?
少し遅れていますが、この投稿に出くわした他の人のためにこの解決策を捨てさせてください。基本的に、元の値のコピーを保持するために、__init__
のmodels.Model
メソッドをオーバーライドする必要があります。これにより、別のDBルックアップを行う必要がなくなります(これは常に良いことです)。
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
私は次のミックスインを使用します:
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)
>>>
このソリューションは、現在のリクエストのコンテキストでのみ機能することに注意してください。したがって、主に単純な場合に適しています。複数のリクエストが同じモデルインスタンスを同時に操作できる同時環境では、間違いなく異なるアプローチが必要です。
そして今、直接の答えのために:フィールドの値が変更されたかどうかを確認する1つの方法は、インスタンスを保存する前にデータベースから元のデータを取得することです。この例を考えてみましょう:
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の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
シグナルを使用することです。この質問が尋ねられて回答された2009年のオプションではなかったかもしれませんが、今日これを見ている人はこのようにするべきです:
@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の古い値をキャッシュできます。次に、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!
フィールド変更の追跡は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 Dirty Fields
Django 1.8現在、Sergeが言及しているように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 1.8で動作します
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
別の遅い回答ですが、新しいファイルがファイルフィールドにアップロードされたかどうかを確認しようとしている場合は、これを試してください:(リンクに関するChristopher Adamsのコメントから適応 http://zmsmith.com/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
最適なソリューションは、おそらくモデルインスタンスを保存する前に追加のデータベース読み取り操作を含まず、Djangoライブラリを追加しないソリューションです。これが、laffusteのソリューションが望ましい理由です。管理サイトのコンテキストでは、上記のSionの答えのように、save_model-methodをオーバーライドし、そこでフォームのhas_changedメソッドを呼び出すだけです。次のようなものに到達します。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
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)操作を実行する場合は役に立ちません(特定のフィールドが変更された場合)
これは実際にはあなたの質問に答えるものではありませんが、別の方法でこれについて説明します。
ローカルコピーを正常に保存したら、remote_image
フィールドをクリアするだけです。その後、saveメソッドで、remote_image
が空でない場合はいつでも画像をいつでも更新できます。
URLへの参照を保持したい場合は、remote_image
フィールド自体ではなく、編集不可のブールフィールドを使用してキャッシュフラグを処理できます。
すべてのフィールドの@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")
別の方法があります。
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()が実行する2番目のステップは、Model.clean()を呼び出すことです。このメソッドは、モデルでカスタム検証を実行するためにオーバーライドする必要があります。このメソッドは、カスタムモデル検証を提供し、必要に応じてモデルの属性を変更するために使用する必要がありますたとえば、フィールドの値を自動的に提供したり、複数のフィールドにアクセスする必要がある検証を行うために使用できます。 "
@livskiyのmixinを次のように拡張しました。
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フィールドが追加され、そのフィールドにはオブジェクトの状態が格納されます
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!"
@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のpublicメソッド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))
)
smileyChrisの答えの拡張として、last_updatedのモデルにdatetimeフィールドを追加し、変更を確認する前に到達できる最大年齢に何らかの制限を設定することができます
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()
@ivanlivskiのmixinは素晴らしいです。
私はそれを拡張しました
更新されたコードはこちらから入手できます。 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()