[Fixed]-Django: Uploading multiple files. List of files needed in cleaned_data['file']

23👍

What happens

When your run form.is_valid(), the fields are validated and cleaned one after one, and stored in the cleaned_data variable. If you look at the Django source code, you’ll find that your form fields go through an individual validation in the _clean_fields methods of the class BaseForm in the file django/forms/forms.py

The validation is made according to the widget type (ie forms.ClearableFileInput in the case of the field you are interested in). Going a bit deeper shows you that the cleaned_data is filled with files.get(name) where files is the list of the updated files, and name is the name of the field currently being validated.

The type of files is MultiValueDict. If you look at the code in django/utils/datastructures.py, you’ll find some interesting stuff around the line 48. I copy the docstring here :

A subclass of dictionary customized to handle multiple values for the
same key.

>>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']})
>>> d['name']
'Simon'
>>> d.getlist('name')
['Adrian', 'Simon']
>>> d.getlist('doesnotexist')
[]
>>> d.getlist('doesnotexist', ['Adrian', 'Simon'])
['Adrian', 'Simon']
>>> d.get('lastname', 'nonexistent')
'nonexistent'
>>> d.setlist('lastname', ['Holovaty', 'Willison'])

This class exists to solve the irritating problem raised by cgi.parse_qs,
which returns a list for every key, even though most Web forms submit
single name-value pairs.

As this behavior depends only on the widget of the field, I can see three different solutions from now.

The solutions

  1. You patch Django to have a correct behavior when the attrs of the widget is set to multiple. (I was about to do it, but I’m really not sure about the consequences.) I’ll study that in depth and may submit a PR.
  2. You create your own Widget, a children of ClearableFileInput, which override the value_from_datadict method to use files.getlist(name) instead of file.get(name).
  3. You use request.FILES.getlist('your_filed_name') as suggested by Astik Anand, or any easier solution.

Let’s take a closer look at the solution 2.
Here are some instructions to create your own widget based on ClearableFileInput. Unfortunately, it is not enough to make it work, as the data are sent through a cleaning process owned by the field. You must create your own FileField as well.

# widgets.py
from django.forms.widgets import ClearableFileInput
from django.forms.widgets import CheckboxInput

FILE_INPUT_CONTRADICTION = object()

class ClearableMultipleFilesInput(ClearableFileInput):
    def value_from_datadict(self, data, files, name):
        upload = files.getlist(name) # files.get(name) in Django source

        if not self.is_required and CheckboxInput().value_from_datadict(
                data, files, self.clear_checkbox_name(name)):

            if upload:
                # If the user contradicts themselves (uploads a new file AND
                # checks the "clear" checkbox), we return a unique marker
                # objects that FileField will turn into a ValidationError.
                return FILE_INPUT_CONTRADICTION
            # False signals to clear any existing value, as opposed to just None
            return False
        return upload

This part is basically taken word by word from the methods of ClearableFileInput, except the first line of value_from_datadict which was upload = files.get(name).

As mentioned before, you also have to create your own Field to override the to_python method of FileField which tries to access a self.name and self.size attributes.

# fields.py
from django.forms.fields import FileField
from .widgets import ClearableMultipleFilesInput
from .widgets import FILE_INPUT_CONTRADICTION

class MultipleFilesField(FileField):
    widget = ClearableMultipleFilesInput

    def clean(self, data, initial=None):
        # If the widget got contradictory inputs, we raise a validation error
        if data is FILE_INPUT_CONTRADICTION:
            raise ValidationError(self.error_message['contradiction'], code='contradiction')
        # False means the field value should be cleared; further validation is
        # not needed.
        if data is False:
            if not self.required:
                return False
            # If the field is required, clearing is not possible (the widg    et
            # shouldn't return False data in that case anyway). False is not
            # in self.empty_value; if a False value makes it this far
            # it should be validated from here on out as None (so it will be
            # caught by the required check).
            data = None
        if not data and initial:
            return initial
        return data

And here is how to use it in your form:

# forms.py
from .widgets import ClearableMultipleFilesInput
from .fields import MultipleFilesField

your_field = MultipleFilesField(
    widget=ClearableMultipleFilesInput(
        attrs={'multiple': True}))

And it works!

>>> print(form.cleaned_data['your_field']
[<TemporaryUploadedFile: file1.pdf (application/pdf)>, <TemporaryUploadedFile: file2.pdf (application/pdf)>, <TemporaryUploadedFile: file3.pdf (application/pdf)>]

Of course, this solution cannot be used directly and needs a lot of improvements. Here, we basically erase all the checking made in the FileField field, we do not set a maximum number of files, the attrs={'multiple': True} is redundant with the widget name, and many similar things. As well, I am pretty sure I missed some important methods in the FileField or ClearableFileInput. This is only a starting idea, but you’ll need much more work, and a look at the widgets and fields on the official documentation.

8👍

I assume that you have:

class FileFieldForm(forms.Form):
     files = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))

and you are trying to get files using : cleaned_data['files'] and you are getting only 1 file instead of 2.

The Reason:

What is happening here is, When you try to do something like this

file in self.cleaned_data['files]:, 

thinking that, you can iterate over a list of uploadedFile objects and pass each to the handler function.

But cleaned_data['files'] is not a list for you, it’s just ONE single instance of uploadedfile.

When you iterate over a file object, you’re actually reading it. So what you pass eventually to the handler function is not the file object but its content (as a bytes string).

The solution

You need to get a list of files and then, perform something what you want on them as below.

files = request.FILES.getlist('files')

for f in files:
    ...  # Do something with each file considering f as file object

1👍

You can use this library: https://github.com/Chive/django-multiupload

Django Multiupload

Dead simple drop-in multi file upload field for django forms using
HTML5’s multiple attribute.

Leave a comment