Al guardar, ¿cómo puede verificar si un campo ha cambiado?

293

En mi modelo tengo:

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

Lo cual funciona muy bien por primera vez los remote_imagecambios.

¿Cómo puedo obtener una nueva imagen cuando alguien ha modificado remote_imageel alias? Y en segundo lugar, ¿hay una mejor manera de almacenar en caché una imagen remota?

Paul Tarjan
fuente

Respuestas:

424

Esencialmente, desea anular el __init__método de models.Modelmodo que conserve una copia del valor original. Esto hace que no tenga que hacer otra búsqueda de DB (que siempre es algo bueno).

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
fuente
24
en lugar de sobrescribir init, usaría post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo el
22
La documentación de Django recomienda los métodos de anulación
Coronel Sponsz
10
@callum para que si realiza cambios en el objeto, guárdelo, realice cambios adicionales y save()vuelva a llamarlo , seguirá funcionando correctamente.
philfreo
17
@Josh no habrá ningún problema si tienes varios servidores de aplicaciones trabajando en la misma base de datos, ya que solo rastrea los cambios en la memoria
Jens Alm
13
@lajarre, creo que tu comentario es un poco engañoso. Los documentos sugieren que tenga cuidado cuando lo haga. No recomiendan contra eso.
Josh el
199

Yo uso siguiente 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

Tenga en cuenta que esta solución funciona bien solo en el contexto de la solicitud actual. Por lo tanto, es adecuado principalmente para casos simples. En un entorno concurrente donde múltiples solicitudes pueden manipular la misma instancia de modelo al mismo tiempo, definitivamente necesita un enfoque diferente.

iperelivskiy
fuente
44
Realmente perfecto, y no realice consultas adicionales. Muchas gracias !
Stéphane
28
+1 para usar mixin. +1 para ningún golpe extra de DB. +1 para muchos métodos / propiedades útiles. Necesito poder votar varias veces.
Jake
Si. Más uno para usar Mixin y ningún golpe de db adicional.
David S
2
Mixin es genial, pero esta versión tiene problemas cuando se usa junto con .only (). La llamada a Model.objects.only ('id') conducirá a una recursión infinita si Model tiene al menos 3 campos. Para resolver esto, debemos eliminar los campos diferidos del guardado en la inicial y cambiar un poco la
_dict
19
Al igual que la respuesta de Josh, este código funcionará engañosamente bien en su servidor de prueba de proceso único, pero en el momento en que lo implemente en cualquier tipo de servidor de procesamiento múltiple, dará resultados incorrectos. No puede saber si está cambiando el valor en la base de datos sin consultar la base de datos.
rspeer
154

La mejor manera es con una pre_saveseñal. Puede que no haya sido una opción en el '09 cuando se hizo y respondió esta pregunta, pero cualquiera que vea esto hoy debería hacerlo de esta manera:

@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
fuente
66
¿Por qué es esta la mejor manera si el método que Josh describe arriba no involucra un impacto adicional en la base de datos?
joshcartme
36
1) ese método es un truco, las señales están diseñadas básicamente para usos como este 2) ese método requiere hacer modificaciones a su modelo, este no 3) como puede leer en los comentarios sobre esa respuesta, tiene efectos secundarios que puede ser potencialmente problemático, esta solución no lo hace
Chris Pratt
2
De esta manera, es excelente si solo le interesa ver el cambio justo antes de guardarlo. Sin embargo, esto no funcionará si desea reaccionar al cambio de inmediato. Me he encontrado con este último escenario muchas veces (y estoy trabajando en una de esas instancias ahora).
Josh
55
@ Josh: ¿Qué quieres decir con "reaccionar al cambio de inmediato"? ¿De qué manera esto no te deja "reaccionar"?
Chris Pratt
2
Lo siento, olvidé el alcance de esta pregunta y me refería a un problema completamente diferente. Dicho esto, creo que las señales son una buena manera de ir aquí (ahora que están disponibles). Sin embargo, creo que muchas personas consideran anular guardar un "hack". No creo que este sea el caso. Como sugiere esta respuesta ( stackoverflow.com/questions/170337/… ), creo que anular es la mejor práctica cuando no está trabajando en cambios que son "específicos para el modelo en cuestión". Dicho esto, no pretendo imponer esa creencia a nadie.
Josh
138

Y ahora para una respuesta directa: una forma de verificar si el valor del campo ha cambiado es obtener datos originales de la base de datos antes de guardar la instancia. Considere este ejemplo:

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)

Lo mismo se aplica cuando se trabaja con un formulario. Puede detectarlo en el método de limpieza o guardado de 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
fuente
24
La solución de Josh es mucho más amigable con la base de datos. Una llamada adicional para verificar lo que ha cambiado es costosa.
dd.
55
Una lectura adicional antes de escribir no es tan costosa. Además, el método de seguimiento de cambios no funciona si hay varias solicitudes. Aunque esto sufriría una condición de carrera entre buscar y guardar.
Dalore
1
Dejar de decirle a la gente que verifique pk is not Noneque no se aplica, por ejemplo, si usa un UUIDField Esto es solo un mal consejo.
user3467349
2
@dalore puedes evitar la condición de carrera decorando el método de guardar con@transaction.atomic
Frank Pape
2
@dalore, aunque necesitaría asegurarse de que el nivel de aislamiento de la transacción sea suficiente. En postgresql, el valor predeterminado es lectura confirmada, pero es necesaria una lectura repetible .
Frank Pape
58

Desde el lanzamiento de Django 1.8, puede usar from_db classmethod para almacenar en caché el valor anterior de remote_image. Luego, en el método de guardar , puede comparar el valor antiguo y el nuevo del campo para verificar si el valor ha cambiado.

@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!
Sarga
fuente
1
Gracias. Aquí hay una referencia a los documentos: docs.djangoproject.com/en/1.8/ref/models/instances/… . Creo que esto todavía da como resultado el problema antes mencionado en el que la base de datos puede cambiar entre cuando se evalúa esto y cuando se realiza la comparación, pero esta es una buena opción nueva.
trpt4him
1
En lugar de buscar valores (que es O (n) según el número de valores), ¿no sería más rápido y claro new._loaded_remote_image = new.remote_image?
Dalore
1
Lamentablemente, tengo que revertir mi comentario anterior (ahora eliminado). Mientras from_dbse llama por refresh_from_db, los atributos de la instancia (es decir, cargados o anteriores) no se actualizan. Como resultado, no puedo encontrar ninguna razón por la que esto es mejor que __init__ya que todavía necesita manejar 3 casos: __init__/ from_db, refresh_from_db, y save.
claytond
18

Si está usando un formulario, puede usar el formulario changed_data ( docs ) del formulario :

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
fuente
5

Esto funciona para mí en Django 1.8

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

Puede usar django-model-changes para hacer esto sin una búsqueda adicional en la base de datos:

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
fuente
4

Otra respuesta tardía, pero si solo está tratando de ver si un nuevo archivo se ha cargado en un campo de archivo, intente esto: (adaptado del comentario de Christopher Adams en el enlace http://zmsmith.com/2010/05/django -check-if-a-field-has-changed / en el comentario de zach aquí)

Enlace actualizado: 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
fuente
Esa es una solución increíble para verificar si se cargó un nuevo archivo. Mucho mejor que verificar el nombre contra la base de datos porque el nombre del archivo podría ser el mismo. También puedes usarlo en el pre_savereceptor. ¡Gracias por compartir esto!
DataGreed
1
He aquí un ejemplo para la actualización de la duración de audio en una base de datos cuando el archivo se actualiza utilizando mutágeno para la lectura de información de audio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed
3

La solución óptima es probablemente una que no incluya una operación de lectura de base de datos adicional antes de guardar la instancia del modelo, ni ninguna otra biblioteca django. Es por eso que las soluciones de laffuste son preferibles. En el contexto de un sitio de administración, uno simplemente puede anular el save_modelmétodo e invocar el has_changedmétodo del formulario allí, tal como en la respuesta anterior de Sion. Llegas a algo como esto, recurriendo a la configuración de ejemplo de Sion pero usando changed_datapara obtener todos los cambios posibles:

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

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

  • changed_dataMétodo incorporado para un campo:

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

usuario3061675
fuente
2

Si bien esto en realidad no responde a su pregunta, abordaría esto de una manera diferente.

Simplemente borre el remote_imagecampo después de guardar con éxito la copia local. Luego, en su método de guardar, siempre puede actualizar la imagen siempre remote_imageque no esté vacía.

Si desea mantener una referencia a la URL, puede usar un campo booleano no editable para manejar la bandera de almacenamiento en caché en lugar del remote_imagecampo en sí.

SmileyChris
fuente
2

Tuve esta situación antes de que mi solución fuera anular el pre_save()método de la clase de campo de destino, se
invocará solo si el campo se ha cambiado útil con el ejemplo de 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

desventaja:
no es útil si desea realizar alguna operación (post_save) como usar el objeto creado en algún trabajo (si cierto campo ha cambiado)

MYaser
fuente
2

mejorando la respuesta @josh para todos los campos:

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 para aclarar, el getattr funciona para obtener campos como person.namecon cadenas (es decirgetattr(person, "name")

Hassek
fuente
¿Y todavía no está haciendo consultas adicionales de db?
andilabs
Estaba intentando implementar tu código. Funciona bien editando campos. Pero ahora tengo problemas para insertar nuevos. Obtengo DoesNotExist para mi campo FK en clase. Se apreciará alguna pista sobre cómo resolverlo.
andilabs
Acabo de actualizar el código, ahora omite las claves externas, por lo que no necesita buscar esos archivos con consultas adicionales (muy caras) y si el objeto no existe, omitirá la lógica adicional.
Hassek
1

He extendido la mezcla de @livskiy de la siguiente manera:

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

y el DictField es:

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)

se puede usar extendiéndolo en sus modelos, se agregará un campo _dict cuando sincronice / migre y ese campo almacenará el estado de sus objetos

MYaser
fuente
1

¿Qué tal usar la solución de David Cramer?

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

He tenido éxito al usarlo así:

@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
fuente
2
Si olvida super (Mode, self) .save (* args, ** kwargs), entonces está deshabilitando la función de guardar, así que recuerde poner esto en el método de guardar.
máximo
El enlace del artículo está desactualizado, este es el nuevo enlace: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop
1

Una modificación a la respuesta 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

Esto utiliza el método público de django 1.10 get_fields lugar. Esto hace que el código sea más a prueba de futuro, pero lo más importante también incluye claves externas y campos donde editable = False.

Como referencia, aquí está la implementación 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
fuente
1

Aquí hay otra forma de hacerlo.

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)

Según la documentación: validando objetos

"El segundo paso que realiza full_clean () es llamar a Model.clean (). Este método debe anularse para realizar una validación personalizada en su modelo. Este método debe usarse para proporcionar una validación de modelo personalizada y para modificar atributos en su modelo si lo desea Por ejemplo, podría usarlo para proporcionar automáticamente un valor para un campo, o para realizar una validación que requiere acceso a más de un solo campo: "

Gonzalo
fuente
1

Hay un atributo __dict__ que tiene todos los campos como claves y valores como valores de campo. Entonces podemos comparar dos de ellos

Simplemente cambie la función de guardar del modelo a la siguiente función

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)

Ejemplo de uso:

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 resultados con solo aquellos campos que han sido cambiados

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

Muy tarde al juego, pero esta es una versión de la respuesta de Chris Pratt que protege contra las condiciones de carrera al tiempo que sacrifica el rendimiento, mediante el uso de un transactionbloqueo yselect_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
fuente
0

Como una extensión de la respuesta de SmileyChris, puede agregar un campo de fecha y hora al modelo para last_updated, y establecer algún tipo de límite para la edad máxima a la que lo dejará llegar antes de verificar un cambio

Jiaaro
fuente
0

El mixin de @ivanlivski es genial.

Lo he extendido a

  • Asegúrese de que funciona con campos decimales.
  • Exponer propiedades para simplificar el uso

El código actualizado está disponible aquí: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Para ayudar a las personas nuevas en Python o Django, daré un ejemplo más completo. Este uso particular es tomar un archivo de un proveedor de datos y garantizar que los registros en la base de datos reflejen el archivo.

Mi objeto modelo:

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 clase que carga el archivo tiene estos métodos:

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
fuente
0

Si no encuentra interés en anular el savemétodo, puede hacerlo

  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
fuente