Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I followed the pattern of the docs, to upload several files with one forms.FileField:

https://docs.djangoproject.com/en/1.11/topics/http/file-uploads/#uploading-multiple-files

Unfortunately cleaned_data['file'] does contain one file, not both files.

What needs to be done to have all uploaded files on cleaned_data['file']?

Here is the code from the docs:

forms.py

from django import forms

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

views.py

from django.views.generic.edit import FormView
from .forms import FileFieldForm

class FileFieldView(FormView):
    form_class = FileFieldForm
    template_name = 'upload.html'  # Replace with your template.
    success_url = '...'  # Replace with your URL or reverse().

    def post(self, request, *args, **kwargs):
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        files = request.FILES.getlist('file_field')
        if form.is_valid():
            for f in files:
                ...  # Do something with each file.
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

Update

There is a pull request to solve this issue: https://github.com/django/django/pull/9011

like image 379
guettli Avatar asked Sep 20 '17 09:09

guettli


People also ask

Does Python Django support multiple file upload?

The Django documentation for File Uploads, gives a brief understanding of the workflow of how to upload a single file using forms and importing in views. In a similar way, we can also tweak it to upload multiple files using forms and views.


2 Answers

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.

like image 162
Dunatotatos Avatar answered Sep 30 '22 18:09

Dunatotatos


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
like image 27
Astik Anand Avatar answered Sep 30 '22 18:09

Astik Anand