Вопрос

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

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

Это было полезно?

Решение

Вы не очень много сказали о своем конкретном варианте использования или потребностях.В частности, было бы полезно знать, что вам нужно делать с информацией об изменениях (как долго вам нужно ее хранить?).Если вам нужно сохранить его только для временных целей, сессионное решение @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 года, и оно немного устарело.Однако различные подходы к решению этой проблемы кажутся одинаковыми.

Подходы:

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

Тот Самый 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 __()' не принимает аргумент...

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top