Lors de l'enregistrement, comment pouvez-vous vérifier si un champ a changé?

293

Dans mon modèle, j'ai:

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
 used when the alias is made")
    image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")


    def save(self, *args, **kw):
        if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
            try :
                data = utils.fetch(self.remote_image)
                image = StringIO.StringIO(data)
                image = Image.open(image)
                buf = StringIO.StringIO()
                image.save(buf, format='PNG')
                self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
            except IOError :
                pass

Ce qui fonctionne très bien pour la première fois les remote_imagechangements.

Comment puis-je récupérer une nouvelle image lorsque quelqu'un a modifié remote_imagel'alias? Et deuxièmement, existe-t-il un meilleur moyen de mettre en cache une image distante?

Paul Tarjan
la source

Réponses:

424

Essentiellement, vous souhaitez remplacer la __init__méthode de models.Modelafin de conserver une copie de la valeur d'origine. Cela vous évite d'avoir à effectuer une autre recherche de base de données (ce qui est toujours une bonne chose).

class Person(models.Model):
    name = models.CharField()

    __original_name = None

    def __init__(self, *args, **kwargs):
        super(Person, self).__init__(*args, **kwargs)
        self.__original_name = self.name

    def save(self, force_insert=False, force_update=False, *args, **kwargs):
        if self.name != self.__original_name:
            # name changed - do something here

        super(Person, self).save(force_insert, force_update, *args, **kwargs)
        self.__original_name = self.name
Josh
la source
24
au lieu d'écraser init, j'utiliserais le post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo
22
La substitution des méthodes est recommandée par la documentation Django: docs.djangoproject.com/en/dev/topics/db/models/…
Colonel Sponsz
10
@callum pour que si vous apportez des modifications à l'objet, enregistrez-le, puis apportez des modifications supplémentaires et appelez- save()le ENCORE, il fonctionnera toujours correctement.
philfreo
17
@Josh ne sera pas un problème si vous avez plusieurs serveurs d'applications fonctionnant avec la même base de données car il ne suit que les changements dans la mémoire
Jens Alm
13
@lajarre, je pense que votre commentaire est un peu trompeur. Les documents suggèrent que vous fassiez attention lorsque vous le faites. Ils ne le déconseillent pas.
Josh
199

J'utilise le mixin suivant:

from django.forms.models import model_to_dict


class ModelDiffMixin(object):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """

    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self.__initial = self._dict

    @property
    def diff(self):
        d1 = self.__initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        super(ModelDiffMixin, self).save(*args, **kwargs)
        self.__initial = self._dict

    @property
    def _dict(self):
        return model_to_dict(self, fields=[field.name for field in
                             self._meta.fields])

Usage:

>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>

Remarque

Veuillez noter que cette solution fonctionne bien dans le contexte de la demande actuelle uniquement. Il convient donc principalement aux cas simples. Dans un environnement simultané où plusieurs demandes peuvent manipuler la même instance de modèle en même temps, vous avez certainement besoin d'une approche différente.

iperelivskiy
la source
4
Vraiment parfait et n'effectue pas de requête supplémentaire. Merci beaucoup !
Stéphane
28
+1 pour un mixin utilisant. +1 pour aucun coup DB supplémentaire. +1 pour de nombreuses méthodes / propriétés utiles. Je dois pouvoir voter plusieurs fois.
Jake
Ouais. Plus un pour utiliser Mixin et pas de hit supplémentaire.
David S
2
Mixin est génial, mais cette version a des problèmes lorsqu'elle est utilisée avec .only (). L'appel à Model.objects.only ('id') entraînera une récursion infinie si Model a au moins 3 champs. Pour résoudre ce problème, nous devons supprimer les champs différés de l'enregistrement dans la propriété initiale et modifier un peu la
_dict
19
Tout comme la réponse de Josh, ce code fonctionnera trompeusement sur votre serveur de test à processus unique, mais au moment où vous le déployez sur n'importe quel type de serveur multi-traitement, il donnera des résultats incorrects. Vous ne pouvez pas savoir si vous modifiez la valeur dans la base de données sans interroger la base de données.
rspeer
154

Le meilleur moyen est avec un pre_savesignal. Peut-être n'était-ce pas une option en 2009 quand cette question a été posée et répondue, mais quiconque le voit aujourd'hui devrait le faire de cette façon:

@receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something
Chris Pratt
la source
6
Pourquoi est-ce le meilleur moyen si la méthode décrite par Josh ci-dessus n'implique pas un hit de base de données supplémentaire?
joshcartme
36
1) cette méthode est un hack, les signaux sont essentiellement conçus pour des utilisations comme celle-ci 2) cette méthode nécessite d'apporter des modifications à votre modèle, celui-ci ne le fait pas 3) comme vous pouvez le lire dans les commentaires sur cette réponse, cela a des effets secondaires qui peut être potentiellement problématique, cette solution ne le fait pas
Chris Pratt
2
Cette méthode est idéale si vous vous souciez uniquement de saisir la modification juste avant d'enregistrer. Cependant, cela ne fonctionnera pas si vous souhaitez réagir immédiatement au changement. J'ai rencontré ce dernier scénario à plusieurs reprises (et je travaille sur une telle instance maintenant).
Josh
5
@Josh: Que voulez-vous dire par "réagir immédiatement au changement"? En quoi cela ne vous laisse-t-il pas «réagir»?
Chris Pratt
2
Désolé, j'ai oublié la portée de cette question et je faisais référence à un problème entièrement différent. Cela dit, je pense que les signaux sont un bon moyen d'aller ici (maintenant qu'ils sont disponibles). Cependant, je trouve que beaucoup de gens envisagent de remplacer l'enregistrement d'un "hack". Je ne pense pas que ce soit le cas. Comme cette réponse le suggère ( stackoverflow.com/questions/170337/… ), je pense que la substitution est la meilleure pratique lorsque vous ne travaillez pas sur des modifications "spécifiques au modèle en question". Cela dit, je n'ai pas l'intention d'imposer cette croyance à quiconque.
Josh
138

Et maintenant, pour une réponse directe: une façon de vérifier si la valeur du champ a changé est de récupérer les données d'origine de la base de données avant d'enregistrer l'instance. Considérez cet exemple:

class MyModel(models.Model):
    f1 = models.CharField(max_length=1)

    def save(self, *args, **kw):
        if self.pk is not None:
            orig = MyModel.objects.get(pk=self.pk)
            if orig.f1 != self.f1:
                print 'f1 changed'
        super(MyModel, self).save(*args, **kw)

La même chose s'applique lorsque vous travaillez avec un formulaire. Vous pouvez le détecter à l'aide de la méthode de nettoyage ou d'enregistrement d'un ModelForm:

class MyModelForm(forms.ModelForm):

    def clean(self):
        cleaned_data = super(ProjectForm, self).clean()
        #if self.has_changed():  # new instance or existing updated (form has data to save)
        if self.instance.pk is not None:  # new instance only
            if self.instance.f1 != cleaned_data['f1']:
                print 'f1 changed'
        return cleaned_data

    class Meta:
        model = MyModel
        exclude = []
zgoda
la source
24
La solution de Josh est beaucoup plus conviviale pour les bases de données. Un appel supplémentaire pour vérifier ce qui a changé coûte cher.
dd.
5
Une lecture supplémentaire avant d'écrire n'est pas si chère. De plus, la méthode de suivi des modifications ne fonctionne pas s'il y a plusieurs demandes. Bien que cela souffre d'une condition de concurrence entre l'extraction et la sauvegarde.
dalore
1
Arrêtez de dire aux gens de vérifier que pk is not Nonecela ne s'applique pas, par exemple si vous utilisez un UUIDField. C'est juste un mauvais conseil.
user3467349
2
@dalore vous pouvez éviter la condition de course en décorant la méthode de sauvegarde avec@transaction.atomic
Frank Pape
2
@dalore bien que vous deviez vous assurer que le niveau d'isolement des transactions est suffisant. Dans postgresql, la lecture par défaut est validée, mais une lecture répétable est nécessaire .
Frank Pape
58

Depuis la sortie de Django 1.8, vous pouvez utiliser la méthode de classe from_db pour mettre en cache l'ancienne valeur de remote_image. Ensuite, dans la méthode de sauvegarde , vous pouvez comparer l'ancienne et la nouvelle valeur du champ pour vérifier si la valeur a changé.

@classmethod
def from_db(cls, db, field_names, values):
    new = super(Alias, cls).from_db(db, field_names, values)
    # cache value went from the base
    new._loaded_remote_image = values[field_names.index('remote_image')]
    return new

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    if (self._state.adding and self.remote_image) or \
        (not self._state.adding and self._loaded_remote_image != self.remote_image):
        # If it is first save and there is no cached remote_image but there is new one, 
        # or the value of remote_image has changed - do your stuff!
Serge
la source
1
Merci - voici une référence aux documents: docs.djangoproject.com/en/1.8/ref/models/instances/… . Je crois que cela entraîne toujours le problème susmentionné où la base de données peut changer entre le moment où cela est évalué et le moment où la comparaison est effectuée, mais c'est une nouvelle option intéressante.
trpt4him
1
Plutôt que de rechercher des valeurs (qui est O (n) en fonction du nombre de valeurs), ne serait-il pas plus rapide et plus clair de le faire new._loaded_remote_image = new.remote_image?
dalore
1
Malheureusement, je dois annuler mon commentaire précédent (maintenant supprimé). Tant que from_dbest appelé par refresh_from_db, les attributs de l'instance (c'est-à-dire chargés ou précédents) ne sont pas mis à jour. Par conséquent, je ne peux trouver aucune raison pour laquelle cela vaut mieux que __init__que vous avez encore besoin de gérer 3 cas: __init__/ from_db, refresh_from_db, et save.
claytond
18

Si vous utilisez un formulaire, vous pouvez utiliser les données modifiées du formulaire ( docs ):

class AliasForm(ModelForm):

    def save(self, commit=True):
        if 'remote_image' in self.changed_data:
            # do things
            remote_image = self.cleaned_data['remote_image']
            do_things(remote_image)
        super(AliasForm, self).save(commit)

    class Meta:
        model = Alias
laffuste
la source
6

Je suis un peu en retard à la fête mais j'ai aussi trouvé cette solution: Django Dirty Fields

Fred Campos
la source
5

Cela fonctionne pour moi dans Django 1.8

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something
jhrs21
la source
4

Vous pouvez utiliser django-model-changes pour ce faire sans recherche de base de données supplémentaire:

from django.dispatch import receiver
from django_model_changes import ChangesMixin

class Alias(ChangesMixin, MyBaseModel):
   # your model

@receiver(pre_save, sender=Alias)
def do_something_if_changed(sender, instance, **kwargs):
    if 'remote_image' in instance.changes():
        # do something
Robert Kajic
la source
4

Une autre réponse tardive, mais si vous essayez simplement de voir si un nouveau fichier a été téléchargé dans un champ de fichier, essayez ceci: (adapté du commentaire de Christopher Adams sur le lien http://zmsmith.com/2010/05/django -check-si-un-champ-a-changé / dans le commentaire de zach ici)

Lien mis à jour: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed/

def save(self, *args, **kw):
    from django.core.files.uploadedfile import UploadedFile
    if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
        # Handle FileFields as special cases, because the uploaded filename could be
        # the same as the filename that's already there even though there may
        # be different file contents.

        # if a file was just uploaded, the storage model with be UploadedFile
        # Do new file stuff here
        pass
Aaron McMillin
la source
C'est une excellente solution pour vérifier si un nouveau fichier a été téléchargé. Bien mieux que de vérifier le nom par rapport à la base de données car le nom du fichier pourrait être le même. Vous pouvez également l'utiliser dans le pre_saverécepteur. Merci d'avoir partagé ça!
DataGreed
1
Voici un exemple de mise à jour de la durée audio dans une base de données lorsque le fichier a été mis à jour à l'aide de mutagen pour lire les informations audio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed
3

La solution optimale est probablement celle qui n'inclut pas d'opération de lecture de base de données supplémentaire avant d'enregistrer l'instance de modèle, ni aucune autre bibliothèque django. C'est pourquoi les solutions de laffuste sont préférables. Dans le contexte d'un site d'administration, on peut simplement remplacer la save_modelméthode -m et y invoquer la has_changedméthode du formulaire , comme dans la réponse de Sion ci-dessus. Vous arrivez à quelque chose comme ça, en vous inspirant du paramètre d'exemple de Sion mais en utilisant changed_datapour obtenir tous les changements possibles:

class ModelAdmin(admin.ModelAdmin):
   fields=['name','mode']
   def save_model(self, request, obj, form, change):
     form.changed_data #output could be ['name']
     #do somethin the changed name value...
     #call the super method
     super(self,ModelAdmin).save_model(request, obj, form, change)
  • Remplacer save_model:

https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

  • Intégré changed_data-method pour un champ:

https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data

user3061675
la source
2

Bien que cela ne réponde pas réellement à votre question, je procéderais différemment.

Effacez simplement le remote_imagechamp après avoir enregistré avec succès la copie locale. Ensuite, dans votre méthode d'enregistrement, vous pouvez toujours mettre à jour l'image à chaque foisremote_image n'est pas vide.

Si vous souhaitez conserver une référence à l'URL, vous pouvez utiliser un champ booléen non modifiable pour gérer l'indicateur de mise en cache plutôt que le remote_imagechamp lui-même.

SmileyChris
la source
2

J'ai eu cette situation avant que ma solution ne remplace la pre_save()méthode de la classe de champ cible, elle ne sera appelée que si le champ a été modifié
utile avec l'exemple FileField:

class PDFField(FileField):
    def pre_save(self, model_instance, add):
        # do some operations on your file 
        # if and only if you have changed the filefield

inconvénient:
pas utile si vous souhaitez effectuer une opération (post_save) comme utiliser l'objet créé dans un travail (si certains champs ont changé)

MYaser
la source
2

amélioration de la réponse @josh pour tous les domaines:

class Person(models.Model):
  name = models.CharField()

def __init__(self, *args, **kwargs):
    super(Person, self).__init__(*args, **kwargs)
    self._original_fields = dict([(field.attname, getattr(self, field.attname))
        for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])

def save(self, *args, **kwargs):
  if self.id:
    for field in self._meta.local_fields:
      if not isinstance(field, models.ForeignKey) and\
        self._original_fields[field.name] != getattr(self, field.name):
        # Do Something    
  super(Person, self).save(*args, **kwargs)

juste pour clarifier, le getattr fonctionne pour obtenir des champs comme person.nameavec des chaînes (iegetattr(person, "name")

Hassek
la source
Et il ne fait toujours pas de requêtes DB supplémentaires?
andilabs
J'essayais d'implémenter votre code. Cela fonctionne bien en modifiant les champs. Mais maintenant, j'ai du mal à insérer de nouveaux. J'obtiens DoesNotExist pour mon champ FK en classe. Quelques conseils sur la façon de le résoudre seront appréciés.
andilabs
Je viens de mettre à jour le code, il ignore maintenant les clés étrangères, vous n'avez donc pas besoin de récupérer ces fichiers avec des requêtes supplémentaires (très cher) et si l'objet n'existe pas, il ignorera la logique supplémentaire.
Hassek
1

J'ai étendu le mixin de @livskiy comme suit:

class ModelDiffMixin(models.Model):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """
    _dict = DictField(editable=False)
    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self._initial = self._dict

    @property
    def diff(self):
        d1 = self._initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        object_dict = model_to_dict(self,
               fields=[field.name for field in self._meta.fields])
        for field in object_dict:
            # for FileFields
            if issubclass(object_dict[field].__class__, FieldFile):
                try:
                    object_dict[field] = object_dict[field].path
                except :
                    object_dict[field] = object_dict[field].name

            # TODO: add other non-serializable field types
        self._dict = object_dict
        super(ModelDiffMixin, self).save(*args, **kwargs)

    class Meta:
        abstract = True

et le DictField c'est:

class DictField(models.TextField):
    __metaclass__ = models.SubfieldBase
    description = "Stores a python dict"

    def __init__(self, *args, **kwargs):
        super(DictField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        if not value:
            value = {}

        if isinstance(value, dict):
            return value

        return json.loads(value)

    def get_prep_value(self, value):
        if value is None:
            return value
        return json.dumps(value)

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

il peut être utilisé en l'étendant dans vos modèles un champ _dict sera ajouté lorsque vous synchroniserez / migrez et ce champ stockera l'état de vos objets

MYaser
la source
1

Que diriez-vous d'utiliser la solution de David Cramer:

http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/

J'ai réussi à l'utiliser comme ceci:

@track_data('name')
class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.CharField(max_length=5)

    def save(self, *args, **kwargs):
        if self.has_changed('name'):
            print 'name changed'

    # OR #

    @classmethod
    def post_save(cls, sender, instance, created, **kwargs):
        if instance.has_changed('name'):
            print "Hooray!"
Sion
la source
2
Si vous oubliez super (Mode, self) .save (* args, ** kwargs), vous désactivez la fonction de sauvegarde, alors n'oubliez pas de le mettre dans la méthode de sauvegarde.
max
Le lien de l'article n'est plus à jour, c'est le nouveau lien: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop
1

Une modification de la réponse de @ ivanperelivskiy:

@property
def _dict(self):
    ret = {}
    for field in self._meta.get_fields():
        if isinstance(field, ForeignObjectRel):
            # foreign objects might not have corresponding objects in the database.
            if hasattr(self, field.get_accessor_name()):
                ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
            else:
                ret[field.get_accessor_name()] = None
        else:
            ret[field.attname] = getattr(self, field.attname)
    return ret

Cela utilise à la place la méthode publique de django 1.10 get_fields. Cela rend le code plus à l'épreuve du temps, mais plus important encore, inclut également les clés étrangères et les champs où editable = False.

Pour référence, voici l'implémentation de .fields

@cached_property
def fields(self):
    """
    Returns a list of all forward fields on the model and its parents,
    excluding ManyToManyFields.

    Private API intended only to be used by Django itself; get_fields()
    combined with filtering of field properties is the public API for
    obtaining this field list.
    """
    # For legacy reasons, the fields property should only contain forward
    # fields that are not private or with a m2m cardinality. Therefore we
    # pass these three filters as filters to the generator.
    # The third lambda is a longwinded way of checking f.related_model - we don't
    # use that property directly because related_model is a cached property,
    # and all the models may not have been loaded yet; we don't want to cache
    # the string reference to the related_model.
    def is_not_an_m2m_field(f):
        return not (f.is_relation and f.many_to_many)

    def is_not_a_generic_relation(f):
        return not (f.is_relation and f.one_to_many)

    def is_not_a_generic_foreign_key(f):
        return not (
            f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
        )

    return make_immutable_fields_list(
        "fields",
        (f for f in self._get_fields(reverse=False)
         if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
    )
theicfire
la source
1

Voici une autre façon de procéder.

class Parameter(models.Model):

    def __init__(self, *args, **kwargs):
        super(Parameter, self).__init__(*args, **kwargs)
        self.__original_value = self.value

    def clean(self,*args,**kwargs):
        if self.__original_value == self.value:
            print("igual")
        else:
            print("distinto")

    def save(self,*args,**kwargs):
        self.full_clean()
        return super(Parameter, self).save(*args, **kwargs)
        self.__original_value = self.value

    key = models.CharField(max_length=24, db_index=True, unique=True)
    value = models.CharField(max_length=128)

Selon la documentation: validation des objets

"La deuxième étape effectuée par full_clean () consiste à appeler Model.clean (). Cette méthode doit être remplacée pour effectuer une validation personnalisée sur votre modèle. Cette méthode doit être utilisée pour fournir une validation de modèle personnalisée et pour modifier les attributs de votre modèle si vous le souhaitez. Par exemple, vous pouvez l'utiliser pour fournir automatiquement une valeur pour un champ ou pour effectuer une validation qui nécessite l'accès à plusieurs champs: "

Gonzalo
la source
1

Il existe un attribut __dict__ qui a tous les champs comme clés et la valeur comme valeurs de champ. Nous pouvons donc simplement comparer deux d'entre eux

Modifiez simplement la fonction de sauvegarde du modèle en fonction ci-dessous

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
    if self.pk is not None:
        initial = A.objects.get(pk=self.pk)
        initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
        initial_json.pop('_state'), final_json.pop('_state')
        only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
        print(only_changed_fields)
    super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

Exemple d'utilisation:

class A(models.Model):
    name = models.CharField(max_length=200, null=True, blank=True)
    senior = models.CharField(choices=choices, max_length=3)
    timestamp = models.DateTimeField(null=True, blank=True)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is not None:
            initial = A.objects.get(pk=self.pk)
            initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
            initial_json.pop('_state'), final_json.pop('_state')
            only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
            print(only_changed_fields)
        super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

donne une sortie avec uniquement les champs qui ont été modifiés

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
Nimish Bansal
la source
1

Très tard dans le jeu, mais c'est une version de la réponse de Chris Pratt qui protège contre les conditions de course tout en sacrifiant les performances, en utilisant un transactionbloc etselect_for_update()

@receiver(pre_save, sender=MyModel)
@transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.select_for_update().get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something
baqyoteto
la source
0

en tant qu'extension de la réponse de SmileyChris, vous pouvez ajouter un champ datetime au modèle pour last_updated et définir une sorte de limite pour l'âge maximum auquel vous le laisserez arriver avant de vérifier une modification

Jiaaro
la source
0

Le mixin de @ivanlivski est super.

Je l'ai étendu à

  • Assurez-vous que cela fonctionne avec les champs décimaux.
  • Exposer les propriétés pour simplifier l'utilisation

Le code mis à jour est disponible ici: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Pour aider les nouveaux utilisateurs de Python ou Django, je vais donner un exemple plus complet. Cette utilisation particulière consiste à extraire un fichier d'un fournisseur de données et à garantir que les enregistrements de la base de données reflètent le fichier.

Mon objet modèle:

class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
    station_name = models.CharField(max_length=200)
    nearby_city = models.CharField(max_length=200)

    precipitation = models.DecimalField(max_digits=5, decimal_places=2)
    # <list of many other fields>

   def is_float_changed (self,v1, v2):
        ''' Compare two floating values to just two digit precision
        Override Default precision is 5 digits
        '''
        return abs (round (v1 - v2, 2)) > 0.01

La classe qui charge le fichier a ces méthodes:

class UpdateWeather (object)
    # other methods omitted

    def update_stations (self, filename):
        # read all existing data 
        all_stations = models.Station.objects.all()
        self._existing_stations = {}

        # insert into a collection for referencing while we check if data exists
        for stn in all_stations.iterator():
            self._existing_stations[stn.id] = stn

        # read the file. result is array of objects in known column order
        data = read_tabbed_file(filename)

        # iterate rows from file and insert or update where needed
        for rownum in range(sh.nrows):
            self._update_row(sh.row(rownum));

        # now anything remaining in the collection is no longer active
        # since it was not found in the newest file
        # for now, delete that record
        # there should never be any of these if the file was created properly
        for stn in self._existing_stations.values():
            stn.delete()
            self._num_deleted = self._num_deleted+1


    def _update_row (self, rowdata):
        stnid = int(rowdata[0].value) 
        name = rowdata[1].value.strip()

        # skip the blank names where data source has ids with no data today
        if len(name) < 1:
            return

        # fetch rest of fields and do sanity test
        nearby_city = rowdata[2].value.strip()
        precip = rowdata[3].value

        if stnid in self._existing_stations:
            stn = self._existing_stations[stnid]
            del self._existing_stations[stnid]
            is_update = True;
        else:
            stn = models.Station()
            is_update = False;

        # object is new or old, don't care here            
        stn.id = stnid
        stn.station_name = name;
        stn.nearby_city = nearby_city
        stn.precipitation = precip

        # many other fields updated from the file 

        if is_update == True:

            # we use a model mixin to simplify detection of changes
            # at the cost of extra memory to store the objects            
            if stn.has_changed == True:
                self._num_updated = self._num_updated + 1;
                stn.save();
        else:
            self._num_created = self._num_created + 1;
            stn.save()
sknutsonsf
la source
0

Si vous ne trouvez pas d'intérêt pour la saveméthode prioritaire , vous pouvez le faire

  model_fields = [f.name for f in YourModel._meta.get_fields()]
  valid_data = {
        key: new_data[key]
        for key in model_fields
        if key in new_data.keys()
  }

  for (key, value) in valid_data.items():
        if getattr(instance, key) != value:
           print ('Data has changed')

        setattr(instance, key, value)

 instance.save()
theTypan
la source