Грязные поля в Джанго - PullRequest
       17

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

29 голосов
/ 21 сентября 2008

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

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

Ответы [ 10 ]

20 голосов
/ 02 декабря 2008

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

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()
14 голосов
/ 21 сентября 2008

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

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

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

12 голосов
/ 21 сентября 2008

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

Если вам нужна эта функция, я бы посоветовал вам взглянуть на Django ORM, внедрить его и поместить патч в трек Django. Это должно быть очень легко добавить, и это поможет другим пользователям. Когда вы сделаете это, добавьте ловушку, которая вызывается каждый раз, когда устанавливается столбец.

Если вы не хотите взламывать сам Django, вы можете скопировать указание на создание объекта и изменить его.

Может быть, с таким миксином:

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.

6 голосов
/ 13 июня 2012

Я расширил решение 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
5 голосов
/ 01 января 2016

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

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

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

Подходы:

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

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

Я оживил django-simple-history несколько лет назад. Для этого нужен второй подход: отразить каждый стол.

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

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

3 голосов
/ 13 января 2011

Я расширил решения 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):
    ....
3 голосов
/ 11 ноября 2009

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

2 голосов
/ 21 сентября 2008

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

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

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()
0 голосов
/ 30 декабря 2015

Обновленное решение с поддержкой m2m (с использованием обновленных dirtyfields и нового _meta API и некоторых исправлений ошибок), основанное на @Trey и @ Tony's выше. Это прошло некоторые базовые легкие испытания для меня.

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()
0 голосов
/ 23 июня 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...