Index: django/db/models/fields/__init__.py
===================================================================
--- django/db/models/fields/__init__.py (revision 5741)
+++ django/db/models/fields/__init__.py (working copy)
@@ -74,7 +74,7 @@
core=False, rel=None, default=NOT_PROVIDED, editable=True, serialize=True,
unique_for_date=None, unique_for_month=None, unique_for_year=None,
validator_list=None, choices=None, radio_admin=None, help_text='', db_column=None,
- db_tablespace=None):
+ db_tablespace=None, save_on_commit=None):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@@ -95,6 +95,7 @@
self.help_text = help_text
self.db_column = db_column
self.db_tablespace = db_tablespace
+ self.save_on_commit = save_on_commit
# Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
self.db_index = db_index
@@ -349,6 +350,9 @@
return self._choices
choices = property(_get_choices)
+ def save_form_data(self, instance, data):
+ setattr(instance, self.name, data)
+
def formfield(self, form_class=forms.CharField, **kwargs):
"Returns a django.newforms.Field instance for this database Field."
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
@@ -664,6 +668,11 @@
self.upload_to = upload_to
Field.__init__(self, verbose_name, name, **kwargs)
+ def get_db_prep_save(self, value):
+ "Returns field's value prepared for saving into a database."
+ # Need to convert UploadedFile objects provided via a form to unicode for database insertion
+ return value and unicode(value) or None
+
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
if not self.blank:
@@ -740,6 +749,19 @@
f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
return os.path.normpath(f)
+ def save_form_data(self, instance, data):
+ if data:
+ getattr(instance, "save_%s_file" % self.name)(os.path.join(self.upload_to, data.filename), data.content, save=False)
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class': forms.FileField}
+ # If a file has been provided previously, then the form doesn't require
+ # that a new file is provided this time.
+ if 'initial' in kwargs:
+ defaults['required'] = False
+ defaults.update(kwargs)
+ return super(FileField, self).formfield(**defaults)
+
class FilePathField(Field):
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
@@ -788,6 +810,10 @@
setattr(new_object, self.height_field, getattr(original_object, self.height_field))
new_object.save()
+ def formfield(self, **kwargs):
+ defaults = {'form_class': forms.ImageField}
+ return super(ImageField, self).formfield(**defaults)
+
class IntegerField(Field):
empty_strings_allowed = False
def get_manipulator_field_objs(self):
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py (revision 5741)
+++ django/db/models/fields/related.py (working copy)
@@ -710,6 +710,9 @@
"Returns the value of this field in the given model instance."
return getattr(obj, self.attname).all()
+ def save_form_data(self, instance, data):
+ setattr(instance, self.attname, data)
+
def formfield(self, **kwargs):
defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()}
defaults.update(kwargs)
Index: django/newforms/extras/widgets.py
===================================================================
--- django/newforms/extras/widgets.py (revision 5741)
+++ django/newforms/extras/widgets.py (working copy)
@@ -53,7 +53,7 @@
return u'\n'.join(output)
- def value_from_datadict(self, data, name):
+ def value_from_datadict(self, data, files, name):
y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
if y and m and d:
return '%s-%s-%s' % (y, m, d)
Index: django/newforms/models.py
===================================================================
--- django/newforms/models.py (revision 5741)
+++ django/newforms/models.py (working copy)
@@ -30,19 +30,23 @@
if form.errors:
raise ValueError("The %s could not be %s because the data didn't validate." % (opts.object_name, fail_message))
cleaned_data = form.cleaned_data
+ commit_fields = []
for f in opts.fields:
if not f.editable or isinstance(f, models.AutoField) or not f.name in cleaned_data:
continue
if fields and f.name not in fields:
continue
- setattr(instance, f.name, cleaned_data[f.name])
+ if f.save_on_commit:
+ commit_fields.append(f)
+ continue
+ f.save_form_data(instance, cleaned_data[f.name])
if commit:
instance.save()
- for f in opts.many_to_many:
+ for f in commit_fields + opts.many_to_many:
if fields and f.name not in fields:
continue
if f.name in cleaned_data:
- setattr(instance, f.attname, cleaned_data[f.name])
+ f.save_form_data(instance, cleaned_data[f.name])
# GOTCHA: If many-to-many data is given and commit=False, the many-to-many
# data will be lost. This happens because a many-to-many options cannot be
# set on an object until after it's saved. Maybe we should raise an
Index: django/newforms/fields.py
===================================================================
--- django/newforms/fields.py (revision 5741)
+++ django/newforms/fields.py (working copy)
@@ -7,10 +7,10 @@
import time
from django.utils.translation import ugettext
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import smart_unicode, StrAndUnicode
from util import ErrorList, ValidationError
-from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, FileInput
__all__ = (
'Field', 'CharField', 'IntegerField',
@@ -20,7 +20,7 @@
'RegexField', 'EmailField', 'URLField', 'BooleanField',
'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
- 'SplitDateTimeField',
+ 'SplitDateTimeField', 'FileField', 'ImageField'
)
# These values, if given to to_python(), will trigger the self.required check.
@@ -351,6 +351,55 @@
# It's OK if Django settings aren't configured.
URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
+class UploadedFile(StrAndUnicode):
+ "A wrapper for files uploaded in a FileField"
+ def __init__(self, filename, content):
+ self.filename = filename
+ self.content = content
+
+ def __unicode__(self):
+ """
+ The unicode representation is the filename, so that the pre-database-insertion
+ logic can use UploadedFile objects
+ """
+ return self.filename
+
+class FileField(Field):
+ widget = FileInput
+ def __init__(self, *args, **kwargs):
+ super(FileField, self).__init__(*args, **kwargs)
+
+ def clean(self, data):
+ super(FileField, self).clean(data)
+ if not self.required and data in EMPTY_VALUES:
+ return None
+ try:
+ f = UploadedFile(data['filename'], data['content'])
+ except TypeError:
+ raise ValidationError(ugettext(u"No file was submitted. Check the encoding type on the form."))
+ except KeyError:
+ raise ValidationError(ugettext(u"No file was submitted."))
+ if not f.content:
+ raise ValidationError(ugettext(u"The submitted file is empty."))
+ return f
+
+class ImageField(FileField):
+ def clean(self, data):
+ """
+ Checks that the file-upload field data contains a valid image (GIF, JPG,
+ PNG, possibly others -- whatever the Python Imaging Library supports).
+ """
+ f = super(ImageField, self).clean(data)
+ if f is None:
+ return None
+ from PIL import Image
+ from cStringIO import StringIO
+ try:
+ Image.open(StringIO(f.content))
+ except IOError: # Python Imaging Library doesn't recognize it as an image
+ raise ValidationError(ugettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."))
+ return f
+
class URLField(RegexField):
def __init__(self, max_length=None, min_length=None, verify_exists=False,
validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
Index: django/newforms/forms.py
===================================================================
--- django/newforms/forms.py (revision 5741)
+++ django/newforms/forms.py (working copy)
@@ -57,9 +57,10 @@
# class is different than Form. See the comments by the Form class for more
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
- def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
- self.is_bound = data is not None
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None):
+ self.is_bound = data is not None or files is not None
self.data = data or {}
+ self.files = files or {}
self.auto_id = auto_id
self.prefix = prefix
self.initial = initial or {}
@@ -88,7 +89,7 @@
return BoundField(self, field, name)
def _get_errors(self):
- "Returns an ErrorDict for self.data"
+ "Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors
@@ -198,10 +199,10 @@
return
self.cleaned_data = {}
for name, field in self.fields.items():
- # value_from_datadict() gets the data from the dictionary.
+ # value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
- value = field.widget.value_from_datadict(self.data, self.add_prefix(name))
+ value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
value = field.clean(value)
self.cleaned_data[name] = value
@@ -309,7 +310,7 @@
"""
Returns the data for this BoundField, or None if it wasn't given.
"""
- return self.field.widget.value_from_datadict(self.form.data, self.html_name)
+ return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
data = property(_data)
def label_tag(self, contents=None, attrs=None):
Index: django/newforms/widgets.py
===================================================================
--- django/newforms/widgets.py (revision 5741)
+++ django/newforms/widgets.py (working copy)
@@ -47,7 +47,7 @@
attrs.update(extra_attrs)
return attrs
- def value_from_datadict(self, data, name):
+ def value_from_datadict(self, data, files, name):
"""
Given a dictionary of data and this widget's name, returns the value
of this widget. Returns None if it's not provided.
@@ -113,7 +113,7 @@
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
return u'\n'.join([(u'' % flatatt(dict(value=force_unicode(v), **final_attrs))) for v in value])
- def value_from_datadict(self, data, name):
+ def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
return data.getlist(name)
return data.get(name, None)
@@ -121,6 +121,13 @@
class FileInput(Input):
input_type = 'file'
+ def render(self, name, value, attrs=None):
+ return super(FileInput, self).render(name, None, attrs=attrs)
+
+ def value_from_datadict(self, data, files, name):
+ "File widgets take data from FILES, not POST"
+ return files.get(name, None)
+
class Textarea(Widget):
def __init__(self, attrs=None):
# The 'rows' and 'cols' attributes are required for HTML correctness.
@@ -188,7 +195,7 @@
value = u'1'
return super(NullBooleanSelect, self).render(name, value, attrs, choices)
- def value_from_datadict(self, data, name):
+ def value_from_datadict(self, data, files, name):
value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
@@ -210,7 +217,7 @@
output.append(u'')
return u'\n'.join(output)
- def value_from_datadict(self, data, name):
+ def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
return data.getlist(name)
return data.get(name, None)
@@ -356,8 +363,8 @@
return id_
id_for_label = classmethod(id_for_label)
- def value_from_datadict(self, data, name):
- return [widget.value_from_datadict(data, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+ def value_from_datadict(self, data, files, name):
+ return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
def format_output(self, rendered_widgets):
"""
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py (revision 5741)
+++ django/contrib/admin/options.py (working copy)
@@ -418,10 +418,7 @@
inline_formsets = []
if request.POST:
- new_data = request.POST.copy()
- if opts.has_field_type(models.FileField):
- new_data.update(request.FILES)
- form = ModelForm(new_data)
+ form = ModelForm(request.POST, request.FILES)
for FormSet in self.get_inline_formsets():
inline_formset = FormSet(data=new_data)
inline_formsets.append(inline_formset)
@@ -471,10 +468,7 @@
inline_formsets = []
if request.POST:
- new_data = request.POST.copy()
- if opts.has_field_type(models.FileField):
- new_data.update(request.FILES)
- form = ModelForm(new_data)
+ form = ModelForm(request.POST, request.FILES)
for FormSet in self.get_inline_formsets():
inline_formset = FormSet(obj, new_data)
inline_formsets.append(inline_formset)
Index: django/contrib/admin/urls.py
===================================================================
--- django/contrib/admin/urls.py (revision 5741)
+++ django/contrib/admin/urls.py (working copy)
@@ -1,10 +1,10 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('',
- #('^$', 'django.contrib.admin.views.main.index'),
+ ('^$', 'django.contrib.admin.views.main.index'),
('^r/(\d+)/(.*)/$', 'django.views.defaults.shortcut'),
#('^jsi18n/$', i18n_view, {'packages': 'django.conf'}),
- #('^logout/$', 'django.contrib.auth.views.logout'),
+ ('^logout/$', 'django.contrib.auth.views.logout'),
#('^password_change/$', 'django.contrib.auth.views.password_change'),
#('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
('^template_validator/$', 'django.contrib.admin.views.template.template_validator'),
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py (revision 5741)
+++ tests/regressiontests/forms/tests.py (working copy)
@@ -173,27 +173,29 @@
# FileInput Widget ############################################################
+FileInput widgets don't ever show the value, because the old value is of no use
+if you are updating the form or if the provided file generated an error.
>>> w = FileInput()
>>> w.render('email', '')
u''
>>> w.render('email', None)
u''
>>> w.render('email', 'test@example.com')
-u''
+u''
>>> w.render('email', 'some "quoted" & ampersanded value')
-u''
+u''
>>> w.render('email', 'test@example.com', attrs={'class': 'fun'})
-u''
+u''
You can also pass 'attrs' to the constructor:
>>> w = FileInput(attrs={'class': 'fun'})
>>> w.render('email', '')
u''
>>> w.render('email', 'foo@example.com')
-u''
+u''
>>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'})
-u''
+u''
# Textarea Widget #############################################################
@@ -1517,6 +1519,42 @@
...
ValidationError: [u'Ensure this value has at most 15 characters.']
+# FileField ##################################################################
+
+>>> f = FileField()
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f.clean({})
+Traceback (most recent call last):
+...
+ValidationError: [u'No file was submitted.']
+
+>>> f.clean('some content that is not a file')
+Traceback (most recent call last):
+...
+ValidationError: [u'No file was submitted. Check the encoding type on the form.']
+
+>>> f.clean({'filename': 'name', 'content':None})
+Traceback (most recent call last):
+...
+ValidationError: [u'The submitted file is empty.']
+
+>>> f.clean({'filename': 'name', 'content':''})
+Traceback (most recent call last):
+...
+ValidationError: [u'The submitted file is empty.']
+
+>>> type(f.clean({'filename': 'name', 'content':'Some File Content'}))
+
+
# URLField ##################################################################
>>> f = URLField()
@@ -2558,7 +2596,7 @@
the next.
>>> class MyForm(Form):
... def __init__(self, data=None, auto_id=False, field_list=[]):
-... Form.__init__(self, data, auto_id)
+... Form.__init__(self, data, auto_id=auto_id)
... for field in field_list:
... self.fields[field[0]] = field[1]
>>> field_list = [('field1', CharField()), ('field2', CharField())]
@@ -2576,7 +2614,7 @@
... default_field_1 = CharField()
... default_field_2 = CharField()
... def __init__(self, data=None, auto_id=False, field_list=[]):
-... Form.__init__(self, data, auto_id)
+... Form.__init__(self, data, auto_id=auto_id)
... for field in field_list:
... self.fields[field[0]] = field[1]
>>> field_list = [('field1', CharField()), ('field2', CharField())]
@@ -3231,6 +3269,35 @@
+# Forms with FileFields ################################################
+
+FileFields are a special case because they take their data from the request.FILES,
+not request.POST.
+
+>>> class FileForm(Form):
+... file1 = FileField()
+>>> f = FileForm(auto_id=False)
+>>> print f
+
File1:
+
+>>> f = FileForm(data={}, files={}, auto_id=False)
+>>> print f
+
File1:
This field is required.
+
+>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':''}}, auto_id=False)
+>>> print f
+
File1:
The submitted file is empty.
+
+>>> f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False)
+>>> print f
+
File1:
No file was submitted. Check the encoding type on the form.
+
+>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False)
+>>> print f
+
File1:
+>>> f.is_valid()
+True
+
# Basic form processing in a view #############################################
>>> from django.template import Template, Context