Skip to content

Commit fbd1a62

Browse files
committed
Fixed #3297 -- Implemented FileField and ImageField for newforms. Thanks to the many users that contributed to and tested this patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@5819 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent e471f42 commit fbd1a62

File tree

9 files changed

+266
-27
lines changed

9 files changed

+266
-27
lines changed

django/db/models/fields/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ def _get_choices(self):
380380
return self._choices
381381
choices = property(_get_choices)
382382

383+
def save_form_data(self, instance, data):
384+
setattr(instance, self.name, data)
385+
383386
def formfield(self, form_class=forms.CharField, **kwargs):
384387
"Returns a django.newforms.Field instance for this database Field."
385388
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
@@ -696,6 +699,13 @@ def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
696699
self.upload_to = upload_to
697700
Field.__init__(self, verbose_name, name, **kwargs)
698701

702+
def get_db_prep_save(self, value):
703+
"Returns field's value prepared for saving into a database."
704+
# Need to convert UploadedFile objects provided via a form to unicode for database insertion
705+
if value is None:
706+
return None
707+
return unicode(value)
708+
699709
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
700710
field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
701711
if not self.blank:
@@ -772,6 +782,19 @@ def get_filename(self, filename):
772782
f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
773783
return os.path.normpath(f)
774784

785+
def save_form_data(self, instance, data):
786+
if data:
787+
getattr(instance, "save_%s_file" % self.name)(os.path.join(self.upload_to, data.filename), data.content, save=False)
788+
789+
def formfield(self, **kwargs):
790+
defaults = {'form_class': forms.FileField}
791+
# If a file has been provided previously, then the form doesn't require
792+
# that a new file is provided this time.
793+
if 'initial' in kwargs:
794+
defaults['required'] = False
795+
defaults.update(kwargs)
796+
return super(FileField, self).formfield(**defaults)
797+
775798
class FilePathField(Field):
776799
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
777800
self.path, self.match, self.recursive = path, match, recursive
@@ -820,6 +843,10 @@ def save_file(self, new_data, new_object, original_object, change, rel, save=Tru
820843
setattr(new_object, self.height_field, getattr(original_object, self.height_field))
821844
new_object.save()
822845

846+
def formfield(self, **kwargs):
847+
defaults = {'form_class': forms.ImageField}
848+
return super(ImageField, self).formfield(**defaults)
849+
823850
class IntegerField(Field):
824851
empty_strings_allowed = False
825852
def get_manipulator_field_objs(self):

django/db/models/fields/related.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,9 @@ def value_from_object(self, obj):
756756
"Returns the value of this field in the given model instance."
757757
return getattr(obj, self.attname).all()
758758

759+
def save_form_data(self, instance, data):
760+
setattr(instance, self.attname, data)
761+
759762
def formfield(self, **kwargs):
760763
defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()}
761764
defaults.update(kwargs)

django/newforms/extras/widgets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def render(self, name, value, attrs=None):
5353

5454
return u'\n'.join(output)
5555

56-
def value_from_datadict(self, data, name):
56+
def value_from_datadict(self, data, files, name):
5757
y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
5858
if y and m and d:
5959
return '%s-%s-%s' % (y, m, d)

django/newforms/fields.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import time
88

99
from django.utils.translation import ugettext
10-
from django.utils.encoding import smart_unicode
10+
from django.utils.encoding import StrAndUnicode, smart_unicode
1111

1212
from util import ErrorList, ValidationError
13-
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
13+
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
1414

1515
try:
1616
from decimal import Decimal, DecimalException
@@ -22,7 +22,7 @@
2222
'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
2323
'DEFAULT_TIME_INPUT_FORMATS', 'TimeField',
2424
'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
25-
'RegexField', 'EmailField', 'URLField', 'BooleanField',
25+
'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField',
2626
'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
2727
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
2828
'SplitDateTimeField',
@@ -348,6 +348,55 @@ def __init__(self, max_length=None, min_length=None, *args, **kwargs):
348348
# It's OK if Django settings aren't configured.
349349
URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
350350

351+
class UploadedFile(StrAndUnicode):
352+
"A wrapper for files uploaded in a FileField"
353+
def __init__(self, filename, content):
354+
self.filename = filename
355+
self.content = content
356+
357+
def __unicode__(self):
358+
"""
359+
The unicode representation is the filename, so that the pre-database-insertion
360+
logic can use UploadedFile objects
361+
"""
362+
return self.filename
363+
364+
class FileField(Field):
365+
widget = FileInput
366+
def __init__(self, *args, **kwargs):
367+
super(FileField, self).__init__(*args, **kwargs)
368+
369+
def clean(self, data):
370+
super(FileField, self).clean(data)
371+
if not self.required and data in EMPTY_VALUES:
372+
return None
373+
try:
374+
f = UploadedFile(data['filename'], data['content'])
375+
except TypeError:
376+
raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form."))
377+
except KeyError:
378+
raise ValidationError(ugettext(u"No file was submitted."))
379+
if not f.content:
380+
raise ValidationError(ugettext(u"The submitted file is empty."))
381+
return f
382+
383+
class ImageField(FileField):
384+
def clean(self, data):
385+
"""
386+
Checks that the file-upload field data contains a valid image (GIF, JPG,
387+
PNG, possibly others -- whatever the Python Imaging Library supports).
388+
"""
389+
f = super(ImageField, self).clean(data)
390+
if f is None:
391+
return None
392+
from PIL import Image
393+
from cStringIO import StringIO
394+
try:
395+
Image.open(StringIO(f.content))
396+
except IOError: # Python Imaging Library doesn't recognize it as an image
397+
raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."))
398+
return f
399+
351400
class URLField(RegexField):
352401
def __init__(self, max_length=None, min_length=None, verify_exists=False,
353402
validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):

django/newforms/forms.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ class BaseForm(StrAndUnicode):
5757
# class is different than Form. See the comments by the Form class for more
5858
# information. Any improvements to the form API should be made to *this*
5959
# class, not to the Form class.
60-
def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
61-
self.is_bound = data is not None
60+
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
61+
self.is_bound = data is not None or files is not None
6262
self.data = data or {}
63+
self.files = files or {}
6364
self.auto_id = auto_id
6465
self.prefix = prefix
6566
self.initial = initial or {}
@@ -88,7 +89,7 @@ def __getitem__(self, name):
8889
return BoundField(self, field, name)
8990

9091
def _get_errors(self):
91-
"Returns an ErrorDict for self.data"
92+
"Returns an ErrorDict for the data provided for the form"
9293
if self._errors is None:
9394
self.full_clean()
9495
return self._errors
@@ -179,10 +180,10 @@ def full_clean(self):
179180
return
180181
self.cleaned_data = {}
181182
for name, field in self.fields.items():
182-
# value_from_datadict() gets the data from the dictionary.
183+
# value_from_datadict() gets the data from the data dictionaries.
183184
# Each widget type knows how to retrieve its own data, because some
184185
# widgets split data over several HTML fields.
185-
value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
186+
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
186187
try:
187188
value = field.clean(value)
188189
self.cleaned_data[name] = value
@@ -283,7 +284,7 @@ def _data(self):
283284
"""
284285
Returns the data for this BoundField, or None if it wasn't given.
285286
"""
286-
return self.field.widget.value_from_datadict(self.form.data, self.html_name)
287+
return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
287288
data = property(_data)
288289

289290
def label_tag(self, contents=None, attrs=None):

django/newforms/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True
3434
continue
3535
if fields and f.name not in fields:
3636
continue
37-
setattr(instance, f.name, cleaned_data[f.name])
37+
f.save_form_data(instance, cleaned_data[f.name])
3838
# Wrap up the saving of m2m data as a function
3939
def save_m2m():
4040
opts = instance.__class__._meta
@@ -43,7 +43,7 @@ def save_m2m():
4343
if fields and f.name not in fields:
4444
continue
4545
if f.name in cleaned_data:
46-
setattr(instance, f.attname, cleaned_data[f.name])
46+
f.save_form_data(instance, cleaned_data[f.name])
4747
if commit:
4848
# If we are committing, save the instance and the m2m data immediately
4949
instance.save()

django/newforms/widgets.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def build_attrs(self, extra_attrs=None, **kwargs):
4747
attrs.update(extra_attrs)
4848
return attrs
4949

50-
def value_from_datadict(self, data, name):
50+
def value_from_datadict(self, data, files, name):
5151
"""
5252
Given a dictionary of data and this widget's name, returns the value
5353
of this widget. Returns None if it's not provided.
@@ -113,14 +113,21 @@ def render(self, name, value, attrs=None, choices=()):
113113
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
114114
return u'\n'.join([(u'<input%s />' % flatatt(dict(value=force_unicode(v), **final_attrs))) for v in value])
115115

116-
def value_from_datadict(self, data, name):
116+
def value_from_datadict(self, data, files, name):
117117
if isinstance(data, MultiValueDict):
118118
return data.getlist(name)
119119
return data.get(name, None)
120120

121121
class FileInput(Input):
122122
input_type = 'file'
123123

124+
def render(self, name, value, attrs=None):
125+
return super(FileInput, self).render(name, None, attrs=attrs)
126+
127+
def value_from_datadict(self, data, files, name):
128+
"File widgets take data from FILES, not POST"
129+
return files.get(name, None)
130+
124131
class Textarea(Widget):
125132
def __init__(self, attrs=None):
126133
# The 'rows' and 'cols' attributes are required for HTML correctness.
@@ -188,7 +195,7 @@ def render(self, name, value, attrs=None, choices=()):
188195
value = u'1'
189196
return super(NullBooleanSelect, self).render(name, value, attrs, choices)
190197

191-
def value_from_datadict(self, data, name):
198+
def value_from_datadict(self, data, files, name):
192199
value = data.get(name, None)
193200
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
194201

@@ -210,7 +217,7 @@ def render(self, name, value, attrs=None, choices=()):
210217
output.append(u'</select>')
211218
return u'\n'.join(output)
212219

213-
def value_from_datadict(self, data, name):
220+
def value_from_datadict(self, data, files, name):
214221
if isinstance(data, MultiValueDict):
215222
return data.getlist(name)
216223
return data.get(name, None)
@@ -377,8 +384,8 @@ def id_for_label(self, id_):
377384
return id_
378385
id_for_label = classmethod(id_for_label)
379386

380-
def value_from_datadict(self, data, name):
381-
return [widget.value_from_datadict(data, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
387+
def value_from_datadict(self, data, files, name):
388+
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
382389

383390
def format_output(self, rendered_widgets):
384391
"""

docs/newforms.txt

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,47 @@ For example::
710710
</ul>
711711
</form>
712712

713+
Binding uploaded files to a form
714+
--------------------------------
715+
716+
Dealing with forms that have ``FileField`` and ``ImageField`` fields
717+
is a little more complicated than a normal form.
718+
719+
Firstly, in order to upload files, you'll need to make sure that your
720+
``<form>`` element correctly defines the ``enctype`` as
721+
``"multipart/form-data"``::
722+
723+
<form enctype="multipart/form-data" method="post" action="/foo/">
724+
725+
Secondly, when you use the form, you need to bind the file data. File
726+
data is handled separately to normal form data, so when your form
727+
contains a ``FileField`` and ``ImageField``, you will need to specify
728+
a second argument when you bind your form. So if we extend our
729+
ContactForm to include an ``ImageField`` called ``mugshot``, we
730+
need to bind the file data containing the mugshot image::
731+
732+
# Bound form with an image field
733+
>>> data = {'subject': 'hello',
734+
... 'message': 'Hi there',
735+
... 'sender': 'foo@example.com',
736+
... 'cc_myself': True}
737+
>>> file_data = {'mugshot': {'filename':'face.jpg'
738+
... 'content': <file data>}}
739+
>>> f = ContactFormWithMugshot(data, file_data)
740+
741+
In practice, you will usually specify ``request.FILES`` as the source
742+
of file data (just like you use ``request.POST`` as the source of
743+
form data)::
744+
745+
# Bound form with an image field, data from the request
746+
>>> f = ContactFormWithMugshot(request.POST, request.FILES)
747+
748+
Constructing an unbound form is the same as always -- just omit both
749+
form data *and* file data:
750+
751+
# Unbound form with a image field
752+
>>> f = ContactFormWithMugshot()
753+
713754
Subclassing forms
714755
-----------------
715756

@@ -1099,6 +1140,50 @@ Has two optional arguments for validation, ``max_length`` and ``min_length``.
10991140
If provided, these arguments ensure that the string is at most or at least the
11001141
given length.
11011142

1143+
``FileField``
1144+
~~~~~~~~~~~~~
1145+
1146+
* Default widget: ``FileInput``
1147+
* Empty value: ``None``
1148+
* Normalizes to: An ``UploadedFile`` object that wraps the file content
1149+
and file name into a single object.
1150+
* Validates that non-empty file data has been bound to the form.
1151+
1152+
An ``UploadedFile`` object has two attributes:
1153+
1154+
====================== =====================================================
1155+
Argument Description
1156+
====================== =====================================================
1157+
``filename`` The name of the file, provided by the uploading
1158+
client.
1159+
``content`` The array of bytes comprising the file content.
1160+
====================== =====================================================
1161+
1162+
The string representation of an ``UploadedFile`` is the same as the filename
1163+
attribute.
1164+
1165+
When you use a ``FileField`` on a form, you must also remember to
1166+
`bind the file data to the form`_.
1167+
1168+
.. _`bind the file data to the form`: `Binding uploaded files to a form`_
1169+
1170+
``ImageField``
1171+
~~~~~~~~~~~~~~
1172+
1173+
* Default widget: ``FileInput``
1174+
* Empty value: ``None``
1175+
* Normalizes to: An ``UploadedFile`` object that wraps the file content
1176+
and file name into a single object.
1177+
* Validates that file data has been bound to the form, and that the
1178+
file is of an image format understood by PIL.
1179+
1180+
Using an ImageField requires that the `Python Imaging Library`_ is installed.
1181+
1182+
When you use a ``FileField`` on a form, you must also remember to
1183+
`bind the file data to the form`_.
1184+
1185+
.. _Python Imaging Library: http://www.pythonware.com/products/pil/
1186+
11021187
``IntegerField``
11031188
~~~~~~~~~~~~~~~~
11041189

@@ -1378,11 +1463,11 @@ the full list of conversions:
13781463
``DateTimeField`` ``DateTimeField``
13791464
``DecimalField`` ``DecimalField``
13801465
``EmailField`` ``EmailField``
1381-
``FileField`` ``CharField``
1466+
``FileField`` ``FileField``
13821467
``FilePathField`` ``CharField``
13831468
``FloatField`` ``FloatField``
13841469
``ForeignKey`` ``ModelChoiceField`` (see below)
1385-
``ImageField`` ``CharField``
1470+
``ImageField`` ``ImageField``
13861471
``IntegerField`` ``IntegerField``
13871472
``IPAddressField`` ``CharField``
13881473
``ManyToManyField`` ``ModelMultipleChoiceField`` (see

0 commit comments

Comments
 (0)