Грязные поля в django

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

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

Я встречал похожие вопросы для грязных полей, но это та же проблема; чтобы администратор посмотрел, что изменилось, вам сначала нужно определить, что изменилось ...

dnozay 22.04.2013 21:52
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
33
1
25 089
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

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

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

tghw 24.10.2009 03:22

Не могли бы вы подробнее рассказать о подходе __set__? Похоже, это соответствовало бы моим текущим потребностям, но я не смог добиться в этом никакого прогресса.

kasperd 05.10.2015 15:35

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

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 22.04.2013 21:48

@dnozay, это неудивительно, так как django-dirtyfields указывает под «кредитами», что он рожден из этого вопроса stackoverflow

Riccardo Galli 15.10.2013 03:02

для всеобщей информации, решение 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 30.12.2015 09:23

@Neil: код AuditTrail был завернут в djnago-simple-history много лет назад. Я добавил второй ответ, отметив рекомендуемые сегодня решения. С момента первоначального ответа многое изменилось. Спасибо, что оживили это!

Trey Hunner 01.01.2016 01:14

Обновленное решение с поддержкой 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 года, и он немного устарел. Однако разные подходы к решению этой проблемы кажутся одинаковыми.

Подходы:

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

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

Я возродил Джанго-простой-история несколько лет назад. Используется второй подход: зеркальное отображение каждой таблицы.

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

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

Другие вопросы по теме