Question

Dans mon application, je dois enregistrer les valeurs modifiées (anciennes et nouvelles) lorsque le modèle est enregistré. Des exemples ou un code de travail?

J'en ai besoin pour la prémodération du contenu. Par exemple, si l'utilisateur modifie quelque chose dans le modèle, l'administrateur peut voir toutes les modifications dans un tableau séparé, puis décider de les appliquer ou non.

Était-ce utile?

La solution

Vous n'avez pas beaucoup parlé de votre cas d'utilisation ou de vos besoins spécifiques. En particulier, il serait utile de savoir ce que vous devez faire avec les informations de changement (combien de temps avez-vous besoin de les stocker?). Si vous n’avez besoin de le stocker que pour des raisons transitoires, la solution de session de S. S.Lott peut être préférable. Si vous souhaitez un suivi d'audit complet de toutes les modifications apportées aux objets stockés dans la base de données, essayez cette solution AuditTrail .

UPDATE : le code AuditTrail que j'ai lié ci-dessus est le plus proche d'une solution complète qui conviendrait à votre cas, même s'il comporte certaines limitations (ne fonctionne pas du tout pour Nombreux champs). Il stockera toutes les versions précédentes de vos objets dans la base de données afin que l'administrateur puisse revenir à n'importe quelle version précédente. Vous devrez travailler un peu avec si vous voulez que le changement ne prenne effet avant son approbation.

Vous pouvez également créer une solution personnalisée basée sur quelque chose comme DiffingMixin de @Armin Ronacher. Vous voudriez stocker le dictionnaire diff (peut-être décapé?) Dans une table que l'administrateur pourra examiner plus tard et appliquer si vous le souhaitez (vous devez écrire le code pour utiliser le dictionnaire diff et l'appliquer à une instance).

Autres conseils

J'ai trouvé l'idée d'Armin très utile. Voici ma variante.

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: j'ai testé ce BTW.

Désolé pour les longues files. La différence est (mis à part les noms), il ne met en cache que les champs locaux non-relationnels. En d'autres termes, il ne met pas en cache les champs d'un modèle parent s'il est présent.

Et il y a encore une chose; vous devez réinitialiser _original_state après avoir enregistré. Mais je ne voulais pas écraser la méthode save () , car la plupart du temps, nous ignorons les instances de modèle après avoir enregistré.

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

Django envoie actuellement toutes les colonnes à la base de données, même si vous venez d’en changer une. Pour changer cela, certaines modifications du système de base de données seraient nécessaires. Cela pourrait être facilement implémenté sur le code existant en ajoutant un ensemble de champs modifiés au modèle et en lui ajoutant des noms de colonne, chaque fois que vous __ définissez __ une valeur de colonne.

Si vous avez besoin de cette fonctionnalité, je vous suggère de regarder l'ORM de Django, de la mettre en œuvre et de mettre un correctif dans la trace de Django. Il devrait être très facile d’ajouter cela et aiderait également les autres utilisateurs. Ajoutez ensuite un hook appelé à chaque fois qu'une colonne est définie.

Si vous ne voulez pas pirater Django lui-même, vous pouvez copier le dict sur la création d’objets et le diff.

Peut-être avec un mixin comme ceci:

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

Ce code n'a pas été testé mais devrait fonctionner. Lorsque vous appelez model.get_changed_columns () , vous obtenez un dict de toutes les valeurs modifiées. Cela ne fonctionnera évidemment pas pour les objets mutables dans les colonnes car l'état d'origine est une copie à plat du dict.

J'ai étendu la solution de Trey Hunner pour prendre en charge les relations m2m. J'espère que cela aidera les autres à la recherche d'une solution similaire.

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

On peut également souhaiter fusionner les deux listes de champs. Pour cela, remplacez la dernière ligne

return changed_fields, changed_m2m_fields

avec

changed_fields.update(changed_m2m_fields)
return changed_fields

Ajouter une deuxième réponse car beaucoup de choses ont changé depuis le moment où cette question a été postée pour la première fois .

Plusieurs applications dans le monde Django résolvent ce problème à présent. Vous pouvez trouver une liste complète des applications d'audit de modèle et d'historique sur les packages Django. site.

J'ai écrit un billet de blog en comparant quelques-unes de ces applications. . Cet article a maintenant 4 ans et il est un peu daté. Les différentes approches pour résoudre ce problème semblent cependant être les mêmes.

Les approches:

  1. Stockez toutes les modifications historiques dans un format sérialisé (JSON?) dans une seule table
  2. Stocker toutes les modifications historiques dans une table reflétant l'original pour chaque modèle
  3. Stockez toutes les modifications historiques dans la même table que le modèle d'origine (je ne le recommande pas)

Le package django-reversion semble toujours être la solution la plus répandue à ce problème. Il adopte la première approche: sérialiser les modifications au lieu de mettre en miroir les tables.

J'ai relancé django-simple-history il y a quelques années. La deuxième approche est la suivante: dupliquez chaque table.

Je recommanderais donc d'utiliser une application pour résoudre ce problème . Il existe quelques solutions populaires qui fonctionnent plutôt bien à ce stade.

Oh, et si vous recherchez simplement une vérification de champ sale et que vous ne stockez pas tous les changements historiques, consultez FieldTracker de django-model-utils .

Continuant sur la suggestion de Muhuk & amp; en ajoutant les signaux de Django et un unique dispatch_uid, vous pouvez réinitialiser l’état lors de la sauvegarde sans remplacer la valeur de 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]])

Ce qui effacerait l’état initial une fois enregistré sans avoir à remplacer save (). Le code fonctionne, mais vous ne savez pas quelle est l’atteinte en performances liée à la connexion des signaux à __init __

J'ai étendu les solutions de muhuk et smn pour inclure la vérification des différences sur les clés primaires pour les clés étrangères et les champs un-à-un:

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

La seule différence est dans _as_dict J'ai changé la dernière ligne de

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

Ce mixin, comme ceux ci-dessus, peut être utilisé comme suit:

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

Si vous utilisez vos propres transactions (et non l'application d'administration par défaut), vous pouvez enregistrer les versions avant et après de votre objet. Vous pouvez enregistrer la version antérieure dans la session ou la placer dans "masqué". champs du formulaire. Les champs cachés sont un cauchemar de sécurité. Par conséquent, utilisez la session pour conserver l'historique de ce qui se passe avec cet utilisateur.

De plus, vous devez bien entendu récupérer l'objet précédent pour pouvoir le modifier. Vous avez donc plusieurs moyens de surveiller les différences.

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

Une solution mise à jour avec le support m2m (utilisant les zones sales mises à jour et le nouveau _ méta API et quelques corrections de bugs), basées sur @Trey et @ Tony ci-dessus. Cela a passé quelques tests de base de la lumière pour moi.

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

Pour tout le monde, la solution de muhuk a échoué sous python2.6 car elle soulève une exception indiquant "objet .__ init __ ()" n'accepte aucun argument ...

modifier: ho! apparemment, c’est peut-être moi qui ai mal utilisé le mixin ... Je n’y ai pas fait attention et l’ai déclaré comme le dernier parent et à cause de cela, l’appel de init s’est retrouvé dans le parent objet plutôt que le suivant parent comme il le ferait normalement avec l'héritage de diagramme de diamant! alors s'il vous plaît ne tenez pas compte de mon commentaire:)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top