私のアプリでは、モデルが保存されたときに変更された値(古いものと新しいもの)を保存する必要があります。例や動作するコードはありますか?
コンテンツの事前モデレートにこれが必要です。たとえば、ユーザーがモデル内で何かを変更した場合、管理者はすべての変更を別のテーブルで確認してから、それらを適用するかどうかを決定できます。
Armin のアイデアは非常に有用であることがわかりました。これが私のバリエーションです。
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
編集:私はこのBTWをテストしました。
長い行で申し訳ありません。違いは (名前は別として) ローカルの非リレーション フィールドのみをキャッシュすることです。つまり、親モデルのフィールドが存在する場合、それをキャッシュしません。
そして、もう 1 つあります。_original_state
保存後にdictをリセットする必要があります。save()
しかし、ほとんどの場合、保存後にモデル インスタンスを破棄するため、メソッドを上書きしたくありませんでした。
def save(self, *args, **kwargs):
super(Klass, self).save(*args, **kwargs)
self._original_state = self._as_dict()
特定のユースケースやニーズについてはあまり語っていません。特に、変更情報をどうする必要があるか (どのくらいの期間保存する必要があるか) を知っておくと役立ちます。一時的な目的でのみ保存する必要がある場合は、@ S.Lott のセッション ソリューションが最適です。DB に保存されているオブジェクトに対するすべての変更の完全な監査証跡が必要な場合は、このAuditTrail ソリューションを試してください。
更新:上記にリンクしたAuditTrailコードは、いくつかの制限がありますが(ManyToManyフィールドではまったく機能しません)、あなたのケースで機能する完全なソリューションに最も近いものです。オブジェクトの以前のバージョンはすべて DB に保存されるため、管理者は以前のバージョンにロールバックできます。承認されるまで変更を有効にしない場合は、少し作業する必要があります。
@Armin Ronacher の DiffingMixin などに基づいてカスタム ソリューションを構築することもできます。管理者が後で確認し、必要に応じて適用できるように、テーブルに diff ディクショナリ (多分ピクル?) を保存します (diff ディクショナリを取得してインスタンスに適用するコードを記述する必要があります)。
Django は現在、1 つだけ変更したとしても、すべての列をデータベースに送信しています。これを変更するには、データベース システムに何らかの変更を加える必要があります。これは、ダーティ フィールドのセットをモデルに追加し、列の値を入力するたびに列名をモデルに追加することで、既存のコードに簡単に実装でき__set__
ます。
その機能が必要な場合は、Django ORM を調べて実装し、Django trac にパッチを当てることをお勧めします。それを追加するのは非常に簡単で、他のユーザーにも役立つはずです。その場合、列が設定されるたびに呼び出されるフックを追加します。
Django 自体をハックしたくない場合は、オブジェクトの作成時に dict をコピーして diff を実行できます。
たぶん、次のようなミックスインを使用します。
class DiffingMixin(object):
def __init__(self, *args, **kwargs):
super(DiffingMixin, self).__init__(*args, **kwargs)
self._original_state = dict(self.__dict__)
def get_changed_columns(self):
missing = object()
result = {}
for key, value in self._original_state.iteritems():
if key != self.__dict__.get(key, missing):
result[key] = value
return result
class MyModel(DiffingMixin, models.Model):
pass
このコードはテストされていませんが、動作するはずです。呼び出すmodel.get_changed_columns()
と、変更されたすべての値の辞書が取得されます。元の状態は dict のフラット コピーであるため、これはもちろん、列内の変更可能なオブジェクトに対しては機能しません。
Trey Hunner のソリューションを拡張して、m2m 関係をサポートしました。これが、同様のソリューションを探している他の人に役立つことを願っています。
from django.db.models.signals import post_save
DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid='%s._reset_state' % self.__class__.__name__)
self._reset_state()
def _as_dict(self):
fields = dict([
(f.attname, getattr(self, f.attname))
for f in self._meta.local_fields
])
m2m_fields = dict([
(f.attname, set([
obj.id for obj in getattr(self, f.attname).all()
]))
for f in self._meta.local_many_to_many
])
return fields, m2m_fields
def _reset_state(self, *args, **kwargs):
self._original_state, self._original_m2m_state = self._as_dict()
def get_dirty_fields(self):
new_state, new_m2m_state = self._as_dict()
changed_fields = dict([
(key, value)
for key, value in self._original_state.iteritems()
if value != new_state[key]
])
changed_m2m_fields = dict([
(key, value)
for key, value in self._original_m2m_state.iteritems()
if sorted(value) != sorted(new_m2m_state[key])
])
return changed_fields, changed_m2m_fields
2 つのフィールド リストをマージすることもできます。そのために、最後の行を置き換えます
return changed_fields, changed_m2m_fields
と
changed_fields.update(changed_m2m_fields)
return changed_fields
muhuk と smn のソリューションを拡張して、外部キーと 1 対 1 フィールドの主キーの違いをチェックできるようにしました。
from django.db.models.signals import post_save
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
self._reset_state()
def _reset_state(self, *args, **kwargs):
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.attname, getattr(self, f.attname)) for f in self._meta.local_fields])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
唯一の違いは_as_dict
、最後の行を
return dict([
(f.name, getattr(self, f.name)) for f in self._meta.local_fields
if not f.rel
])
に
return dict([
(f.attname, getattr(self, f.attname)) for f in self._meta.local_fields
])
この mixin は、上記のものと同様に、次のように使用できます。
class MyModel(DirtyFieldsMixin, models.Model):
....
Muhuk の提案を続け、Django のシグナルと一意の dispatch_uid を追加すると、save() をオーバーライドせずに保存時に状態をリセットできます。
from django.db.models.signals import post_save
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
self._reset_state()
def _reset_state(self, *args, **kwargs):
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
save() をオーバーライドすることなく、一度保存された元の状態を消去します。コードは機能しますが、__init__ で信号を接続することによるパフォーマンスの低下が何であるかはわかりません
独自のトランザクション(デフォルトの管理アプリケーションではない)を使用している場合は、オブジェクトのバージョンの前後を保存できます。以前のバージョンをセッションに保存するか、フォームの「非表示」フィールドに入力することができます。隠されたフィールドはセキュリティの悪夢です。したがって、セッションを使用して、このユーザーで何が起こっているかの履歴を保持します。
さらに、もちろん、変更を加えることができるように、前のオブジェクトをフェッチする必要があります。したがって、違いを監視する方法はいくつかあります。
def updateSomething( request, object_id ):
object= Model.objects.get( id=object_id )
if request.method == "GET":
request.session['before']= object
form= SomethingForm( instance=object )
else request.method == "POST"
form= SomethingForm( request.POST )
if form.is_valid():
# You have before in the session
# You have the old object
# You have after in the form.cleaned_data
# Log the changes
# Apply the changes to the object
object.save()
上記の @Trey と @Tony に基づく、m2m サポート (更新されたdirtyfieldsと新しい_meta APIといくつかのバグ修正を使用) を備えた更新されたソリューション。これは、いくつかの基本的な照明テストに合格しました。
from dirtyfields import DirtyFieldsMixin
class M2MDirtyFieldsMixin(DirtyFieldsMixin):
def __init__(self, *args, **kwargs):
super(M2MDirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(
reset_state, sender=self.__class__,
dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format(
name=self.__class__.__name__))
reset_state(sender=self.__class__, instance=self)
def _as_dict_m2m(self):
if self.pk:
m2m_fields = dict([
(f.attname, set([
obj.id for obj in getattr(self, f.attname).all()
]))
for f,model in self._meta.get_m2m_with_model()
])
return m2m_fields
return {}
def get_dirty_fields(self, check_relationship=False):
changed_fields = super(M2MDirtyFieldsMixin, self).get_dirty_fields(check_relationship)
new_m2m_state = self._as_dict_m2m()
changed_m2m_fields = dict([
(key, value)
for key, value in self._original_m2m_state.iteritems()
if sorted(value) != sorted(new_m2m_state[key])
])
changed_fields.update(changed_m2m_fields)
return changed_fields
def reset_state(sender, instance, **kwargs):
# original state should hold all possible dirty fields to avoid
# getting a `KeyError` when checking if a field is dirty or not
instance._original_state = instance._as_dict(check_relationship=True)
instance._original_m2m_state = instance._as_dict_m2m()