despite of many mordern website employ an OSS for serving image, I still want to build a backend to manage small thumbnails locally.
however, django image field is a bit tricky.
there are three views I may change image reference:
models.py
views.py
forms.py
I used to do it simply by:
forms.py
request.user.profile.image = self.files['image']
and I always have a default
models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = ProcessedImageField(
default='profile/default.jpg',
upload_to='profile',
processors=[ResizeToFill(*THUMBNAIL_SIZE)],
format='JPEG',
options={'quality': THUMBNAIL_QUALITY},
)
After a lot of testing, I found that it always result in an issue, can be among:
default image file get deleted.
if the image has been set before, it holds an value that is not the default, when I reset it, the old referenced file is not deleted and will occupy disk storage.
to do it perfectly, I decided to write a global function for import, whenever I want to set an image, call it
from django.conf import settings
def setImage(instance, attr, file):
""" instance will be saved """
if file:
ifield = getattr(instance, attr)
# old reference, can be default
iurl = ifield.url
# default
durl = settings.MEDIA_URL + instance._meta.get_field(attr).get_default()
if iurl != durl:
# old reference is not default
# delete old reference and free up space
ifield.delete(save=True)
# set new file
setattr(ifield, attr, file)
instance.save()
pretty straight-forward. however, in testing, I found the image will never be set. Following are the possibale reasons I have eliminated:
multipart
enctype
attributeprocessData
, contentType
is correctly setsave
in model class is not overridenif it's all ok, where went wrong? I logged out all the values.
setImage(self.user.profile, 'image', self.files['image'])
# self.files['image'] has valid value and is passed
# to setImage, which I think, is not garbage collected
def setImage(instance, attr, file):
""" instance will be saved """
print('======')
print(file)
if file:
ifield = getattr(instance, attr)
iurl = ifield.url
durl = settings.MEDIA_URL + instance._meta.get_field(attr).get_default()
print(iurl)
print(durl)
if iurl != durl:
ifield.delete(save=True)
print(f'--- del {iurl}')
setattr(ifield, attr, file)
print('----res')
print(getattr(ifield, attr))
print(ifield.image)
print('--- ins')
print(instance)
instance.save()
print('--- after save')
print(instance.image.url)
print(getattr(instance, attr))
the field has a default value, and I upload the screen shot in testing.
======
Screen Shot 2022-11-03 at 10.59.41 pm.png
/media/profile/default.jpg
/media/profile/default.jpg
----res
Screen Shot 2022-11-03 at 10.59.41 pm.png
Screen Shot 2022-11-03 at 10.59.41 pm.png
--- ins
tracey
--- after save
/media/profile/default.jpg
profile/default.jpg
why the image is not setting, anybody have any ideas?
after tons of testing, only a line went wrong and just simple a wrong variable.
def setImage(instance, attr, file):
""" instance will be saved """
if file:
ifield = getattr(instance, attr)
iurl = ifield.url
durl = settings.MEDIA_URL + instance._meta.get_field(attr).get_default()
if iurl != durl:
ifield.delete(save=True)
setattr(instance, attr, file)
instance.save()
for form update validation, here's a method I've worked out and testified working:
def clean_image(form, attr:str, field:str):
"""
form.instance.attr -> ImageFile
form.cleaned_data[field] -> ImageFile
"""
upload_img = form.cleaned_data[field]
if form.instance:
# condition: update
# create does not matter
ifield = getattr(form.instance, attr)
iurl = ifield.url
if isinstance(upload_img, InMemoryUploadedFile):
# upload new file
# else, the file is not changed
durl = settings.MEDIA_URL + form.instance._meta.get_field(attr).get_default()
if iurl != durl:
ifield.delete(save=True)
return upload_img
e.g. you can call it simply like:
class Project(models.Model):
image = ProcessedImageField(
default='projects/default.jpg',
upload_to='projects',
processors=[ResizeToFill(*THUMBNAIL_SIZE)],
options={'quality': THUMBNAIL_QUALITY},
format='JPEG',
)
class ProjectCreateForm(forms.ModelForm):
class Meta:
model = Project
fields = ['name', 'image']
def clean_image(self):
return clean_image(self, 'image', 'image')
Honestly, I did not test your function to say what is wrong with it. Instead I implemented it in my own way taking your model as a base. If you really want a set_image(instance, attr, file)
function, you can adapt it from this answer create_profile_ajax
at views.py.
settings.py
DEFAULT_IMAGE_URL = 'profile/default.jpg'
models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = ProcessedImageField(
default=settings.DEFAULT_IMAGE_URL,
upload_to='avatars',
processors=[ResizeToFill(100, 50)],
format='JPEG',
options={'quality': 60},
blank=True,
null=True
)
@staticmethod
def default_image_absolute_url():
return settings.MEDIA_URL + settings.DEFAULT_IMAGE_URL
@staticmethod
def default_image_url():
return settings.DEFAULT_IMAGE_URL
forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['image']
views.py
@login_required
def create_profile(request):
form = ProfileForm()
return render(request, 'profile/create.html', {'form': form})
def create_profile_ajax(request):
image = request.FILES.get('image')
profile, created = Profile.objects.get_or_create(user=request.user)
if image:
if profile.image.url == Profile.default_image_absolute_url():
profile.image = image
else:
profile.image.delete()
profile.image = image
else:
profile.image = Profile.default_image_url()
profile.save()
profile.refresh_from_db()
return JsonResponse({'new_image_url': profile.image.url})
profile/create.html (csrf with ajax)
{% extends 'base.html' %}
{% block content %}
{{form.as_p}}
<input type="submit" value="Create" onclick="sendProfile()">
<img src="{{request.user.profile.image.url}}"
id="thumb"
width="500"
height="600"
{% if not request.user.profile.image %} hidden {% endif %}
style="object-fit: contain;">
<script>
function getCookie(name) {
...
}
function sendProfile() {
const csrftoken = getCookie('csrftoken');
var input = document.getElementById('id_image');
var data = new FormData()
data.append('image', input.files[0])
fetch('/your/ajax/url/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken
},
body: data
})
.then((response) => response.json())
.then((data) => {
var thumb = document.getElementById('thumb');
thumb.src = data.new_image_url;
thumb.hidden = false;
input.value = '';
});
}
</script>
{% endblock %}
quotting FormData documentation:
When using FormData to submit POST requests using XMLHttpRequest or the Fetch_API with the multipart/form-data Content-Type (e.g. when uploading Files and Blobs to the server), do not explicitly set the Content-Type header on the request.