it-swarm-pt.tech

Django: Como salvar, como pode ser um campo para você?

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 que o remote_image muda. 

Como posso buscar uma nova imagem quando alguém modificou o remote_image no alias? E, em segundo lugar, há uma maneira melhor de armazenar em cache uma imagem remota?

237
Paul Tarjan

Embora seja um pouco tarde, deixe-me jogar fora essa solução para os outros que se deparam com este post. Essencialmente, você deseja substituir o método __init__ de models.Model para que você mantenha uma cópia do valor original. Isso faz com que você não precise fazer outra pesquisa de banco de dados (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
370
Josh

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

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

163
iperelivskiy

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 quando se trabalha 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 = []
130
zgoda

A melhor maneira é com um sinal pre_save. Pode não ter sido uma opção em 2009, quando essa pergunta foi feita e respondida, mas qualquer um que veja isso hoje deve fazer isso da seguinte 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
129
Chris Pratt

Desde o lançamento do Django 1.8, você pode usar from_db classmethod para armazenar em cache o valor antigo de remote_image. Então, em save method, você pode comparar o valor antigo e 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!
49
Serge

Note que o rastreamento de mudança de campo está disponível no Django-model-utils.

https://Django-model-utils.readthedocs.org/en/latest/index.html

15
Lee Hinde

Se você estiver usando um formulário, você pode usar o Form's changed_data ( 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
13
laffuste

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

5
Fred Campos

A partir do Django 1.8, existe o método from_db, como Serge menciona. Na verdade, os documentos do Django incluem este caso de uso específico como exemplo:

https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading

Abaixo está um exemplo mostrando como registrar os valores iniciais dos campos que são carregados do banco de dados

5
Amichai Schreiber

Você pode usar Django-model-changes para fazer isso sem uma consulta adicional ao 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
3
Robert Kajic

A solução ideal é provavelmente aquela que não inclui uma operação adicional de leitura do banco de dados antes de salvar a instância do modelo, nem qualquer outra biblioteca do Django. É por isso que as soluções de laffuste são preferíveis. No contexto de um site admin, pode-se simplesmente sobrescrever o método save_model e invocar o método has_changed do formulário, assim como na resposta do Sion acima. Você chega a algo assim, com base no exemplo de configuração do Sion, mas usando "changed_data" 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)
  • Sobrescreva save_model:

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

  • Método changed_data incorporado para um campo:

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

3
user3061675

Isso funciona para mim no Django 1.8

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

Eu tive essa situação antes da minha solução foi para substituir o método pre_save() da classe de campo de destino que será chamado apenas se o campo foi 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ê quiser fazer qualquer operação (post_save) como usar o objeto criado em algum trabalho (se determinado campo tiver sido alterado)

2
MYaser

Outra resposta tardia, mas se você está apenas tentando ver se um novo arquivo foi enviado para um campo de arquivo, tente o seguinte: (adaptado do comentário de Christopher Adams no link http://zmsmith.com/2010/05/Django-check-if-a-field-alterou/ no comentário do 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
2
Aaron McMillin

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

Simplesmente limpe o campo remote_image depois de salvar com sucesso a cópia local. Em seguida, no seu método salvar, você sempre poderá atualizar a imagem sempre que remote_image não estiver vazio.

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

2
SmileyChris

melhorando a resposta do @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.name com strings (por exemplo, getattr(person, "name")

2
Hassek

Eu estendi o mixin de @livskiy da seguinte forma:

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

1
MYaser

Uma modificação para a 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 do futuro, mas, mais importante, também inclui chaves estrangeiras e campos onde editável = False.

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

Aqui está outra maneira de fazer isso.

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 a documentação: validando objetos

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

0
Gonzalo

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á chegar antes de verificar uma alteração

0
Jiaaro

Se você não encontrar interesse em substituir o método save, 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()
0
theTypan

O mixin de @ivanlivski é ótimo. 

Eu estendi para

  • Certifique-se de que trabalha com os campos Decimal.
  • 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 pessoas novas em Python ou Django, darei um exemplo mais completo. Esse uso em particular é 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()
0
sknutsonsf

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 isso 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!"
0
Sion