Al momento del salvataggio, come è possibile verificare se un campo è cambiato?


293

Nel mio modello ho:

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

Che funziona alla grande per la prima volta i remote_imagecambiamenti.

Come posso recuperare una nuova immagine quando qualcuno ha modificato remote_imagel'alias? E in secondo luogo, esiste un modo migliore per memorizzare nella cache un'immagine remota?

Risposte:


424

In sostanza, si desidera sovrascrivere il __init__metodo in models.Modelmodo da conservare una copia del valore originale. Questo lo rende in modo da non dover fare un'altra ricerca DB (che è sempre una buona cosa).

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

24
invece di sovrascrivere init, userei post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo

22
I metodi di sostituzione sono raccomandati dalla documentazione di Django: docs.djangoproject.com/en/dev/topics/db/models/…
Colonnello Sponsz,

10
@callum in modo che se si apportano modifiche all'oggetto, salvarlo, quindi apportare ulteriori modifiche e richiamarlo save()ANCORA, funzionerà ancora correttamente.
philfreo,

17
@Josh non ci sarà alcun problema se hai diversi server applicazioni che lavorano sullo stesso database in quanto tiene traccia solo delle modifiche in memoria
Jens Alm

13
@lajarre, penso che il tuo commento sia un po 'fuorviante. I documenti suggeriscono che ti prendi cura di farlo. Non sconsigliano.
Josh,

199

Uso il seguente mixin:

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

Uso:

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

Nota

Questa soluzione funziona bene solo nel contesto della richiesta corrente. Quindi è adatto principalmente per casi semplici. In un ambiente concorrente in cui più richieste possono manipolare la stessa istanza del modello contemporaneamente, è necessario un approccio diverso.


4
Davvero perfetto e non eseguire query extra. Molte grazie !
Stéphane

28
+1 per un mixin usando. +1 per nessun hit DB aggiuntivo. +1 per molti metodi / proprietà utili. Devo poter votare più volte.
Jake,

si. Più uno per l'utilizzo di Mixin e nessun hit db aggiuntivo.
David S,

2
Mixin è fantastico, ma questa versione ha problemi se usato insieme a .only (). La chiamata a Model.objects.only ('id') porterà a una ricorsione infinita se Model ha almeno 3 campi. Per risolvere questo problema, dovremmo rimuovere i campi differiti dal salvataggio iniziale e modificare un po 'la
_dict

19
Proprio come la risposta di Josh, questo codice funzionerà in modo ingannevole sul tuo server di test a processo singolo, ma nel momento in cui lo distribuisci su qualsiasi tipo di server multi-elaborazione, otterrai risultati errati. Non puoi sapere se stai modificando il valore nel database senza interrogare il database.
rspeer

154

Il modo migliore è con un pre_savesegnale. Potrebbe non essere stata un'opzione nel '09 quando questa domanda è stata posta e risposta, ma chiunque la veda oggi dovrebbe farlo in questo modo:

@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

6
Perché questo è il modo migliore se il metodo descritto in precedenza da Josh non comporta un hit aggiuntivo del database?
joshcartme,

36
1) quel metodo è un hack, i segnali sono fondamentalmente progettati per usi come questo 2) quel metodo richiede di apportare modifiche al tuo modello, questo non lo fa 3) come puoi leggere nei commenti su quella risposta, ha effetti collaterali che può essere potenzialmente problematico, questa soluzione no
Chris Pratt,

2
In questo modo è ottimo se ti interessa solo prendere la modifica appena prima del salvataggio. Tuttavia, questo non funzionerà se si desidera reagire immediatamente alla modifica. Mi sono imbattuto in quest'ultimo scenario molte volte (e sto lavorando a una di queste istanze ora).
Josh,

5
@Josh: Cosa intendi con "reagire immediatamente al cambiamento"? In che modo questo non ti consente di "reagire"?
Chris Pratt,

2
Siamo spiacenti, ho dimenticato l'ambito di questa domanda e mi riferivo a un problema completamente diverso. Detto questo, penso che i segnali siano un buon modo per andare qui (ora che sono disponibili). Tuttavia, trovo che molte persone considerino prioritario salvare un "hack". Non credo che sia così. Come suggerisce questa risposta ( stackoverflow.com/questions/170337/… ), penso che l'override sia la migliore pratica quando non si lavora su modifiche "specifiche del modello in questione". Detto questo, non ho intenzione di imporre questa convinzione a nessuno.
Josh,

138

E ora per la risposta diretta: un modo per verificare se il valore per il campo è cambiato è quello di recuperare i dati originali dal database prima di salvare l'istanza. Considera questo esempio:

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 stessa cosa vale quando si lavora con un modulo. Puoi rilevarlo con il metodo clean o save di 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 = []

24
La soluzione di Josh è molto più intuitiva per i database. Un'ulteriore chiamata per verificare cosa è cambiato è costosa.
dd.

5
Una lettura extra prima di scrivere non è poi così costosa. Inoltre, il metodo di rilevamento delle modifiche non funziona se sono presenti più richieste. Anche se questo soffrirebbe di una condizione di razza tra recupero e salvataggio.
dalore,

1
Smetti di dire alle persone di controllare pk is not Noneche non si applichi ad esempio se usi un UUIDField. Questo è solo un cattivo consiglio.
user3467349

2
@dalore puoi evitare le condizioni di gara decorando il metodo di salvataggio con@transaction.atomic
Frank Pape il

2
@dalore anche se dovresti assicurarti che il livello di isolamento della transazione sia sufficiente. In postgresql, il valore predefinito viene letto come commit, ma è necessaria una lettura ripetibile .
Frank Pape,

58

Dalla versione di Django 1.8, puoi usare il metodo class from_db per memorizzare nella cache il vecchio valore di remote_image. Quindi nel metodo di salvataggio puoi confrontare il vecchio e il nuovo valore del campo per verificare se il valore è cambiato.

@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!

1
Grazie - ecco un riferimento alla documentazione: docs.djangoproject.com/en/1.8/ref/models/instances/… . Ritengo che ciò comporti ancora il problema sopra menzionato in cui il database può cambiare tra quando viene valutato e quando viene effettuato il confronto, ma questa è una nuova opzione piacevole.
trpt4him,

1
Invece di cercare tra i valori (che è O (n) in base al numero di valori) non sarebbe più veloce e chiaro new._loaded_remote_image = new.remote_image?
dalore,

1
Purtroppo devo invertire il mio commento precedente (ora cancellato). Mentre from_dbviene chiamato da refresh_from_db, gli attributi sull'istanza (cioè caricati o precedenti) non vengono aggiornati. Di conseguenza, non riesco a trovare alcun motivo per cui questo è meglio di __init__come è ancora necessario per gestire 3 casi: __init__/ from_db, refresh_from_db, e save.
Claytond,


18

Se si utilizza un modulo, è possibile utilizzare il file change_data ( docs ) del modulo :

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



5

Questo funziona per me in Django 1.8

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

4

È possibile utilizzare django-model-changes per farlo senza una ricerca aggiuntiva nel database:

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

4

Un'altra risposta in ritardo, ma se stai solo cercando di vedere se un nuovo file è stato caricato in un campo di file, prova questo: (adattato dal commento di Christopher Adams sul link http://zmsmith.com/2010/05/django -check-if-a-a-field-è-cambiato / nel commento di zach qui)

Link aggiornato: 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

Questa è un'ottima soluzione per verificare se è stato caricato un nuovo file. Molto meglio che controllare il nome contro il database perché il nome del file potrebbe essere lo stesso. Puoi usarlo anche nel pre_savericevitore. Grazie per averlo condiviso!
Data accordo

1
Ecco un esempio per l'aggiornamento della durata dell'audio in un database quando il file è stato aggiornato utilizzando mutagen per la lettura delle informazioni audio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed

3

La soluzione ottimale è probabilmente quella che non include un'operazione di lettura del database aggiuntiva prima di salvare l'istanza del modello, né qualsiasi ulteriore libreria django. Questo è il motivo per cui le soluzioni di laffuste sono preferibili. Nel contesto di un sito di amministrazione, si può semplicemente ignorare il save_modelmetodo e invocare has_changedlì il metodo del modulo , proprio come nella risposta di Sion sopra. Arrivi a qualcosa del genere, attingendo all'impostazione di esempio di Sion ma usando changed_dataper ottenere ogni possibile cambiamento:

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)
  • Ignora save_model:

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

  • Built-in changed_data-Metodo per un campo:

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


2

Anche se questo in realtà non risponde alla tua domanda, lo farei in modo diverso.

Basta cancellare il remote_imagecampo dopo aver salvato con successo la copia locale. Quindi nel tuo metodo di salvataggio puoi sempre aggiornare l'immagine ogni volta che remote_imagenon è vuota.

Se desideri mantenere un riferimento all'URL, puoi utilizzare un campo booleano non modificabile per gestire il flag di memorizzazione nella cache anziché il remote_imagecampo stesso.


2

Ho avuto questa situazione prima che la mia soluzione fosse quella di sovrascrivere il pre_save()metodo della classe del campo target che verrà chiamato solo se il campo è stato modificato
utile con l'esempio 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

svantaggio:
non utile se si desidera eseguire qualsiasi operazione (post_save) come l'utilizzo dell'oggetto creato in un lavoro (se un determinato campo è cambiato)


2

migliorare la risposta di @josh per tutti i campi:

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)

solo per chiarire, getattr lavora per ottenere campi come person.namecon le stringhe (esgetattr(person, "name")


E non sta ancora facendo ulteriori query db?
andilabs

Stavo cercando di implementare il tuo codice. Funziona bene modificando i campi. Ma ora ho problemi con l'inserimento di nuovi. Ottengo DoesNotExist per il mio campo FK in classe. Alcuni suggerimenti su come risolverlo saranno apprezzati.
andilabs

Ho appena aggiornato il codice, ora salta le chiavi esterne, quindi non è necessario recuperare quei file con query extra (molto costose) e se l'oggetto non esiste, salterà la logica aggiuntiva.
Hassek,

1

Ho esteso il mixin di @livskiy come segue:

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

e DictField è:

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)

può essere utilizzato estendendolo nei tuoi modelli verrà aggiunto un campo _dict quando si sincronizza / migra e quel campo memorizzerà lo stato dei tuoi oggetti


1

Che ne dici di usare la soluzione di David Cramer:

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

Ho avuto successo usandolo in questo modo:

@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!"

2
Se dimentichi super (Mode, self) .save (* args, ** kwargs) allora stai disabilitando la funzione di salvataggio, quindi ricorda di metterlo nel metodo di salvataggio.
max

Il link dell'articolo è obsoleto, questo è il nuovo link: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop

1

Una modifica alla risposta di @ 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

Questo utilizza get_fieldsinvece il metodo pubblico di django 1.10 . Questo rende il codice più a prova di futuro, ma soprattutto include chiavi e campi esterni dove editable = False.

Per riferimento, ecco l'implementazione di .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))
    )

1

Ecco un altro modo di farlo.

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)

Come da documentazione: validazione di oggetti

"Il secondo passaggio full_clean () esegue è chiamare Model.clean (). Questo metodo deve essere ignorato per eseguire la convalida personalizzata sul modello. Questo metodo deve essere utilizzato per fornire la convalida del modello personalizzato e, se lo si desidera, modificare gli attributi sul modello Ad esempio, è possibile utilizzarlo per fornire automaticamente un valore per un campo o per eseguire una convalida che richiede l'accesso a più di un singolo campo: "


1

C'è un attributo __dict__ che ha tutti i campi come chiavi e valore come valori di campo. Quindi possiamo solo confrontarne due

Basta cambiare la funzione di salvataggio del modello nella funzione di seguito

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)

Esempio di utilizzo:

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)

produce output solo con quei campi che sono stati modificati

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}

1

Molto tardi al gioco, ma questa è una versione della risposta di Chris Pratt che protegge dalle condizioni di gara sacrificando le prestazioni, usando un transactionblocco eselect_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

0

come estensione della risposta di SmileyChris, puoi aggiungere un campo datetime al modello per last_updated e impostare una sorta di limite per l'età massima che ti consentirà di raggiungere prima di verificare una modifica


0

Il mixin di @ivanlivski è fantastico.

L'ho esteso a

  • Assicurarsi che funzioni con i campi decimali.
  • Esporre le proprietà per semplificare l'utilizzo

Il codice aggiornato è disponibile qui: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Per aiutare le persone che non conoscono Python o Django, farò un esempio più completo. Questo particolare utilizzo consiste nel prendere un file da un fornitore di dati e assicurarsi che i record nel database riflettano il file.

Il mio oggetto modello:

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 che carica il file ha questi metodi:

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

0

Se non trovi interesse per il savemetodo di sostituzione , puoi farlo

  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()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.