Intro
According to Wikipedia,
Cloudinary is a company that provides cloud-based image and video management services.
Cloudinary is a SaaS technology company headquartered in Santa Clara, California, with offices in Israel, England, Poland, and Singapore. The company provides cloud-based image and video management services. It enables users to upload, store, manage, manipulate, and deliver images and video for websites and apps.
Cloudinary works very well with Django and the setup is very easy. I would recommend it without any reservations for Django projects. To configure your project to use it, just follow the instructions on Adityamali's medium post or Shuaib Oseni's section.io post. Of course, there are many others. I also encourage you to view Cloudinary's documentation on the topic.
The Problem
When I was testing a Django App that used forms with some image uploads, I noticed that anytime I submit the form with errors, the image that I selected would be uploaded even though the form was not saved in the database. Eventually, when the form is now submitted without errors, the image is now uploaded again. Consequently, we would now have the same image uploaded at least twice!
I looked for a solution to this problem online but I was not able to find one even on StackOverflow or the Cloudinary's documentation. Given that the deadline for the project was approaching, I had to think hard and fast about what to do. This led me to the source code of the Cloudinary python package (here's a short LinkedIn post I made about looking through source code.
Replicating the Problem
To replicate the problem, set up your Django project with Cloudinary installed. I would now create a simple form with an image upload. Here I am creating a Custom User form to extend the default Django User and I have included a picture field there.
views.py
from .forms import CustomUserCreationForm
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import generic
class SignUpView(generic.CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy('login')
template_name = 'signup.html'
def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
return redirect('login')
def get_form_kwargs(self):
""" Passes the request object to the form class."""
kwargs = super(SignUpView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from cloudinary.models import CloudinaryField
class CustomUser(AbstractUser):
picture = CloudinaryField('Picture', null=True, blank=True)
forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.db import transaction
from .models import CustomUser
from cloudinary.forms import CloudinaryFileField
class CustomUserCreationForm (UserCreationForm):
username = forms.CharField(label='Username', min_length=5, max_length=254,
help_text='Enter anything except "admin", "user", or "superuser" ')
picture = CloudinaryFileField(options = {
'crop': 'limit',
'zoom': 0.75,
'width': 200,
'height': 200,
'folder': 'picture',
}, required=False, help_text="Upload a profile picture")
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = UserCreationForm.Meta.fields + ('picture',)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
def clean_username(self):
# I just added a simple validation so we can test the problem
blacklisted_usernames = ['admin', 'user', 'superuser']
username = self.cleaned_data.get('username')
if username in blacklisted_usernames:
raise forms.ValidationError('You cannot use this username')
return username
Let us now submit the form ensuring that the validation fails. You can see the error that is returned back
Now let me go to my cloudinary console where we can see that the image has been uploaded even though the validation failed.
The Solution: Source code to the rescue
Let us now take a look at the source code of the CloudinaryFileField
here.
CloudinaryFileField source code
class CloudinaryFileField(forms.FileField):
my_default_error_messages = {
"required": _("No file selected!")
}
default_error_messages = forms.FileField.default_error_messages.copy()
default_error_messages.update(my_default_error_messages)
def __init__(self, options=None, autosave=True, *args, **kwargs):
self.autosave = autosave
self.options = options or {}
super(CloudinaryFileField, self).__init__(*args, **kwargs)
def to_python(self, value):
"""Upload and convert to CloudinaryResource"""
value = super(CloudinaryFileField, self).to_python(value)
if not value:
return None
if self.autosave:
return cloudinary.uploader.upload_image(value, **self.options)
else:
return value
Let us pay attention to the __init__
and to_python
methods. We notice that there is a boolean attribute named autosave
which has a default value of True
. If we set this to False
, we can manually upload the file in the save
method of the form after all the validation has passed.
forms.py with solution
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.db import transaction
from .models import CustomUser
from cloudinary.forms import CloudinaryFileField
from cloudinary.uploader import upload_resource # new
class CustomUserCreationForm (UserCreationForm):
username = forms.CharField(label='Username', min_length=5, max_length=254,
help_text='Enter anything except "admin", "user", or "superuser"')
picture = CloudinaryFileField(options = {
'crop': 'limit',
'zoom': 0.75,
'width': 200,
'height': 200,
'folder': 'picture',
}, autosave=False, required=False, help_text="Upload a profile picture") # new
class Meta(UserCreationForm.Meta):
model = CustomUser
fields = UserCreationForm.Meta.fields + ('picture',)
def clean_username(self):
# I just added a simple validation so we can test the problem
blacklisted_usernames = ['admin', 'user', 'superuser']
username = self.cleaned_data.get('username')
if username in blacklisted_usernames:
raise forms.ValidationError('You cannot use this username')
return username
# new
@transaction.atomic
def save(self, commit=True):
# At this point, all validation has been passed
user = super(CustomUserCreationForm, self).save(commit=False)
user_picture = self.request.FILES.get('picture', None)
if user_picture:
user.picture = upload_resource(user_picture, **self.fields['picture'].options)
else:
user.picture = None
if commit:
user.save()
Some important things to note about the if
block in the overridden save
method above.
- I used upload_resource to upload the image (not the form field) because it returns the exact image to help Django store it appropriately.
- If you instead use
user.picture = self.fields['picture'].to_python(self.request.FILES.get('picture', None))
to upload the picture, what would be stored in the database would be a URL string of the image, and that is not the same way Django represents it. - Using
upload_image
function would return a Hash containing data about the uploaded picture. See what Cloudinary's documentation says here. - Simply writing
self.fields['picture'].autosave = True
in thesave
method would for some reason, just upload the image into the root directory of your Cloudinary Media Library without regard for the folder you set in the field options
Conclusion
In this post, I showed how to override the default action of automatically uploading files by Cloudinary in Django projects using the pycloudinary package. I hope you enjoyed reading this.
Is there a better or easier way to do this? let me know in the comments.
Find below, a link to the GitHub repo.
Daveson217 / django-cloudinary-autosave
A Django project that demonstrates the use of the CloudinaryFileField autosave option
Django-cloudinary-autosave
A repo for this dev.to post that I made explaining how to use the
autosave
attribute in pycloudinary'sCloudinaryFileField
.
Read the post here
Requirements
Tools and packages required to successfully install this project.
Installation
A step by step list of commands to install this project locally.
- Download the project ZIP and extract it into a new folder. Alternatively, you could clone the repo using
$ git clone project-url
.
$ pip install pipenv
$ pipenv install
$ python manage.py migrate
$ python manage.py createsuperuser
Follow the prompts
$ python manage.py runserver
Tech Stack
You can find me here at:
Image from Freepik
Top comments (0)