Domanda

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 dei contenuti. 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.

È stato utile?

Soluzione

Non hai parlato molto del tuo caso d'uso specifico o delle tue esigenze. In particolare, sarebbe utile sapere cosa è necessario fare con le informazioni di modifica (per quanto tempo è necessario conservarle?). Se è necessario memorizzarlo solo per scopi temporanei, la soluzione di sessione di @ S.Lott potrebbe essere la migliore. Se desideri una traccia di controllo completa di tutte le modifiche ai tuoi oggetti archiviati nel DB, prova questa Soluzione AuditTrail .

AGGIORNAMENTO : il codice AuditTrail che ho collegato sopra è il più vicino che ho visto a una soluzione completa che funzionerebbe per il tuo caso, anche se ha alcune limitazioni (non funziona affatto per Numerosi campi). Memorizzerà tutte le versioni precedenti dei tuoi oggetti nel DB, quindi l'amministratore potrebbe tornare 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 DiffingMixin di @Armin Ronacher. Memorizzeresti il ??dizionario diff (magari in decapato?) In una tabella affinché l'amministratore possa rivederlo in seguito e applicarlo se lo desideri (dovresti scrivere il codice per prendere il dizionario diff e applicarlo a un'istanza).

Altri suggerimenti

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

Modifica: ho provato questo BTW.

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

E c'è un'altra cosa; è necessario ripristinare _original_state dict dopo il salvataggio. 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()

Django sta attualmente inviando tutte le colonne al database, anche se ne hai appena cambiata una. Per cambiarlo, sarebbero necessarie alcune modifiche al sistema di database. Questo potrebbe essere facilmente implementato sul codice esistente aggiungendo una serie di campi sporchi al modello e aggiungendo i nomi di colonna ad esso, ogni volta che __set__ un valore di colonna.

Se hai bisogno di quella funzione, ti suggerisco di guardare Django ORM, implementarlo e inserire una patch nel trac Django. Dovrebbe essere molto facile aggiungerlo e aiuterebbe anche altri utenti. Quando lo fai, aggiungi un hook chiamato ogni volta che viene impostata una colonna.

Se non vuoi hackerare Django stesso, puoi copiare il dict sulla creazione di oggetti e diff.

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 è testato ma dovrebbe funzionare. Quando chiami model.get_changed_columns () ottieni un dict di tutti i valori modificati. Questo ovviamente non funzionerà con oggetti mutabili in colonne perché lo stato originale è una copia piatta del dict.

Ho esteso la soluzione di Trey Hunner per supportare le relazioni m2m. Speriamo che questo possa aiutare 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 i due elenchi di campi. Per questo, sostituisci l'ultima riga

return changed_fields, changed_m2m_fields

con

changed_fields.update(changed_m2m_fields)
return changed_fields

Aggiunta di una seconda risposta perché molte cose sono cambiate dal momento in cui questa domanda è stata inizialmente pubblicata .

Esistono diverse app nel mondo di Django che risolvono questo problema ora. Puoi trovare un elenco completo di app per il controllo dei modelli e la cronologia nei pacchetti Django sito.

Ho scritto 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 una singola tabella
  2. Memorizza tutte le modifiche storiche in una tabella che rispecchia l'originale per ciascun modello
  3. Memorizza tutte le modifiche storiche nella stessa tabella del modello originale (non lo consiglio)

Il pacchetto django-reversion sembra ancora essere la soluzione più popolare a questo problema. Prende il primo approccio: serializzare le modifiche anziché eseguire il mirroring delle tabelle.

Ho rianimato django-simple-history qualche anno fa. Prende il secondo approccio: rispecchia ogni tabella.

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

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

Continuando sul suggerimento di Muhuk & amp; aggiungendo i segnali di Django e un unico dispatch_uid potresti ripristinare lo stato su save senza sovrascrivere 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]])

Che pulirà lo stato originale una volta salvato senza dover sovrascrivere save (). Il codice funziona ma non è sicuro di quale sia la penalità prestazionale nel collegare segnali in __init__

Ho esteso le soluzioni muhuk e smn per includere il controllo delle differenze sulle chiavi primarie per la chiave esterna e i 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):
    ....

Se stai utilizzando le tue transazioni (non l'applicazione di amministrazione predefinita), puoi salvare le versioni precedente e successiva dell'oggetto. Puoi salvare la versione precedente nella sessione oppure inserirla in " nascosto " campi 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 poterlo modificare. 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()

Una soluzione aggiornata con supporto m2m (utilizzando dirtyfields aggiornati e nuovi _ meta API e alcune correzioni di bug), basato su @Trey e @ Tony's sopra. Questo ha superato alcuni test di base 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()

per l'informazione di tutti, la soluzione di muhuk fallisce in python2.6 in quanto solleva un'eccezione affermando 'oggetto .__ init __ ()' non accetta alcun argomento ...

modifica: ho! a quanto pare potrebbe essere stato un uso improprio del mixin ... Non ho prestato attenzione e l'ho dichiarato come l'ultimo genitore e per questo motivo la chiamata a init è finita nell'oggetto parent anziché nel successivo genitore come farebbe normalmente con l'eredità del diagramma a diamante! quindi per favore ignora il mio commento :)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top