Pergunta

No meu aplicativo eu preciso para salvar os valores alterados (velho e novo), quando model ser salvo. Qualquer exemplos ou código de trabalho?

Eu preciso disso para premoderation de conteúdo. Por exemplo, se o usuário muda alguma coisa no modelo, então administrador pode ver todas as alterações na tabela separada e, em seguida, decidir aplicá-las ou não.

Foi útil?

Solução

Você não disse muito sobre o seu caso ou necessidades uso específico. Em particular, seria útil saber o que você precisa fazer com a informação da mudança (quanto tempo você precisa para armazená-lo?). Se você só precisa armazená-lo para fins transitórios, @ solução sessão de S. Lott pode ser melhor. Se você quer uma trilha de auditoria completa de todas as alterações aos seus objetos armazenados no DB, tente este AuditTrail solução .

Atualizar : O código AuditTrail eu link acima é o mais próximo que eu vi a uma solução completa que iria trabalhar para o seu caso, embora tenha algumas limitações (não funciona em tudo para campos ManyToMany). Ele irá armazenar todas as versões anteriores de seus objetos no DB, então o administrador poderia reverter para qualquer versão anterior. Você teria que trabalhar com ele um pouco se você deseja que a alteração não terá efeito até que aprovado.

Você também pode criar uma solução personalizada com base em algo como DiffingMixin de @Armin Ronacher. Você iria armazenar o dicionário diff (talvez em conserva?) Em uma tabela para o administrador para rever mais tarde e aplicar, se desejar (você precisa escrever o código para pegar o dicionário diff e aplicá-lo a uma instância).

Outras dicas

Eu achei a idéia de Armin muito útil. Aqui é a minha variação;

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

Edit:. Eu testei este BTW

Sorry about as longas filas. A diferença é (além dos nomes) só armazena em cache campos não-relação local. Em outras palavras, ele não armazena em cache campos de um modelo pai se presente.

E há mais uma coisa; você precisa redefinir _original_state dict depois de salvar. Mas eu não queria método save() de substituição já que a maioria das vezes descartamos modelar casos depois de salvar.

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

Django está enviando todas as colunas ao banco de dados, mesmo se você só mudou um. Para mudar isso, algumas mudanças no sistema de banco de dados seria necessário. Isso pode ser facilmente implementado no código existente, adicionando um conjunto de campos sujos para o modelo e adicionar nomes de coluna para que, cada vez que você __set__ um valor de coluna.

Se você precisar desse recurso, eu sugiro que você olhar para o Django ORM, implementá-lo e colocar um patch no trac Django. Deve ser muito fácil de adicionar isso e ele iria ajudar outros usuários também. Quando você faz isso, adicione um gancho que é chamado sempre que uma coluna está definido.

Se você não quer cortar na própria Django, você pode copiar o dict na criação do objeto e diff-lo.

Talvez com um mixin assim:

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

Este código não foi testado, mas deve funcionar. Quando você chama model.get_changed_columns() você começa um dicionário de todos os valores alterados. Isto, obviamente, não vai funcionar para objetos mutáveis ??em colunas porque o estado original é uma cópia simples do dict.

Eu estendi a solução da Trey Hunner às relações apoio M2M. Esperemos que isto irá ajudar os outros que procuram uma solução similar.

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

Um também pode querer fundir as duas listas de campo. Para isso, substituir a última linha

return changed_fields, changed_m2m_fields

com

changed_fields.update(changed_m2m_fields)
return changed_fields

Como adicionar uma segunda resposta, porque muita coisa mudou desde o tempo esta pergunta foi originalmente .

Há uma série de aplicativos no mundo Django que resolvem este problema agora. Você pode encontrar uma completa href="https://www.djangopackages.com/grids/g/model-audit/" do modelo de auditoria e de história aplicativos sobre os pacotes de Django site.

Eu escrevi um blog pós comparando alguns desses aplicativos . Este post é agora 4 anos de idade e é um pouco antiquado. As abordagens diferentes para resolver este problema parece ser o mesmo embora.

As abordagens:

  1. Guarde todas as mudanças históricas em um formato serializado (JSON?) Em uma tabela única
  2. Guarde todas as mudanças históricas em uma tabela que espelham o original para cada modelo
  3. Guarde todas as mudanças históricas na mesma tabela como o modelo original (Eu não recomendo este)

O django-reversão pacote ainda parece ser a solução mais popular para este problema. Leva a primeira abordagem:. Serialize muda em vez de espelhar tabelas

Eu reviveu django-simple-história alguns anos atrás. Leva a segunda abordagem:. Espelho cada mesa

Então, eu recomendaria usando um aplicativo para resolver este problema . Há uma dupla popular que o trabalho muito bem neste momento.

Ah, e se você está olhando apenas para verificação de campo sujo e não armazenar todas as mudanças históricas, veja FieldTracker de django-modelo-utils .

Continuando a sugestão de Muhuk & adicionando sinais do Django e uma dispatch_uid original você poderia repor o estado em salvar sem substituir 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]])

Qual seria limpar o estado original uma vez salvo, sem ter que substituir save (). O código funciona, mas não sei o que a penalidade de desempenho é de sinais de conexão no __init __

Eu estendi as soluções da SMN muhuk e incluir verificação de diferença sobre as chaves primárias para chave estrangeira e one-to-one campos:

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

A única diferença está em _as_dict eu mudei a última linha de

return dict([
    (f.name, getattr(self, f.name)) for f in self._meta.local_fields
    if not f.rel
])

para

return dict([
    (f.attname, getattr(self, f.attname)) for f in self._meta.local_fields
])

Esta mixin, como as acima, pode ser usado assim:

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

Se você estiver usando suas próprias transações (e não a aplicação de administrador padrão), você pode salvar o antes e depois de versões do seu objeto. Você pode salvar a versão antes da sessão, ou você pode colocá-lo em campos "escondido" no formulário. campos ocultos é um pesadelo de segurança. Portanto, use a sessão para manter a história do que está acontecendo com este usuário.

Além disso, é claro, você tem que buscar o objeto anterior para que você possa fazer alterações nele. Então você tem várias maneiras de monitorar as diferenças.

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

Uma solução atualizado com suporte M2M (usando atualizados dirtyfields e novo _ correções meta API e alguns bugs), com base em @Trey e @ Tony acima. Este passou alguns testes de luz básico para mim.

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

Para obter informações de todos, a solução da muhuk falha sob python2.6 como ele levanta uma exceção afirmando 'objeto .__ o init __ ()' não aceita argumento ...

edit: ho! aparentemente, ele poderia ter sido me mau uso do do mixin ... Eu prestar atenção não funcionavam e declarou-o como o último pai e por isso a chamada para init acabou no objeto pai, em vez da próxima pai como noramlly faria com herança diagrama diamante! então por favor desconsidere meu comentário:)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top