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
2 Answers
Answers 1
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
- You patch Django to have a correct behavior when the
attrs
of the widget is set tomultiple
. (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. - You create your own Widget, a children of
ClearableFileInput
, which override thevalue_from_datadict
method to usefiles.getlist(name)
instead offile.get(name)
. - 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.
Answers 2
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
Hope it helps!!!
0 comments:
Post a Comment