Ao salvar, como você pode verificar se um campo foi alterado?

293

No meu modelo eu tenho:

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

O que funciona muito bem pela primeira vez nas remote_imagemudanças.

Como posso buscar uma nova imagem quando alguém modificou remote_imageo alias? E segundo, existe uma maneira melhor de armazenar em cache uma imagem remota?

Paul Tarjan
fonte

Respostas:

423

Essencialmente, você deseja substituir o __init__método de models.Modelpara manter uma cópia do valor original. Isso faz com que você não precise fazer outra pesquisa no banco de dados (o que é sempre uma coisa boa).

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
fonte
24
em vez de substituir o init, eu usaria o sinal-post_init docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo
22
A substituição de métodos é recomendada pela documentação do Django: docs.djangoproject.com/en/dev/topics/db/models/…
Coronel Sponsz
10
@ callum para que, se você fizer alterações no objeto, salve-o e faça alterações adicionais e o chame save()novamente, ele ainda funcionará corretamente.
31412 philfreo
17
@ Josh não haverá um problema se você tiver vários servidores de aplicativos que trabalham contra o mesmo banco de dados, pois só controla as alterações na memória
Jens Alm
13
@ lajarre, acho que seu comentário é um pouco enganador. Os documentos sugerem que você tome cuidado ao fazê-lo. Eles não recomendam contra isso.
23412 Josh
199

Eu uso o seguinte 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

Observe que esta solução funciona bem apenas no contexto da solicitação atual. Portanto, é adequado principalmente para casos simples. No ambiente simultâneo em que várias solicitações podem manipular a mesma instância de modelo ao mesmo tempo, você definitivamente precisa de uma abordagem diferente.

iperelivskiy
fonte
4
Realmente perfeito e não executa consultas extras. Muito obrigado !
Stéphane
28
+1 para um mixin usando. +1 para nenhum acerto extra no banco de dados. +1 para muitos métodos / propriedades úteis. Eu preciso ser capaz de votar várias vezes.
Jake #
sim. Mais um por usar o Mixin e nenhum hit extra de db.
David S
2
Mixin é ótimo, mas esta versão tem problemas quando usada junto com .only (). A chamada para Model.objects.only ('id') levará a uma recursão infinita se o Modelo tiver pelo menos 3 campos. Para resolver isso, devemos remover campos diferido de poupança na propriedade inicial e mudança _dict um pouco
gleb.pitsevich
19
Assim como a resposta de Josh, esse código funcionará enganosamente no servidor de teste de processo único, mas no momento em que você o implantar em qualquer tipo de servidor de multiprocessamento, ele fornecerá resultados incorretos. Você não pode saber se está alterando o valor no banco de dados sem consultar o banco de dados.
rspeer
154

A melhor maneira é com um pre_savesinal. Pode não ter sido uma opção em 2009 quando essa pergunta foi feita e respondida, mas qualquer pessoa que esteja vendo isso hoje deve fazê-lo desta maneira:

@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
fonte
6
Por que essa é a melhor maneira se o método que Josh descreve acima não envolve um acerto extra no banco de dados?
Joshcartme #
36
1) esse método é um hack, os sinais são basicamente projetados para usos como este 2) esse método requer alterações no seu modelo, este não requer 3) como você pode ler nos comentários dessa resposta, tem efeitos colaterais que pode ser potencialmente problemático, esta solução não
Chris Pratt
2
Dessa forma, é ótimo se você só se preocupa em receber a alteração antes de salvar. No entanto, isso não funcionará se você quiser reagir à alteração imediatamente. Me deparei com o último cenário muitas vezes (e estou trabalhando em uma instância agora).
314 Josh
5
@ Jos: O que você quer dizer com "reagir à mudança imediatamente"? De que maneira isso não permite que você "reaja"?
Chris Pratt
2
Desculpe, esqueci o escopo desta pergunta e estava me referindo a um problema totalmente diferente. Dito isto, acho que os sinais são um bom caminho a percorrer aqui (agora que estão disponíveis). No entanto, acho que muitas pessoas consideram anuladoras, exceto um "hack". Não acredito que seja esse o caso. Como essa resposta sugere ( stackoverflow.com/questions/170337/… ), acho que substituir é a melhor prática quando você não está trabalhando em alterações "específicas do modelo em questão". Dito isto, não pretendo impor essa crença a ninguém.
314 Josh
138

E agora para resposta direta: uma maneira de verificar se o valor do campo foi alterado é buscar dados originais do banco de dados antes de salvar a instância. Considere este exemplo:

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)

O mesmo se aplica ao trabalhar com um formulário. Você pode detectá-lo no método clean ou save de um 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
fonte
24
A solução de Josh é muito mais amigável ao banco de dados. Uma ligação extra para verificar o que mudou mudou é cara.
dd.
5
Uma leitura extra antes de você escrever não é tão cara. Além disso, o método de alterações de rastreamento não funciona se houver várias solicitações. Embora isso sofra de uma condição de corrida entre buscar e salvar.
dalore 24/02
1
Pare de pk is not Nonepedir às pessoas para verificar se não se aplica, por exemplo, se você estiver usando um UUIDField. Este é apenas um mau conselho.
user3467349
2
@dalore você pode evitar a condição de corrida, decorando o método Save com@transaction.atomic
Frank Pape
2
@dalore, embora você precise garantir que o nível de isolamento da transação seja suficiente. No postgresql, o padrão é a leitura confirmada, mas a leitura repetível é necessária .
26616 Frank Pape
58

Desde o lançamento do Django 1.8, você pode usar o método de classe from_db para armazenar em cache o valor antigo da imagem_remota. Em seguida, no método save , você pode comparar o valor antigo e o novo do campo para verificar se o valor foi alterado.

@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!
Sarja
fonte
1
Obrigado - aqui está uma referência aos documentos: docs.djangoproject.com/en/1.8/ref/models/instances/… . Acredito que isso ainda resulte no problema mencionado acima, em que o banco de dados pode mudar entre quando isso é avaliado e quando a comparação é feita, mas essa é uma ótima opção nova.
trpt4him
1
Em vez de pesquisar valores (que é O (n) com base no número de valores), não seria mais rápido e mais claro new._loaded_remote_image = new.remote_image?
dalore
1
Infelizmente, tenho que reverter meu comentário anterior (agora excluído). Enquanto from_dbé chamado por refresh_from_db, os atributos na instância (ou seja, carregados ou anteriores) não são atualizados. Como resultado, eu não consigo encontrar nenhuma razão para isso é melhor do que __init__como você ainda precisa lidar com 3 casos: __init__/ from_db, refresh_from_db, e save.
Claytond
18

Se você estiver usando um formulário, poderá usar os dados alterados do formulário ( 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
fonte
6

Estou um pouco atrasado para a festa, mas encontrei esta solução também: Django Dirty Fields

Fred Campos
fonte
5

Isso funciona para mim no Django 1.8

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

Você pode usar o django-model-changes para fazer isso sem uma pesquisa adicional no banco de dados:

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

Outra resposta tardia, mas se você está apenas tentando ver se um novo arquivo foi carregado em um campo, tente o seguinte: (adaptado do comentário de Christopher Adams no link http://zmsmith.com/2010/05/django -check-if-a-field-mudou / no comentário de zach aqui)

Link atualizado: 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
fonte
Essa é uma solução incrível para verificar se um novo arquivo foi carregado. Muito melhor do que verificar o nome no banco de dados porque o nome do arquivo pode ser o mesmo. Você também pode usá-lo no pre_savereceptor. Obrigado por compartilhar isso!
DataGreed 02/12/19
1
Aqui está um exemplo para atualizar a duração de áudio em um banco de dados quando o arquivo foi atualizado usando mutagênico para a leitura de informações de áudio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed
3

A solução ideal provavelmente é aquela que não inclui uma operação de leitura de banco de dados adicional antes de salvar a instância do modelo, nem qualquer outra biblioteca django. É por isso que as soluções da laffuste são preferíveis. No contexto de um site de administração, pode-se simplesmente substituir o save_modelmétodo-e invocar o has_changedmétodo do formulário , como na resposta de Sion acima. Você chega a algo assim, usando changed_dataa configuração de exemplo de Sion, mas usando para obter todas as alterações possíveis:

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

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

  • Built-in changed_data-method para um campo:

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

user3061675
fonte
2

Embora isso não responda à sua pergunta, eu faria isso de uma maneira diferente.

Simplesmente limpe o remote_imagecampo após salvar com êxito a cópia local. Em seu método de salvamento, você sempre pode atualizar a imagem sempre que remote_imagenão estiver vazia.

Se você quiser manter uma referência ao URL, use um campo booleano não editável para manipular o sinalizador de cache, em vez do remote_imagepróprio campo.

SmileyChris
fonte
2

Eu tive essa situação antes de minha solução substituir o pre_save()método da classe de campo de destino, ela será chamada apenas se o campo tiver sido alterado
útil com o exemplo 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

desvantagem:
não é útil se você deseja executar qualquer operação (post_save) como usar o objeto criado em algum trabalho (se determinado campo foi alterado)

MYaser
fonte
2

melhorando a resposta @josh para todos os 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)

apenas para esclarecer, o getattr trabalha para obter campos como person.namecom strings (ou seja,getattr(person, "name")

Hassek
fonte
E ainda não está fazendo consultas extras de banco de dados?
andilabs
Eu estava tentando implementar seu código. Funciona bem editando campos. Mas agora eu tenho problema com a inserção de novo. Recebo DoesNotExist para o meu campo FK na classe. Algumas dicas de como resolvê-lo serão apreciadas.
30714 Andilabs
Acabei de atualizar o código, agora ele ignora as chaves estrangeiras para que você não precise buscar esses arquivos com consultas extras (muito caras) e, se o objeto não existir, ignorará a lógica extra.
Hassek
1

Estendi o mixin de @livskiy da seguinte maneira:

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

ele pode ser usado estendendo-o em seus modelos, um campo _dict será adicionado quando você sincronizar / migrar e esse campo armazenará o estado de seus objetos

MYaser
fonte
1

Que tal usar a solução de David Cramer:

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

Eu tive sucesso usando-o assim:

@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
fonte
2
Se você esquecer super (Mode, self) .save (* args, ** kwargs), estará desativando a função de salvamento. Lembre-se de colocá-lo no método de salvamento.
máximo
O link do artigo está desatualizado, este é o novo link: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop
1

Uma modificação na resposta 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

Isso usa o método público do django 1.10 get_fields. Isso torna o código mais à prova de futuro, mas o mais importante também inclui chaves estrangeiras e campos em que editável = Falso.

Para referência, aqui está a implementação 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
fonte
1

Aqui está outra maneira de fazê-lo.

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)

Conforme documentação: validando objetos

"A segunda etapa que o full_clean () executa é chamar Model.clean (). Esse método deve ser substituído para executar a validação customizada no seu modelo. Esse método deve ser usado para fornecer validação de modelo customizado e modificar atributos em seu modelo, se desejado Por exemplo, você pode usá-lo para fornecer automaticamente um valor para um campo ou para fazer a validação que requer acesso a mais de um único campo: "

Gonzalo
fonte
1

Há um atributo __dict__ que possui todos os campos como as chaves e o valor como os valores do campo. Então, podemos apenas comparar dois deles

Basta alterar a função salvar do modelo para a função abaixo

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)

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

produz saída apenas com os campos que foram alterados

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

Muito tarde para o jogo, mas esta é uma versão da resposta de Chris Pratt que protege contra as condições da corrida enquanto sacrifica o desempenho, usando um transactionbloco 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
baqyoteto
fonte
0

como uma extensão da resposta do SmileyChris, você pode adicionar um campo de data e hora ao modelo para last_updated e definir algum tipo de limite para a idade máxima que você deixará atingir antes de verificar uma alteração

Jiaaro
fonte
0

O mixin de @ivanlivski é ótimo.

Eu estendi para

  • Verifique se ele funciona com campos decimais.
  • Expor propriedades para simplificar o uso

O código atualizado está disponível aqui: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Para ajudar as pessoas novas em Python ou Django, darei um exemplo mais completo. Esse uso específico é pegar um arquivo de um provedor de dados e garantir que os registros no banco de dados reflitam o arquivo.

Meu objeto de 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

A classe que carrega o arquivo possui estes 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
fonte
0

Se você não encontrar interesse no savemétodo de substituição , poderá fazer

  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
fonte