How to use autosave=False in CloudinaryFileField when using Django forms
Tags: webdev, django, python, cloudinary
Published: 2023-02-07 15:32:05

## Intro
According to [Wikipedia](https://en.wikipedia.org/wiki/Cloudinary), 
> Cloudinary is a company that provides cloud-based image and video management services. 

{% embed https://en.wikipedia.org/wiki/Cloudinary %}

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](https://adityamali33.medium.com/how-to-use-cloudinary-with-django-application-for-uploading-images-95f5a3694094) medium post or [Shuaib Oseni's](https://www.section.io/engineering-education/uploading-images-to-cloudinary-from-django-application/) section.io post. Of course, there are many others. I also encourage you to view [Cloudinary's](https://cloudinary.com/documentation/django_integration) 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](url) of the Cloudinary python package ([here's](https://www.linkedin.com/feed/update/urn:li:activity:7013829285341753344?utm_source=share&utm_medium=member_desktop) 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**
```python
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**
```python
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**
```python
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*

![Form submitted with errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8536kf1pf8tpf1rbcw77.png)

*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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ktlosqe8m24cvb2jvmdn.png)

## The Solution: Source code to the rescue
Let us now take a look at the source code of the `CloudinaryFileField` [here](https://github.com/cloudinary/pycloudinary/blob/master/cloudinary/forms.py).

**CloudinaryFileField source code**
```python
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**
```python
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.*

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](https://cloudinary.com/documentation/django_image_and_video_upload#upload_response).
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](https://github.com/cloudinary/pycloudinary/blob/master/cloudinary/forms.py) 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.
{% embed https://github.com/Daveson217/django-cloudinary-autosave %}

Image from <a href="https://www.freepik.com/free-vector/gradient-white-monochrome-background_15185924.htm#query=background&position=1&from_view=search&track=aitest">Freepik</a>