Django model not being saved after calling save()? - django

I'm overriding a ModelForm's save method. I'm parsing an Excel file and, if there are some values, I update a quantity in the form's instance related objects. It works the first time, when the instance has no related objects. But the second time, when I'm updating the values, nothing happens. I'm not sure if it has something to do with the commit parameter.
EDIT - Relevant code:
def save(self, commit=True):
"""
Overrides the save method in order to add the product inventory items listed in
the uploaded Excel file, if one was uploaded.
"""
inventory = super(AddOrChangeProductsInventoryForm, self).save(commit)
self._update_inventory_items_quantities()
if not commit:
inventory.save()
self.save_m2m()
return inventory
def _update_inventory_items_quantities(self):
inventory = self.instance
if len(self.excel_data_dict) == 0:
return inventory
non_existing_products = []
for i, product_sku in enumerate(self.excel_data_dict['SKU']):
quantity = self.excel_data_dict['Cantidad'][i]
new_item = inventory.productinventoryitem_set.filter(product__sku=product_sku).first()
if new_item is None:
product = Product.objects.filter(sku=product_sku).first()
if product is None:
# TODO: Raise warning in view
non_existing_products.append(product_sku)
continue
new_item = ProductInventoryItem()
new_item.product = product
new_item.inventory = inventory
new_item.quantity += quantity
# TODO: Check why after first update it's not being updated

If your model has any many-to-many relations, then you need to be sure and use self.save_m2m() in combination with the save method. Here's a quick example:
# save method of your forms.ModelForm class.
def save(self):
obj = super(MyModelForm, self).save(commit=False)
obj.save()
self.save_m2m()
return obj

You are saving form when commit is False.
Replace (line 10):
if not commit:
with:
if commit:
UPDATE:
new_item is new copy of inventory.productinventoryitem_set.filter(product__sku=product_sku).first()
or
ProductInventoryItem()
It is local variable and it is not updating inventory. All these changes will destroy after executing the function. You need to store changes in inventory.
When you are overriding _save_m2m in:
for obj in self.instance.productinventoryitem_set.all():
obj.quantity += 2
obj.save()
Then you are saving changes before destroying.
You can do something like this:
def save(self, commit=True):
inventory = super(AddOrChangeProductsInventoryForm, self).save(commit)
self._update_inventory_items_quantities(inventory, commit)
return inventory
def _update_inventory_items_quantities(self, inventory, commit):
if len(self.excel_data_dict) == 0:
return inventory
non_existing_products = []
for i, product_sku in enumerate(self.excel_data_dict['SKU']):
quantity = self.excel_data_dict['Cantidad'][i]
new_item = inventory.productinventoryitem_set.filter(product__sku=product_sku).first()
if new_item is None:
product = Product.objects.filter(sku=product_sku).first()
if product is None:
# TODO: Raise warning in view
non_existing_products.append(product_sku)
continue
new_item = ProductInventoryItem()
new_item.product = product
new_item.inventory = inventory
new_item.quantity += quantity
if commit:
new_item.save()

Related

Django m2m field doesn't assign to parent object in view

THE CONTEXT
I am trying to implement a tagging system for my project. The various plug-in solutions (taggit, tagulous) are each unsuitable in some way.
I would like to allow users to select from existing tags or create new ones in a Select2 tagging field. Existing tags can be added or removed without problem. My difficulty is in the dynamic generation and assignment of new tags.
MY APPROACH
Select2 helpfully renders the manually-entered tags differently in the DOM from those picked from the database via autocomplete. So upon clicking submit, I have javascript collect the new tags and string them together in the value of a hidden input, then delete them from the Select2 field to avoid any validation errors (the form otherwise POSTs the tag names as the ids, which throws a db error).
In the view, I iterate over the desired new tags. For each entry I create the new tag, then add it to the parent object's related set.
THE PROBLEM
While this successfully creates each tag (verified via Admin) it doesn't add it to the related set.
No errors are generated on the (clearly not succeeding) related set add.
The newly-generated tags are correctly instantiated and can be Select2-chosen and sucessfully assigned on a subsequent UpdateView, so I'm certain the problem lies in the view-assignment of the tags to the parent.
The same code executed via the Django shell work flawlessly, so I don't believe its a simple syntax error.
Thus the locus of the problem seems to be in the POST view code adding newly-generated tags to the parent, but I cannot see where the code goes astray.
Thanks for any insights or advice!
models.py:
class Recipe_tag(models.Model):
id = models.UUIDField(primary_key=True,default=uuid.uuid4,null=False)
tag = models.CharField('Tag name',max_length=32,null=False,unique=True)
def __str__(self):
return str(self.tag)
class Recipe_base(models.Model):
id = models.UUIDField(primary_key=True,default=uuid.uuid4,null=False)
name = models.CharField('Recipe name',max_length=128,null=False)
tags = models.ManyToManyField(Recipe_tag,related_name='recipes',null=True,blank=True)
def __str__(self):
return str(self.name)
The post portion of the view:
def post(self, request, *args, **kwargs):
self.object = None
r = Recipe_base.objects.get(id=self.kwargs.get('pk'))
form = RecipeUpdateTagsForm(request.POST,instance=r)
form_valid = form.is_valid()
if form_valid:
if form.has_changed:
f = form.save(commit=False)
clean = form.cleaned_data
f.addedTags = clean['addedTags']
if f.addedTags == 'placeholder':
pass
else:
new_tags = f.addedTags.split(',')
for new_tag in new_tags:
a = Recipe_tag(tag=new_tag)
a.save()
r.tags.add(a)
f.save()
form.save_m2m()
else:
pass
return self.form_valid(form)
else:
return self.form_invalid(form)
Doing further digging, I found a post on another site in which the OP was experiencing the same issues. The trick is to remove the m2m assignment from the "create" or "update" process entirely, because the final save() that occurs in form_valid will discard any changes to the parent's related set.
In my case, the solution was to punt the iteration/assignment of the new tags to form_valid, directly after the final save() occurs there:
def form_valid(self, form, **kwargs):
self.object = form.save()
addedTags = kwargs['addedTags']
r = Recipe_base.objects.get(id=self.kwargs.get('pk'))
if addedTags == 'placeholder':
pass
else:
new_tags = addedTags.split(',')
for new_tag in new_tags:
a = Recipe_tag(tag=new_tag)
a.save()
# print("debug // new tag: %s, %s" % (a.tag, a.id))
r.tags.add(a)
# print("debug // added %s to %s" % (a.tag,r.id))
return HttpResponseRedirect(self.get_success_url())
def get(self, request, *args, **kwargs):
self.object = Recipe_base.objects.get(id=self.kwargs.get('pk'))
recipe_name = self.object.name
recipe_id = self.kwargs.get('pk')
form = RecipeUpdateForm(instance=Recipe_base.objects.get(id=self.kwargs.get('pk')))
return self.render_to_response(self.get_context_data(form=form,recipe_name=recipe_name,recipe_id=recipe_id))
def post(self, request, *args, **kwargs):
self.object = None
r = Recipe_base.objects.get(id=self.kwargs.get('pk'))
form = RecipeUpdateForm(request.POST,instance=r)
form_valid = form.is_valid()
#print("debug // form_valid PASSED")
if form_valid:
if form.has_changed:
#print("debug // Form changed...")
f = form.save(commit=False)
#print("debug // Passes save C=F")
clean = form.cleaned_data
#print('debug // pre-existing tags: ',clean['tags'])
f.addedTags = clean['addedTags']
addedTags = clean['addedTags']
#print('debug // manual tags: ',clean['addedTags'])
f.save()
form.save_m2m()
#print("debug // Form saved")
else:
#print("debug // Form unchanged, skipping...")
pass
#print("debug // Reached successful return")
return self.form_valid(form, addedTags=addedTags,pk=r.id)
else:
#print("debug // Form fails validation")
#print("debug // Reached unsuccessful return")
return self.form_invalid(form)

Validate a dynamic select field in Django

I'm using Django 1.4 with Python 2.7 on Ubuntu 12.10.
I have a form where I need to populate a few drop-downs dynamically (using jQuery) but need 2 of them to be required and the 3rd to be optional.
I'm using Tastypie to help with the API to get the options. Basically the first drop-down is populated with industry level codes for schools. Once a code is selected a category drop-down is populated for all categories for that code. Once the category is chosen a subcategory drop-down is populated for all subcategories for that combination of code and category.
I'm able to require the code drop-down (it's not dynamically populated). However, I'm having a tough time getting the category drop-down to be required. There are basically 2 routes I can take - front-end validation or back-end validation. I'm trying to go with back-end validation so I can easily create further validation if needed.
Here is the form:
class SchoolProductForm(forms.ModelForm):
cip_category = forms.ChoiceField(required=True,
choices=(('', '----------'),))
def __init__(self, *args, **kwargs):
super(SchoolProductForm, self).__init__(*args, **kwargs)
self.fields['short_description'].widget = TA_WIDGET
self.fields['salary_info'].widget = TA_WIDGET
self.fields['job_opportunities'].widget = TA_WIDGET
self.fields['related_careers'].widget = TA_WIDGET
self.fields['meta_keywords'].widget = TI_WIDGET
self.fields['meta_description'].widget = TI_WIDGET
self.fields['cip'].queryset = models.CIP.objects.filter(
parent_id__isnull=True)
class Meta:
model = models.SchoolProduct
exclude = ('campus',)
I've tried to override the clean method. I've tried to create a field specific clean method. Neither seem to work.
Variations of the following:
def clean(self):
super(SchoolProductForm, self).clean()
if cip_category in self._errors:
del self._errors['cip_category']
if self.cleaned_data['cip_category'] == '----------':
self._errors['cip_category'] = 'This field is required.'
return self.cleaned_data
This gives an error that there is no cip_category in cleaned_data, which makes sense because it didn't validate.
I've tried variations with the field specific clean:
def clean_cip_category(self):
data = self.cleaned_data['cip_category']
self.fields['cip_category'].choices = data
return data
But get a validation error on the page stating my choice is not one of the available choices.
I've tried to create a dynamic field type (several variations):
class DynamicChoiceField(forms.ChoiceField):
def valid_value(self, value):
return True
class SchoolProductForm(forms.ModelForm):
cip_category = DynamicChoiceField(required=True,
choices=(('', '----------'),))
But it accepts ---------- as a valid option (which I don't want) and causes an error since the ORM tries to match a value of ---------- in the database (which it won't find).
Any ideas?
I was able to solve this with a little overriding of a method in ChoiceField.
I added the field to the form and handled the pre-population with the self.initial:
class SchoolProductForm(forms.ModelForm):
cip_category = common_forms.DynamicChoiceField(
required=True, choices=(('', '----------'),))
def __init__(self, *args, **kwargs):
super(SchoolProductForm, self).__init__(*args, **kwargs)
self.fields['short_description'].widget = TA_WIDGET
self.fields['salary_info'].widget = TA_WIDGET
self.fields['job_opportunities'].widget = TA_WIDGET
self.fields['related_careers'].widget = TA_WIDGET
self.fields['meta_keywords'].widget = TI_WIDGET
self.fields['meta_description'].widget = TI_WIDGET
self.fields['cip'].queryset = models.CIP.objects.filter(
parent_id__isnull=True)
# Get the top parent and pre-populate
if 'cip' in self.initial:
self.initial['cip'] = models.CIP.objects.get(
pk=self.initial['cip']).top_parent()
class Meta:
model = models.SchoolProduct
exclude = ('campus',)
Where DynamicChoiceField is:
class DynamicChoiceField(forms.ChoiceField):
def valid_value(self, value):
return True
Then, in the view I added handling in the form_valid override:
def form_valid(self, form):
self.object = form.save(commit=False)
# Handle the CIP code
self.object.cip_id = self.request.POST.get('cip_subcategory')
if self.object.cip_id == '':
self.object.cip_id = self.request.POST.get('cip_category')
self.object.save()

assign unique id avoiding race conditions

I have a form called Vehicles and i'm trying to assign a unique id to each one, each time a user completes one.
class Vehicles(models.Model):
id = models.DecimalField(primary_key=True, unique=True)
Trying to avoid race conditions(when two forms are being submitted in the same time) after the initial value that I assign to the id field according to the last vehicle-db-record, before saving the form I query again the db for the id of the last record. More or less I do it this way:
def vehicle(request):
vehicles= Vehicles.objects.all().order_by("-id")[0]
id = vehicles.id+1
if request.method == 'POST':
form = VehiclesForm(data=request.POST)
if form.is_valid():
vehicles= Vehicles.objects.all().order_by("-id")[0]
id = vehicles.id+1
temp = form.save(new_veh_id=id)
return render_to_response('success.html', locals(), context_instance= RequestContext(request))
else:
form = VehiclesForm(initial={'id': id})
return render_to_response('insertVehicle.html', locals(), context_instance= RequestContext(request))
and in forms I override the save method:
def save(self, *args, **kwargs):
commit = kwargs.pop('commit', True)
new_veh_id = kwargs.pop('new_veh_id', None)
instance = super(VehiclesForm, self).save(*args, commit = False, **kwargs)
if id is not None: instance.id = new_veh_id
if commit:
instance.save()
return instance
but is_valid returns false with form error:vehicles with this id already exists.
I use exactly this practice with another model and form (identical fields to these) and it works like a charm, forms pass validation despite the same id and it changes it in the last submitted form. Can anyone help me on this or suggest a better solution to achieve this functionality? What I do is maybe somehow 'custom'.
EDIT: I also tried this, but is_valid fails again
def clean(self):
cleaned_data = super(VehiclesForm, self).clean()
id = unicode(self.cleaned_data.get('id'))
vehicles= Vehicles.objects.all().order_by("-id")[0]
id = vehicles.id+1
cleaned_data[id] = id
return cleaned_data
I think you need select_for_update which locks the rows until the end
of transaction. Mind you that although it is designated to work with many
rows, you can still lock just one row, by making sure your filter query
will return only one object.

query in django model

I am using django and as I am pretty new I have some questions.
I have one model called Signatures and a ModelForm called SignatureForm in my models.py file:
class Signature(models.Model):
sig = models.ForeignKey(Device)
STATE = models.CharField(max_length=3, choices=STATE_CHOICES)
interval = models.DecimalField(max_digits=3, decimal_places=2)
verticies = models.CharField(max_length=150)
class SignatureForm(ModelForm):
class Meta:
model = Signature
widgets = {
'verticies': HiddenInput,
}
To use it, I wrote the following function in views.py:
def SigEditor(request):
# If the form has been sent:
if request.method == 'POST':
form = SignatureForm(request.POST)
# If it is valid
if form.is_valid():
# Create a new Signature object.
form.save()
return render_to_response('eQL/sig/form_sent.html')
else:
return render_to_response('eQL/sig/try_again.html')
else:
form = SignatureForm()
return render_to_response('eQL/sig/showImage.html', {'form' : form})
However, I don't want to save all the new signatures. I mean, if the user introduces a new signature of the device A and state B, I would like to check if I have some signature like that in my database, delete it and then save the new one so that I have only one signature saved for each device and state.
I have tried something like this before saving it but of course is not working:
q = Signature.objects.filter(sig = s, STATE = st)
if q.count != 0:
q.delete()
form.save()
can anyone help?? thanks!!
If you really do want to delete, why not?
Signature.objects.filter(sig=s, STATE=st).delete()
If you only ever want one combination of those items, you could use get_or_create, and pass in the instance to your ModelForm.
instance, created = Signature.objects.get_or_create(sig=s, STATE=st)
form = SignatureForm(request.POST, instance=signature)
# edit instance.
Or put it in your form save logic:
class SignatureForm(ModelForm):
def save(self, *args, **kwargs):
data = self.cleaned_data
instance, created = Signature.objects.get_or_create(sig=data['sig'], STATE=data['state'])
self.instance = instance
super(SignatureForm, self).save(*args, **kwargs)

When saving, how can you check if a field has changed?

In my model I have :
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
Which works great for the first time the remote_image changes.
How can I fetch a new image when someone has modified the remote_image on the alias? And secondly, is there a better way to cache a remote image?
Essentially, you want to override the __init__ method of models.Model so that you keep a copy of the original value. This makes it so that you don't have to do another DB lookup (which is always a good thing).
class Person(models.Model):
name = models.CharField()
__original_name = None
def __init__(self, *args, **kwargs):
super().__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().save(force_insert, force_update, *args, **kwargs)
self.__original_name = self.name
I use following 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])
Usage:
>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>
Note
Please note that this solution works well in context of current request only. Thus it's suitable primarily for simple cases. In concurrent environment where multiple requests can manipulate the same model instance at the same time, you definitely need a different approach.
Best way is with a pre_save signal. May not have been an option back in '09 when this question was asked and answered, but anyone seeing this today should do it this way:
#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
And now for direct answer: one way to check if the value for the field has changed is to fetch original data from database before saving instance. Consider this example:
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)
The same thing applies when working with a form. You can detect it at the clean or save method of a 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 = []
Since Django 1.8 released, you can use from_db classmethod to cache old value of remote_image. Then in save method you can compare old and new value of field to check if the value has changed.
#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!
Note that field change tracking is available in django-model-utils.
https://django-model-utils.readthedocs.org/en/latest/index.html
If you are using a form, you can use 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
I am a bit late to the party but I found this solution also:
Django Dirty Fields
Another late answer, but if you're just trying to see if a new file has been uploaded to a file field, try this: (adapted from Christopher Adams's comment on the link http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/ in zach's comment here)
Updated link: 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
There is an attribute __dict__ which have all the fields as the keys and value as the field values. So we can just compare two of them
Just change the save function of model to the function below
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)
Example Usage:
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)
yields output with only those fields that have been changed
{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
As of Django 1.8, there's the from_db method, as Serge mentions. In fact, the Django docs include this specific use case as an example:
https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading
Below is an example showing how to record the initial values of fields that are loaded from the database
This works for me in Django 1.8
def clean(self):
if self.cleaned_data['name'] != self.initial['name']:
# Do something
Very late to the game, but this is a version of Chris Pratt's answer that protects against race conditions while sacrificing performance, by using a transaction block and select_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
You can use django-model-changes to do this without an additional database lookup:
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
The optimal solution is probably one that does not include an additional database read operation prior to saving the model instance, nor any further django-library. This is why laffuste's solutions is preferable. In the context of an admin site, one can simply override the save_model-method, and invoke the form's has_changed method there, just as in Sion's answer above. You arrive at something like this, drawing on Sion's example setting but using changed_data to get every possible change:
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)
Override save_model:
https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model
Built-in changed_data-method for a Field:
https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data
While this doesn't actually answer your question, I'd go about this in a different way.
Simply clear the remote_image field after successfully saving the local copy. Then in your save method you can always update the image whenever remote_image isn't empty.
If you'd like to keep a reference to the url, you could use an non-editable boolean field to handle the caching flag rather than remote_image field itself.
I had this situation before my solution was to override the pre_save() method of the target field class it will be called only if the field has been changed
useful with FileField
example:
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
disadvantage:
not useful if you want to do any (post_save) operation like using the created object in some job (if certain field has changed)
I have extended the mixin of #livskiy as follows:
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
and the DictField is:
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)
it can be used by extending it in your models
a _dict field will be added when you sync/migrate and that field will store the state of your objects
improving #josh answer for all fields:
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)
just to clarify, the getattr works to get fields like person.name with strings (i.e. getattr(person, "name")
My take on #iperelivskiy's solution: on large scale, creating the _initial dict for every __init__ is expensive, and most of the time - unnecessary. I have changed the mixin slightly such that it records changes only when you explicitly tell it to do so (by calling instance.track_changes):
from typing import KeysView, Optional
from django.forms import model_to_dict
class TrackChangesMixin:
_snapshot: Optional[dict] = None
def track_changes(self):
self._snapshot = self.as_dict
#property
def diff(self) -> dict:
if self._snapshot is None:
raise ValueError("track_changes wasn't called, can't determine diff.")
d1 = self._snapshot
d2 = self.as_dict
diffs = [(k, (v, d2[k])) for k, v in d1.items() if str(v) != str(d2[k])]
return dict(diffs)
#property
def has_changed(self) -> bool:
return bool(self.diff)
#property
def changed_fields(self) -> KeysView:
return self.diff.keys()
#property
def as_dict(self) -> dict:
return model_to_dict(self, fields=[field.name for field in self._meta.fields])
I have found this package django-lifecycle.
It uses django signals to define #hook decorator, which is very robust and reliable. I used it and it is a bliss.
How about using David Cramer's solution:
http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/
I've had success using it like this:
#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!"
A modification to #ivanperelivskiy's answer:
#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
This uses django 1.10's public method get_fields instead. This makes the code more future proof, but more importantly also includes foreign keys and fields where editable=False.
For reference, here is the implementation of .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))
)
as an extension of SmileyChris' answer, you can add a datetime field to the model for last_updated, and set some sort of limit for the max age you'll let it get to before checking for a change
The mixin from #ivanlivski is great.
I've extended it to
Ensure it works with Decimal fields.
Expose properties to simplify usage
The updated code is available here:
https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py
To help people new to Python or Django, I'll give a more complete example.
This particular usage is to take a file from a data provider and ensure the records in the database reflect the file.
My model object:
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
The class that loads the file has these methods:
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()
Here is another way of doing it.
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)
As per documentation: validating objects
"The second step full_clean() performs is to call Model.clean(). This method should be overridden to perform custom validation on your model.
This method should be used to provide custom model validation, and to modify attributes on your model if desired. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field:"
If you do not find interest in overriding save method, you can do
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()
Sometimes I want to check for changes on the same specific fields on multiple models that share those fields, so I define a list of those fields and use a signal. In this case, geocoding addresses only if something has changed, or if the entry is new:
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=SomeUserProfileModel)
#receiver(pre_save, sender=SomePlaceModel)
#receiver(pre_save, sender=SomeOrganizationModel)
#receiver(pre_save, sender=SomeContactInfoModel)
def geocode_address(sender, instance, *args, **kwargs):
input_fields = ['address_line', 'address_line_2', 'city', 'state', 'postal_code', 'country']
try:
orig = sender.objects.get(id=instance.id)
if orig:
changes = 0
for field in input_fields:
if not (getattr(instance, field)) == (getattr(orig, field)):
changes += 1
if changes > 0:
# do something here because at least one field changed...
my_geocoder_function(instance)
except:
# do something here because there is no original, or pass.
my_geocoder_function(instance)
Writing it once and attaching with "#receiver" sure beats overriding multiple model save methods, but perhaps some others have better ideas.