Champs sales dans django

Dans mon application, je dois enregistrer les valeurs modifiées (anciennes et nouvelles) lorsque le modèle est enregistré. Des exemples ou du 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 une table séparée, puis décider de les appliquer ou non.

29
demandé sur Dmitry Shevchenko 2008-09-21 15:27:24

10 réponses

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 devez-vous conserver?). Si vous avez seulement besoin de le stocker à des fins transitoires, la solution de session de @S. Lott peut être la meilleure. Si vous voulez une piste d'audit complète de toutes les modifications apportées à vos objets stockés dans la base de données, essayez cette solution AuditTrail.

UPDATE : le code AuditTrail auquel j'ai lié ci-dessus est le plus proche J'ai vu une solution complète qui fonctionnerait pour votre cas, bien qu'elle ait quelques limitations (ne fonctionne pas du tout pour les champs ManyToMany). 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 devez travailler avec un peu si vous voulez que la modification ne prend effet qu'après approbation.

Vous pouvez également construire une solution personnalisée basée sur quelque chose comme DiffingMixin de @ Armin Ronacher. Vous stockeriez le dictionnaire diff (peut-être marinés?) dans une table pour que l'administrateur examine plus tard et applique si désiré (vous devez écrire le code pour prendre le dictionnaire diff et l'appliquer à une instance).

14
répondu Carl Meyer 2008-09-21 18:22:46

J'ai trouvé L'idée D'Armin très utile. Voici ma variation;

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 d'attente. La différence est (à part les noms) qu'il met en cache uniquement les champs locaux non-relation. En d'autres termes, il ne cache pas 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 dict après l'enregistrement. Mais je ne voulais pas écraser la méthode save () car la plupart du temps, nous rejetons les instances de modèle après économie.

def save(self, *args, **kwargs):
    super(Klass, self).save(*args, **kwargs)
    self._original_state = self._as_dict()
19
répondu muhuk 2008-12-01 23:05:17

Django envoie actuellement toutes les colonnes à la base de données, même si vous venez d'en changer une. Pour changer cela, quelques changements dans le 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 sales au Modèle et en y ajoutant des noms de colonnes, chaque fois que vous __set__ une valeur de colonne.

Si vous avez besoin de cette fonctionnalité, je vous suggère de regarder L'ORM Django, de l'implémenter et de mettre un patch dans le trac Django. Il devrait être très facile à ajouter cela et cela aiderait aussi les autres utilisateurs. Lorsque vous faites cela, ajoutez un crochet qui est 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'objet 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'est pas testé mais devrait fonctionner. Lorsque vous appelez model.get_changed_columns() vous obtenez un dict de toutes les valeurs modifiées. Cela ne fonctionnera bien sûr pas pour les objets mutables dans les colonnes car l'état d'origine est une copie plate du dict.

11
répondu Armin Ronacher 2008-09-21 16:33:51

J'ai étendu la solution de Trey Hunner pour prendre en charge les relations m2m. Espérons 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 aussi vouloir fusionner les deux listes. Pour cela, remplacez la dernière ligne

return changed_fields, changed_m2m_fields

Avec

changed_fields.update(changed_m2m_fields)
return changed_fields
6
répondu Tony Abou-Assaleh 2013-02-13 18:10:41

Ajout d'une deuxième réponse car Beaucoup de choses ont changé depuis la publication initiale de cette question.

Il y a un certain nombre d'applications dans le monde Django qui résolvent ce problème maintenant. Vous pouvez trouver une liste complète des applications d'audit de modèle et d'historique sur le site Django Packages.

J'ai écrit un blog comparaison de quelques-unes de ces applications. Ce poste est maintenant 4 ans et il est un peu daté. Les différentes approches pour résoudre ce problème semblent être même si.

Les approches:

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

Le paquetdjango-reversion semble toujours être la solution la plus populaire à ce problème. Il prend la première approche: sérialiser les changements au lieu de mettre en miroir les tables.

J'ai relancé django-simple-histoire quelques années en arrière. Il prend la deuxième approche: miroir chaque table.

Je recommande donc d'utiliser une application pour résoudre ce problème . Il y a quelques populaires qui fonctionnent assez bien à ce stade.

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

5
répondu Trey Hunner 2015-12-31 22:11:05

En continuant sur la suggestion de Muhuk et en ajoutant les signaux de Django et un dispatch_uid unique, vous pouvez réinitialiser l'état sur save sans remplacer 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]])

Qui nettoierait l'état d'origine une fois enregistré sans avoir à remplacer save(). Le code fonctionne mais ne sait pas quelle est la pénalité de performance de la connexion des signaux à _ _ init_ _

3
répondu 2009-11-11 15:43:49

J'ai étendu les solutions de muhuk et smn pour inclure la vérification des différences sur les clés primaires pour les champs de clé étrangère et 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 ceci:

class MyModel(DirtyFieldsMixin, models.Model):
    ....
3
répondu Trey Hunner 2013-11-06 23:52:59

Si vous utilisez vos propres transactions (pas l'application d'administration par défaut), vous pouvez enregistrer les versions avant et après de votre objet. Vous pouvez enregistrer la version avant la session, ou vous pouvez le mettre dans "caché" des champs dans le formulaire. Champs cachés est 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, bien sûr, vous devez récupérer l'objet précédent afin de pouvoir y apporter des modifications. Si vous avez plusieurs façons de surveillez 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()
2
répondu S.Lott 2008-09-21 12:30:39

Une solution mise à jour avec le support m2m (en utilisant mis à jour dirtyfields et nouvelle _META API et quelques corrections de bugs), basé sur @Trey et @Tony ci-dessus. Cela a passé quelques tests de lumière de base 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()
0
répondu Neil 2015-12-30 08:26:30

Pour l'information de tout le monde, la solution de muhuk échoue sous python2.6 car elle soulève une exception indiquant ' object.__ init _ _ () ' n'accepte aucun argument...

Modifier: ho! apparemment, j'ai peut-être abusé du mixin... Je n'ai pas fait attention et l'ai déclaré comme le dernier parent et à cause de cela l'appel à init a fini dans le parent d'objet plutôt que le parent suivant comme il le ferait noramlly avec l'héritage de diagramme de diamant! veuillez donc ignorer mon commentaire:)

-1
répondu 2009-06-23 18:04:20