Resizing and updating image in Django - django

I'm overriding my save method because I want to resize my image. My problem is obvious in this code, what happens after I save my data is that django starts an endless loop. Of course is because I'm checking if self.vertical is set or not (and is always set). Save method is always called.
I could solve this problem checking if my self.id is none, but the code will work only for new entries, if the user tries to update, nothing changes because the id is not null.
I also tried to pop an false argument to kwargs and after saving, set true, but also didnt work. Someone has an idea how can I solve this?
PS: get_thumbnail is a method of sorl thumbnail plugin.
def save(self, *args, **kwargs):
if self.vertical_poster:
super(Movie, self).save(*args, **kwargs)
resized_vertical = get_thumbnail(self.vertical_poster, "155x240", quality=99)
#save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)
Any idea or sample code will be appreciated! Thanks

You can check the image whether it is already in your desired dimensions. I think you can use https://pypi.python.org/pypi/dimensions to make it simple
def save(self, *args, **kwargs):
if self.vertical_poster:
import dimensions
# Set desired dimensions
width = 155
height = 240
image_info = dimensions.dimensions(self.vertical_poster)
if image_info[0] != width and image_info[1] != height:
dimension = '%dx%d' % (width, height)
resized_vertical = get_thumbnail(self.vertical_poster, dimension, quality=99)
#save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)
Hope it helps.

If you use ImageField you can do it like this:
def save(self, *args, **kwargs):
if self.vertical_poster.x != 155 or self.vertical_poster.y != 240:
resized_vertical = get_thumbnail(self.vertical_poster, "155x240", quality=99)
# save the new resized file
self.vertical_poster.save(resized_vertical.name, ContentFile(resized_vertical.read()), True)
super(Movie, self).save(*args, **kwargs)

My solution was not override my save method. I did something different:
I decided to use "django-resized". This plugin is compatible with "sorl-thumbnail" and resizes image origin to specified size on the model field, as below:
from django_resized import ResizedImageField
class Movie(models.Model):
....
vertical_poster = ResizedImageField(upload_to="movies", width=155, height=240)
I hope that can help other people.
Link: https://github.com/un1t/django-resized

You can create a mixin and use wherever you want likewise.
mixins.py
from io import BytesIO
from PIL import Image
class ShrinkImageMixin:
def shrink_image(self, field_name, resize_shape):
img: Image = Image.open(getattr(self, field_name))
img.thumbnail(self.get_shrinked_size(field_name, resize_shape), Image.ANTIALIAS)
image_file = BytesIO()
img.save(image_file, 'jpg')
getattr(self, field_name).file = image_file
def get_shrinked_size(self, field_name, resize_shape):
actual_img_width, actual_img_height = getattr(self, field_name).width, getattr(self, field_name).height
ratio = min(resize_shape[0] / actual_img_width, resize_shape[1] / actual_img_height)
return int(actual_img_width * ratio), int(actual_img_height * ratio)
models.py
from .mixins import ShrinkImageMixin
class Article(ShrinkImageMixin, models.Model):
...
background_image = models.ImageField()
...
def save(self, *args, **kwargs):
self.shrink_image('background_image', (750, 375))
super(Article, self).save()
This will shrink your image down to your desired size maintaining the aspect ratio. You just need to provide the image field name in the model and resize_shpe tuple.

Related

Django - Uploaded image gets saved twice: under original and validated name

I'm trying to solve a problem.
On image file upload, I want to rename the file and resize it. I came up with a method below.
I managed to get the image resized and saved under a valid name.
Unfortunately the file gets saved twice, also under the invalid name. And the invalid one is stored in my object. How can I fix this? Thanks in advance
class SparePartImages(models.Model):
sparepart = models.ForeignKey('SparePart', on_delete=models.CASCADE)
image = models.ImageField(upload_to='spare-part/', blank=True, null=True)
def save(self, *args, **kwargs):
super(SparePartImages, self).save(*args, **kwargs)
max_size = settings.IMAGE_MAX_SIZE
file = Image.open(self.image)
(width, height) = file.size
if (width/max_size < height/max_size):
factor = height/max_size
else:
factor = width/max_size
size = (int(width/factor), int(height/factor))
file = file.resize(size, Image.ANTIALIAS)
file.save(removeAccent(self.image.path))
You are calling save twice one with super and other with save to fix it do this:
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile
import sys
class SparePartImages(models.Model):
# rest of the code
def save(self, *args, **kwargs):
# rest of the code
# but remove file.save(removeAccent(self.image.path))
output = BytesIO()
file.save(output, format='JPEG', quality=100)
output.seek(0)
self.image = InMemoryUploadedFile(
output ,'ImageField', "%s.jpg" %self.image.name.split('.')[0], 'image/jpeg', sys.getsizeof(output), None
)
super(SparePartImages, self).save(*args, **kwargs)

How to create an BytesIO img and pass to template

AIM
I am attempting to:
Create a histogram,
Store it temporary memory,
Pass the image to the template.
I am having trouble with Step 3 above. I suspect that I am making a simple and fundamental error in relation to passing the context data to the template.
ERROR
HTML is rendering with a broken image tag.
CODE
Views.py
class SearchResultsView(DetailView):
...
def get(self, request, *args, **kwargs):
self.get_histogram(request)
return super(SearchResultsView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(SearchResultsView, self).get_context_data(**kwargs)
return context
def get_histogram(self, request):
""" Function to create and save histogram of Hashtag.locations """
# create the histogram
plt.show()
img_in_memory = BytesIO()
plt.savefig(img_in_memory, format="png")
image = base64.b64encode(img_in_memory.getvalue())
context = {'image':image}
return context
Results.html
<img src="data:image/png;base64,{{image}}" alt="Location Histogram" />
SOLUTION
In addition to issues with get and get_context_data as outlined by #ruddra below, another issue was that I had to decode the base64 string as a Unicode string. For more information, see here.
To do so, I included: image = image.decode('utf8')
So that, views.py looks like this:
def get_histogram(self, request):
# draw histogram
plt.show()
img_in_memory = BytesIO()
plt.savefig(img_in_memory, format="png") # save the image in memory using BytesIO
img_in_memory.seek(0) # rewind to beginning of file
image = base64.b64encode(img_in_memory.getvalue()) # load the bytes in the context as base64
image = image.decode('utf8')
return {'image':image}
You are calling the get_histogram in wrong way. You can do it like this:
class SearchResultsView(DetailsView):
...
def get_context_data(self, **kwargs):
context = super(SearchResultsView, self).get_context_data(**kwargs)
context.update(self.get_histogram(self.request))
return context
You don't need to call the get_histogram method in get, or override the get method.
Update
I have tried like this:
class SearchResultsView(DetailsView):
...
def get_histogram(self):
x = 2
y = 3
z = 2
t= 3
plt.plot(x, y)
plt.plot(z, t)
plt.show()
img_in_memory = io.BytesIO() # for Python 3
plt.savefig(img_in_memory, format="png")
image = base64.b64encode(img_in_memory.getvalue())
return {'image':image}
def get_context_data(self, *args, **kwargs):
context = super(SearchResultsView, self).get_context_data(*args, **kwargs)
context.update(self.get_histogram())
return context
Output looks like this:

Django how to check if user uploaded an image file when saving model with ImageField?

I am looking for a way to resize, compress, and optimize the uploaded image when saving an ImageField.
class Image(models.Model):
name = models.CharField(max_length=254, blank=True)
caption = models.TextField(max_length=1000, blank=True)
height = models.IntegerField()
width = models.IntegerField()
image = models.ImageField(upload_to='', height_field='height', width_field='width', storage=S3MediaStorage())
My first thought was to override the model's save() and implement this logic there, but I don't want the resize/compression/optimization to run again if the user doesn't update the image file (i.e. if he only updates name or caption on an existing object and saves it).
What is a proper way to check when a new image file is uploaded to the ImageField, but not when the user only changes another field in the Model, eg. the user updates caption but leaves everything else as-is?
How can the uploaded image file be accessed in code? I.e. what is the variable that contains the actual image file that can be passed to Pillow?
edit: This is unique from the suspected duplicate. I am not asking if the field has changed, because that would always cause false positives. I am asking if the user has uploaded an image file, which I will immediately change (resize/optimize/compress), so if the user immediately downloads his uploaded image he'll find that has a different binary with a randomly generated filename, and therefore comparing the filename or binary are not valid methods to determine if the user is uploading a different image.
Your model could use a different name.
Nevertheless, you can try manipulating the image through a post_save signal (https://docs.djangoproject.com/en/1.9/ref/signals/#post-save)
from PIL import Image
from django.db.models.signals import post_save
#receiver(post_save, sender=Image)
def crop_image(sender, instance, **kwargs):
img = instance.image
original = Image.open(img.src.path)
# ... your code here...
EDIT: Apologies. Jumped the gun a bit. One of your actual problems was to not manipulate the image if it's the same. You can do it on save() like this (UNTESTED):
def save(self, **kwargs):
try:
related_img = Image.objects.get(id=self.id)
if related_img.image != self.image:
crop_me(self.image)
except Image.DoesNotExist:
# object doesn't exist. Passing...
pass
return super(Image, self).save(**kwargs)
def crop_me(img):
original_img = Image.open(img.src.path)
# ... your code here...
EDIT 2: If the name changes you could save the original filename in an helper field
class Image(models.Model):
image = models.ImageField(upload_to='', height_field='height', width_field='width', storage=S3MediaStorage())
__original_image_filename = None
def __init__(self, *args, **kwargs):
super(Image, self).__init__(*args, **kwargs)
self.__original_image_filename = self.image.name
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.image.name != self.__original_image_filename:
# name changed - do something here
super(Image, self).save(force_insert, force_update, *args, **kwargs)
self.__original_image_filename = self.image.name
I am modifying another answer on the fly so there could be an error or two. Please check the original answer. There are other methods on that question that could help you.

Django save override ImageField handling

After the problems I had on this thread, there is still a big problem in my models.py when I'm using the Django Admin. Here is my code (I removed stuff non related to my problem) :
from django.core.files.uploadedfile import InMemoryUploadedFile
from PIL import Image as Img
import StringIO
class Mymodel(models.Model):
photo = models.ImageField(upload_to="photo/", blank=True, null=True)
def save(self, *args, **kwargs):
width = 500
height = 500
size = (width,height)
if self.photo:
image = Img.open(StringIO.StringIO(self.photo.read()))
(imw, imh) = image.size
if (imw>width) or (imh>height) :
image.thumbnail(size, Img.ANTIALIAS)
#If RGBA, convert transparency
if image.mode == "RGBA":
image.load()
background = Img.new("RGB", image.size, (255, 255, 255))
background.paste(image, mask=image.split()[3]) #3 is alpha channel
image=background
output = StringIO.StringIO()
image.save(output, format='JPEG', quality=60)
output.seek(0)
self.photo = InMemoryUploadedFile(output,'ImageField', "%s.jpg" %self.photo_principale.name.split('.')[0], 'image/jpeg', output.len, None)
try:
this = Mymodel.objects.get(id=self.id)
if this.photo != self.photo:
this.photo.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Mymodel, self).save(*args, **kwargs)
It works, the files are uploaded, resized and converted to JPEG successfully when needed. The problem, every time I edit it, even when NOT uploading a new image, it creates a new image (for example, I save my model a first time with image "hello.jpg", then I edit it, it'll create a new image called "hello_1.jpg" even if I didn't upload anything).
I thought the try/except block would work when only editing (so no new file upload), but apparently not.
Thanks in advance for the help :)
Final solution, working for me :
from django.core.files.uploadedfile import InMemoryUploadedFile
from PIL import Image as Img
import StringIO
from django.db.models.signals import post_delete
from django.dispatch import receiver
Class Mymodel(models.Model):
photo= models.ImageField(upload_to="photo/", blank=True, null=True)
def save(self, *args, **kwargs):
width = 500
height = 500
size = (width,height)
isSame = False
if self.photo:
try:
this = Mymodel.objects.get(id=self.id)
if this.photo==self.photo :
isSame= True
except: pass # when new photo then we do nothing, normal case
image = Img.open(StringIO.StringIO(self.photo.read()))
(imw, imh) = image.size
if (imw>width) or (imh>height) :
image.thumbnail(size, Img.ANTIALIAS)
#If RGBA, convert transparency
if image.mode == "RGBA":
image.load()
background = Img.new("RGB", image.size, (255, 255, 255))
background.paste(image, mask=image.split()[3]) # 3 is the alpha channel
image=background
output = StringIO.StringIO()
image.save(output, format='JPEG', quality=60)
output.seek(0)
self.photo = InMemoryUploadedFile(output,'ImageField', "%s.jpg" %self.photo.name.split('.')[0], 'image/jpeg', output.len, None)
try:
this = Mymodel.objects.get(id=self.id)
if this.photo==self.photo or isSame :
self.photo=this.photo
else :
this.photo.delete(save=False)
except: pass # when new photo then we do nothing, normal case
super(Mymodel, self).save(*args, **kwargs)
#receiver(post_delete, sender=Mymodel)
def photo_post_delete_handler(sender, **kwargs):
instance = kwargs['instance']
storage, path = instance.photo.storage, instance.photo.path
if (path!='.') and (path!='/') and (path!='photo/') and (path!='photo/.'):
storage.delete(path)
Hope it can help somebody ;)
This Builds on Ralph's answer worked for me for python 3 and django 2
first you must import io:
from io import BytesIO
To resize and add white background where necessary:
def resize_with_white_background(pil_image: Image.Image, desired_width, desired_height):
img_copy = pil_image.copy()
# get proportioned image ie (if image is 200X600 and trying to resize to 100X200
# thumbnail will NOT do this but resize to keep the ratio so it would be 67x200 to maintain the ratio (uses the larger)
# img_copy changed in place (does not create new image)
img_copy.thumbnail((desired_width, desired_height), Image.ANTIALIAS)
# create white background
background = Image.new('RGB', (desired_width, desired_height), (255,255,255))
pixels_to_move_left = int((background.width - img_copy.width) * 0.50) # centered horizontally
pixels_to_move_down = int((background.height - img_copy.height) * 0.50) # centered vertically
# paste image into white background box argument tells where to paste
background.paste(img_copy, box=(pixels_to_move_left, pixels_to_move_down))
return background # this will return the background with img_copy pasted in and will be resized to fit your desired size
To set the resized image to and ImageField create a method in your model:
def set_image(self, desired_width, desired_height):
try:
this = MyModel.objects.get(id=self.id)
except MyModel.DoesNotExist:
pass
else:
# will not resize or set to new image (this avoids setting image every single time you edit and save
if this.image == self.image and (self.image.width, self.image.height) == (desired_width, desired_height):
return
im = Image.open(BytesIO(self.image.read()))
resized_image = resize_with_white_background(
pil_image=im,
desired_width=desired_width,
desired_height=desired_height
)
# output (file like object)
output = BytesIO()
# save image into file-like object
resized_image.save(output, format='JPEG', quality=94)
# get size of file
a_size = output.tell()
# reset to beginning of file-like object
output.seek(0)
self.image.file = InMemoryUploadedFile(
output,
'ImageField',
f"{self.image.name.split('.')[0]}.jpg",
'image/jpeg',
a_size,
None
)
override the save() method of your Model and call the set_image() method before calling the Super().save(*args, **kwargs) method
def save(self, *args, **kwargs):
self.set_image(
desired_width=100, # can be whatever you want
desired_height=200
)
super().save(*args, **kwargs)
Try:
if self.photo.name != '':
or
if self.photo.size > 0:

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.