В моем приложении мне нужно сохранить измененные значения (старые и новые) при сохранении модели. Есть примеры или рабочий код?
Мне это нужно для предварительной модерации контента. Например, если пользователь что-то меняет в модели, то администратор может увидеть все изменения в отдельной таблице, а затем решить применить их или нет.






Если вы используете свои собственные транзакции (а не приложение администратора по умолчанию), вы можете сохранить до и после версии вашего объекта. Вы можете сохранить предыдущую версию в сеансе или поместить ее в «скрытые» поля формы. Скрытые поля - кошмар безопасности. Поэтому используйте сеанс, чтобы сохранить историю того, что происходит с этим пользователем.
Кроме того, конечно, вам нужно получить предыдущий объект, чтобы вы могли внести в него изменения. Итак, у вас есть несколько способов отслеживать различия.
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()
Вы не очень много сказали о своем конкретном сценарии использования или потребностях. В частности, было бы полезно знать, что вам нужно делать с информацией об изменениях (как долго вам нужно ее хранить?). Если вам нужно сохранить его только для временных целей, решение сеанса @S.Lott может быть лучшим. Если вам нужен полный контрольный журнал всех изменений ваших объектов, хранящихся в БД, попробуйте этот Решение AuditTrail.
ОБНОВИТЬ: Код AuditTrail, на который я ссылался выше, является наиболее близким к полному решению, которое я видел для вашего случая, хотя он имеет некоторые ограничения (вообще не работает для полей ManyToMany). Он сохранит все предыдущие версии ваших объектов в БД, чтобы администратор мог вернуться к любой предыдущей версии. Вам придется немного поработать с этим, если вы хотите, чтобы изменение не вступило в силу до тех пор, пока оно не будет одобрено.
Вы также можете создать собственное решение на основе чего-то вроде DiffingMixin @Armin Ronacher. Вы бы сохранили словарь различий (может быть, маринованный?) В таблице, чтобы администратор мог просмотреть их позже и применить при желании (вам нужно будет написать код, чтобы взять словарь различий и применить его к экземпляру).
Django в настоящее время отправляет все столбцы в базу данных, даже если вы только что изменили один. Чтобы изменить это, потребуются некоторые изменения в системе баз данных. Это можно легко реализовать в существующем коде, добавив в модель набор «грязных» полей и добавив к ней имена столбцов каждый раз, когда вы используете __set__ для значения столбца.
Если вам нужна эта функция, я бы посоветовал вам взглянуть на Django ORM, реализовать ее и поместить патч в трассировку Django. Это должно быть очень легко добавить, и это поможет и другим пользователям. Когда вы это сделаете, добавьте ловушку, которая вызывается каждый раз, когда устанавливается столбец.
Если вы не хотите взламывать сам 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.
Это, вероятно, давно пора, но это должен быть if value != self.__dict__.get(key, missing):
Не могли бы вы подробнее рассказать о подходе __set__? Похоже, это соответствовало бы моим текущим потребностям, но я не смог добиться в этом никакого прогресса.
Я нашел идею Армина очень полезной. Вот мой вариант;
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.
Извините за длинные очереди. Разница в том, что (помимо имен) кэшируются только локальные поля, не относящиеся к отношениям. Другими словами, он не кэширует поля родительской модели, если они есть.
И еще кое-что; вам необходимо сбросить _original_state dict после сохранения. Но я не хотел перезаписывать метод save(), поскольку в большинстве случаев мы отбрасываем экземпляры модели после сохранения.
def save(self, *args, **kwargs):
super(Klass, self).save(*args, **kwargs)
self._original_state = self._as_dict()
приложение django-dirtyfields предоставляет такой же миксин.
@dnozay, это неудивительно, так как django-dirtyfields указывает под «кредитами», что он рожден из этого вопроса stackoverflow
для всеобщей информации, решение muhuk не работает под python2.6, поскольку оно вызывает исключение, в котором говорится, что 'object .__ init __ ()' не принимает аргументов ...
редактировать: хо! видимо, это могло быть из-за того, что я неправильно использовал миксин ... Я не обратил внимания и объявил его последним родителем, и из-за этого вызов в этом закончился в родительском объекте, а не в следующем родителе, как это обычно бывает с алмазом наследование диаграммы! так что не обращайте внимания на мой комментарий :)
Продолжая предложение Мухука и добавляя сигналы 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):
....
Я расширил решение 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
Можно также объединить два списка полей. Для этого замените последнюю строку
return changed_fields, changed_m2m_fields
с
changed_fields.update(changed_m2m_fields)
return changed_fields
@trey, похоже на m2m. Вы проверили, что это работает? Кроме того, обновлено ли это для последней версии Django с использованием обновленного API _meta?
@Neil: код AuditTrail был завернут в djnago-simple-history много лет назад. Я добавил второй ответ, отметив рекомендуемые сегодня решения. С момента первоначального ответа многое изменилось. Спасибо, что оживили это!
Обновленное решение с поддержкой 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()
Добавляем второй ответ, потому что многое изменилось с момента публикации этого вопроса.
В мире Django есть ряд приложений, которые решают эту проблему. Вы можете найти полный список приложений для аудита модели и истории на сайте Django Packages.
Я написал сообщение в блоге, сравнивая несколько из этих приложений. Этому посту уже 4 года, и он немного устарел. Однако разные подходы к решению этой проблемы кажутся одинаковыми.
Подходы:
Пакет джанго-реверсия по-прежнему кажется самым популярным решением этой проблемы. Он использует первый подход: сериализовать изменения вместо зеркального отображения таблиц.
Я возродил Джанго-простой-история несколько лет назад. Используется второй подход: зеркальное отображение каждой таблицы.
Поэтому я бы рекомендовал использование приложения для решения этой проблемы. Есть несколько популярных, которые на данный момент работают очень хорошо.
Да, и если вы просто ищете грязную проверку полей и не сохраняете все исторические изменения, посмотрите FieldTracker из django-model-utils.
Я встречал похожие вопросы для грязных полей, но это та же проблема; чтобы администратор посмотрел, что изменилось, вам сначала нужно определить, что изменилось ...