Вопрос
В моем приложении мне нужно сохранить измененные значения (старые и новые) при сохранении модели.Есть какие-нибудь примеры или рабочий код?
Мне это нужно для предварительной модерации контента.Например, если пользователь что-то меняет в модели, то администратор может просмотреть все изменения в отдельной таблице и затем решить, применять их или нет.
Решение
Вы не очень много сказали о своем конкретном варианте использования или потребностях.В частности, было бы полезно знать, что вам нужно делать с информацией об изменениях (как долго вам нужно ее хранить?).Если вам нужно сохранить его только для временных целей, сессионное решение @S.Lott может быть лучшим.Если вы хотите получить полный журнал аудита всех изменений ваших объектов, хранящихся в базе данных, попробуйте следующее Решение AuditTrail.
Обновить:Код AuditTrail, на который я ссылался выше, является наиболее близким, что я видел, к полному решению, которое сработало бы в вашем случае, хотя у него есть некоторые ограничения (вообще не работает для многих полей).Он сохранит все предыдущие версии ваших объектов в базе данных, чтобы администратор мог вернуться к любой предыдущей версии.Вам придется немного поработать с этим, если вы хотите, чтобы изменение не вступало в силу до тех пор, пока оно не будет одобрено.
Вы также могли бы создать собственное решение, основанное на чем-то вроде DiffingMixin от @Armin Ronacher.Вы бы сохранили словарь различий (может быть, маринованный?) в таблице, которую администратор может просмотреть позже и применить при желании (вам нужно будет написать код, чтобы взять словарь различий и применить его к экземпляру).
Другие советы
Я нахожу идею Армина очень полезной.Вот мой вариант;
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]])
Редактировать:Кстати, я это тестировал.
Извините за длинные очереди.Разница в том, что (помимо имен) он кэширует только локальные поля, не связанные между собой.Другими словами, он не кэширует поля родительской модели, если они присутствуют.
И есть еще кое-что;вам нужно выполнить перезагрузку _original_state
диктуйте после сохранения.Но я не хотел переписывать save()
метод, поскольку в большинстве случаев мы отбрасываем экземпляры модели после сохранения.
def save(self, *args, **kwargs):
super(Klass, self).save(*args, **kwargs)
self._original_state = self._as_dict()
В данный момент Django отправляет все столбцы в базу данных, даже если вы только что изменили один.Чтобы изменить это, потребуются некоторые изменения в системе баз данных.Это можно было бы легко реализовать в существующем коде, добавив в модель набор грязных полей и добавляя к нему имена столбцов каждый раз, когда вы __set__
значение столбца.
Если вам нужна эта функция, я бы посоветовал вам взглянуть на Django ORM, реализовать ее и поместить патч в Django trac.Добавить это должно быть очень легко, и это помогло бы и другим пользователям.Когда вы сделаете это, добавьте перехват, который вызывается каждый раз, когда задается столбец.
Если вы не хотите взламывать сам Django, вы могли бы скопировать dict при создании объекта и изменить его.
Может быть, с такой смесью, как эта:
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.
Я расширил решение Трея Ханнера для поддержки отношений 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
Возможно, также возникнет желание объединить два списка полей.Для этого замените последнюю строку
return changed_fields, changed_m2m_fields
с
changed_fields.update(changed_m2m_fields)
return changed_fields
Добавляю второй ответ, потому что многое изменилось с тех пор, как были первоначально опубликованы эти вопросы.
В мире Django есть несколько приложений, которые сейчас решают эту проблему.Вы можете найти полный список приложений для аудита моделей и ведения истории на сайте пакетов Django.
Я написал запись в блоге сравнение нескольких из этих приложений.Этому сообщению уже 4 года, и оно немного устарело.Однако различные подходы к решению этой проблемы кажутся одинаковыми.
Подходы:
- Храните все исторические изменения в сериализованном формате (JSON?) в одной таблице
- Храните все исторические изменения в таблице, отражающей оригинал для каждой модели
- Храните все исторические изменения в той же таблице, что и исходная модель (я не рекомендую это делать).
Тот Самый django-реверсия пакет по-прежнему остается самым популярным решением этой проблемы.Для этого нужен первый подход:сериализуйте изменения вместо зеркального отображения таблиц.
Я ожил django-простой-история несколько лет назад.Для этого нужен второй подход:отразите каждый стол в зеркальном отражении.
Так что я бы порекомендовал использование приложения для решения этой проблемы.Есть пара популярных из них, которые на данный момент работают довольно хорошо.
О, и если вы просто ищете грязную проверку полей и не сохраняете все исторические изменения, ознакомьтесь FieldTracker из django-модель-утилиты.
Продолжая предложение Мухука и добавляя сигналы 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__
Я расширил решения muhuk и smn, включив проверку различий в первичных ключах для полей внешнего ключа и "один к одному":
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
])
Эту смесь, как и описанные выше, можно использовать следующим образом:
class MyModel(DirtyFieldsMixin, models.Model):
....
Если вы используете свои собственные транзакции (а не приложение администратора по умолчанию), вы можете сохранить версии вашего объекта "до" и "после".Вы можете сохранить предыдущую версию в сеансе или поместить ее в "скрытые" поля формы.Скрытые поля - это кошмар безопасности.Поэтому используйте сеанс, чтобы сохранить историю того, что происходит с этим пользователем.
Кроме того, конечно, вам нужно извлечь предыдущий объект, чтобы вы могли внести в него изменения.Таким образом, у вас есть несколько способов отслеживать различия.
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()
Обновленное решение с поддержкой m2m (с использованием обновленного грязные поля и новый _meta API и некоторые исправления ошибок), основанный на приведенных выше сообщениях @Trey и @Tony.Для меня это прошло некоторые базовые световые тесты.
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()
к сведению всех, решение мухука терпит неудачу в python2.6, поскольку оно вызывает исключение с указанием 'object.__ init __()' не принимает аргумент...
Редактировать:хо!очевидно, это могло быть из-за того, что я неправильно использовал миксин...Я не обратил внимания и объявил его последним родительским, и из-за этого вызов инициализация оказался в родительском объекте, а не в следующем родительском, как это обычно было бы при наследовании ромбовидной диаграммы!поэтому, пожалуйста, не обращайте внимания на мой комментарий :)