DEV Community

Cover image for How to use autosave=False in CloudinaryFileField when using Django forms
David Babalola
David Babalola

Posted on

How to use autosave=False in CloudinaryFileField when using Django forms

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let us now submit the form ensuring that the validation fails. You can see the error that is returned back

Form submitted with errors

Now let me go to my cloudinary console where we can see that the image has been uploaded even though the validation failed.

Image is uploaded to Cloudinary even though validation fails

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
Enter fullscreen mode Exit fullscreen mode

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()            
Enter fullscreen mode Exit fullscreen mode

Some important things to note about the if block in the overridden save method above.

  1. I used upload_resource to upload the image (not the form field) because it returns the exact image to help Django store it appropriately.
  2. 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.
  3. Using upload_image function would return a Hash containing data about the uploaded picture. See what Cloudinary's documentation says here.
  4. Simply writing self.fields['picture'].autosave = True in the save 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.

GitHub logo 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's CloudinaryFileField.

Read the post here

Requirements

Tools and packages required to successfully install this project.

  • Python 3.x and up Install
  • pipenv, version 2018.11.26 or later Install

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

  1. Django - The Django framework
  2. Python - The Python programming language

You can find me here at:






Image from Freepik

Top comments (0)