Грязные поля в джанго

В моем приложении мне нужно сохранить измененные значения (старые и новые), когда модель будет сохранена. Любые примеры или рабочий код?

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

Ответ 1

Вы не очень много говорили о своем конкретном случае использования или потребностях. В частности, было бы полезно узнать, что вам нужно делать с информацией об изменении (сколько времени вам нужно хранить?). Если вам нужно только сохранить его в преходящих целях, решение @S.Lott может быть лучшим. Если вы хотите получить полный контрольный список всех изменений в ваших объектах, хранящихся в БД, попробуйте решение AuditTrail.

ОБНОВЛЕНИЕ. Код AuditTrail, который я связал с выше, является самым близким к полному решению, которое будет работать для вашего дела, хотя оно имеет некоторые ограничения (не работает вообще для ManyToMany). Он сохранит все предыдущие версии ваших объектов в БД, чтобы администратор мог вернуться к любой предыдущей версии. Вам придется немного поработать с ним, если вы хотите, чтобы изменения не вступили в силу до одобрения.

Вы также можете создать собственное решение на основе чего-то вроде @Armin Ronacher DiffingMixin. Вы сохранили бы словарь diff (возможно, маринован?) В таблице, чтобы администратор просмотрел его позже и применил, если это необходимо (вам нужно будет написать код, чтобы взять словарь diff и применить его к экземпляру).

Ответ 2

Я нашел идею Армина очень полезной. Вот моя вариация;

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.

Извините за длинные строки. Разница заключается в том, что (кроме имен) она кэширует только локальные поля не связанных отношений. Другими словами, он не кэширует поля родительской модели, если они есть.

И еще одна вещь; вам нужно reset _original_state dict после сохранения. Но я не хотел перезаписывать метод save(), так как большую часть времени мы отбрасываем экземпляры модели после сохранения.

def save(self, *args, **kwargs):
    super(Klass, self).save(*args, **kwargs)
    self._original_state = self._as_dict()

Ответ 3

В настоящее время Django отправляет все столбцы в базу данных, даже если вы просто изменили их. Чтобы изменить это, необходимы некоторые изменения в системе баз данных. Это может быть легко реализовано в существующем коде путем добавления в модель набора грязных полей и добавления имен столбцов к нему каждый раз, когда вы __set__ значение столбца.

Если вам нужна эта функция, я предлагаю вам взглянуть на ORM Django, реализовать ее и поместить патч в трассировку 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.

Ответ 4

Я расширил решение Трей Хуннера для поддержки отношений 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

Ответ 5

Добавление второго ответа, потому что многое изменилось с момента публикации этих вопросов.

В мире Django существует множество приложений, которые решают эту проблему. Полный список списка аудита моделей и приложений для истории можно найти на сайте Django Packages.

Я написал сообщение в блоге, сравнивающее некоторые из этих приложений. Этот пост сейчас 4 года, и он немного устарел. Различные подходы к решению этой проблемы кажутся одинаковыми, хотя.

Подходы:

  • Сохранять все исторические изменения в сериализованном формате (JSON?) в одной таблице
  • Сохранять все исторические изменения в таблице, отражающей оригинал для каждой модели.
  • Сохраняйте все исторические изменения в той же таблице, что и исходная модель (я не рекомендую это)

Пакет django-reversion по-прежнему является самым популярным решением этой проблемы. Он использует первый подход: сериализуйте изменения вместо зеркалирования таблиц.

Я восстановил django-simple-history несколько лет назад. Он использует второй подход: зеркалировать каждую таблицу.

Поэтому я бы рекомендовал использовать приложение для решения этой проблемы. Там пара популярных, которые работают очень хорошо на данный момент.

О, и если вы просто ищете проверку грязного поля и не сохраняете все исторические изменения, посмотрите FieldTracker из django-model-utils.

Ответ 6

Продолжая предложение Мухука и добавляя сигналы Django и уникальный dispatch_uid, вы можете reset сохранить состояние без сохранения 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 __

Ответ 7

Я расширил решения 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
])

Этот mixin, как и выше, можно использовать так:

class MyModel(DirtyFieldsMixin, models.Model):
    ....

Ответ 8

Если вы используете свои собственные транзакции (а не приложение администратора по умолчанию), вы можете сохранить до и после версий вашего объекта. Вы можете сохранить предыдущую версию в сеансе, или вы можете поместить ее в "скрытые" поля в форме. Скрытые поля - кошмар безопасности. Поэтому используйте сеанс, чтобы сохранить историю того, что происходит с этим пользователем.

Кроме того, конечно, вам нужно извлечь предыдущий объект, чтобы вы могли вносить в него изменения. Таким образом, у вас есть несколько способов контролировать различия.

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()

Ответ 9

Обновленное решение с поддержкой m2m (с использованием обновленного dirtyfields и нового _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()

Ответ 10

для каждой информации, решение muhuk выходит из строя под python2.6, поскольку оно вызывает исключение, указывающее на объект.__ init __() 'не принимает аргумент...

Изменить: ho! по-видимому, возможно, я неправильно использовал mixin... Я не обращал внимания и объявил его последним родителем, и из-за этого вызов init оказался в родительском объекте, а не в следующем родительский, поскольку он норамли будет с наследованием алмазной диаграммы! поэтому, пожалуйста, не обращайте внимания на мой комментарий:)