Domanda Campi sporchi nel django


Nella mia app ho bisogno di salvare i valori modificati (vecchi e nuovi) quando il modello viene salvato. Qualche esempio o codice funzionante?

Ho bisogno di questo per la premoderazione del contenuto. Ad esempio, se l'utente modifica qualcosa nel modello, l'amministratore può vedere tutte le modifiche in una tabella separata e quindi decidere di applicarle o meno.


28
2017-09-21 11:27


origine


risposte:


Non hai detto molto sul tuo caso d'uso o sui tuoi bisogni specifici. In particolare, sarebbe utile sapere cosa è necessario fare con le informazioni sulla modifica (per quanto tempo è necessario memorizzarle?). Se hai solo bisogno di memorizzarlo per scopi transitori, la soluzione di sessione di @ S.Lott potrebbe essere la migliore. Se vuoi un audit trail completo di tutte le modifiche ai tuoi oggetti memorizzati nel DB, prova questo Soluzione AuditTrail.

AGGIORNARE: Il codice AuditTrail che ho collegato sopra è il più vicino che ho visto a una soluzione completa che avrebbe funzionato per il tuo caso, sebbene abbia alcune limitazioni (non funziona affatto per i campi ManyToMany). Memorizzerà tutte le versioni precedenti degli oggetti nel DB, quindi l'amministratore potrebbe eseguire il rollback a qualsiasi versione precedente. Dovresti lavorarci un po 'se vuoi che la modifica non abbia effetto fino all'approvazione.

Potresti anche creare una soluzione personalizzata basata su qualcosa come @Armin Ronacher's DiffingMixin. Dovresti memorizzare il dizionario diff (magari in salamoia?) In una tabella affinché l'amministratore possa rivederlo in un secondo momento e applicarlo se lo desideri (dovresti scrivere il codice per prendere il dizionario diff e applicarlo a un'istanza).


13
2017-09-21 14:04



Ho trovato l'idea di Armin molto utile. Ecco la mia variazione;

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: ho testato questo BTW.

Mi dispiace per le lunghe code. La differenza è (a parte i nomi) che memorizza solo i campi locali di non-relazione. In altre parole, non memorizza nella cache i campi di un modello principale se presenti.

E c'è ancora una cosa; è necessario reimpostare _original_state dict dopo aver salvato. Ma non volevo sovrascrivere il metodo save () poiché la maggior parte delle volte scartiamo le istanze del modello dopo il salvataggio.

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

19
2017-12-01 21:09



Django sta attualmente inviando tutte le colonne al database, anche se ne hai appena modificato uno. Per cambiare questo, sarebbero necessarie alcune modifiche nel sistema di database. Questo potrebbe essere facilmente implementato sul codice esistente aggiungendo un set di campi sporchi al modello e aggiungendo i nomi delle colonne ad esso, ogni volta che si __set__ un valore di colonna.

Se hai bisogno di questa funzione, ti suggerisco di guardare l'ORM di Django, metterlo in pratica e inserire una patch nel tracciato di Django. Dovrebbe essere molto facile aggiungerlo e aiutare gli altri utenti. Quando lo fai, aggiungi un hook che viene chiamato ogni volta che viene impostata una colonna.

Se non vuoi hackerare Django, potresti copiare il testo sulla creazione dell'oggetto e diffarlo.

Forse con un mixin come questo:

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

Questo codice non è stato testato ma dovrebbe funzionare. Quando chiami model.get_changed_columns() ottieni un dettato di tutti i valori modificati. Questo ovviamente non funzionerà con oggetti mutabili nelle colonne perché lo stato originale è una copia piatta del dict.


10
2017-09-21 16:33



Ho esteso la soluzione di Trey Hunner per supportare le relazioni m2m. Speriamo che questo aiuti gli altri a cercare una soluzione simile.

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

Si potrebbe anche voler unire le due liste di campi. Per questo, sostituisci l'ultima riga

return changed_fields, changed_m2m_fields

con

changed_fields.update(changed_m2m_fields)
return changed_fields

6
2018-06-13 08:57



Aggiungere una seconda risposta perché molto è cambiato dal momento in cui queste domande sono state originariamente pubblicate.

Ci sono un certo numero di app nel mondo Django che risolvono questo problema ora. Puoi trovarne uno pieno elenco di app di verifica del modello e di cronologia sul sito dei pacchetti Django.

scrissi un post sul blog confrontando alcune di queste app. Questo post ha ora 4 anni ed è un po 'datato. I diversi approcci per risolvere questo problema sembrano essere gli stessi però.

Gli approcci:

  1. Memorizza tutte le modifiche storiche in un formato serializzato (JSON?) In un'unica tabella
  2. Salva tutte le modifiche storiche in una tabella che rispecchia l'originale per ciascun modello
  3. Salva tutte le modifiche storiche nella stessa tabella del modello originale (non lo consiglio)

Il django-reversione il pacchetto sembra essere la soluzione più popolare a questo problema. Prende il primo approccio: serializza le modifiche invece delle tabelle di mirroring.

Ho rianimato django-semplici-storia alcuni anni fa. Prende il secondo approccio: rispecchia ogni tavolo.

Quindi lo consiglierei usando un'app per risolvere questo problema. Ce ne sono un paio di quelli che funzionano abbastanza bene a questo punto.

Oh, e se stai solo cercando il controllo del campo sporco e non memorizzando tutte le modifiche storiche, controlla FieldTracker da django-model-utils.


5
2017-12-31 22:11



Continuando sul suggerimento di Muhuk e aggiungendo i segnali di Django e un dispatch_uid unico, è possibile ripristinare lo stato di salvataggio senza eseguire l'override di 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]])

Quale pulisce lo stato originale una volta salvato senza dover sovrascrivere save (). Il codice funziona ma non è sicuro quale sia la penalità di prestazioni dei segnali di connessione su __init__


3
2017-11-11 15:43



Ho esteso le soluzioni muhuk e smn per includere il controllo delle differenze sulle chiavi primarie per chiavi esterne e campi uno a uno:

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

L'unica differenza è in _as_dict Ho cambiato l'ultima riga da

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

a

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

Questo mixin, come quelli sopra, può essere usato in questo modo:

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

3
2018-01-13 02:03



Se si utilizzano le proprie transazioni (non l'applicazione di amministrazione predefinita), è possibile salvare le versioni precedenti e successive dell'oggetto. È possibile salvare la versione precedente nella sessione oppure inserirla nei campi "nascosti" nel modulo. I campi nascosti sono un incubo per la sicurezza. Pertanto, utilizzare la sessione per conservare la cronologia di ciò che sta accadendo con questo utente.

Inoltre, ovviamente, devi recuperare l'oggetto precedente in modo da poter apportare modifiche ad esso. Quindi hai diversi modi per monitorare le differenze.

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

1
2017-09-21 11:42



Una soluzione aggiornata con supporto m2m (usando aggiornato dirtyfields e nuovo _meta API e alcune correzioni di bug), basate su @Trey e @ Tony's sopra. Questo ha superato alcuni test di base sulla luce per me.

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
2017-12-30 08:26