diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index d0cc657c4a..b705eac512 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -380,6 +380,9 @@ class Field(object): 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} @@ -696,6 +699,13 @@ class FileField(Field): 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 + if value is None: + return None + return unicode(value) + 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: @@ -772,6 +782,19 @@ class FileField(Field): 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 @@ -820,6 +843,10 @@ class ImageField(FileField): 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): diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index c4284f04b1..d3603b0016 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -756,6 +756,9 @@ class ManyToManyField(RelatedField, Field): "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) diff --git a/django/newforms/extras/widgets.py b/django/newforms/extras/widgets.py index 724dcd9b50..96b1c7244d 100644 --- a/django/newforms/extras/widgets.py +++ b/django/newforms/extras/widgets.py @@ -53,7 +53,7 @@ class SelectDateWidget(Widget): 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) diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 79e90da5fb..e9f50ad4ec 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -7,10 +7,10 @@ import re import time from django.utils.translation import ugettext -from django.utils.encoding import smart_unicode +from django.utils.encoding import StrAndUnicode, smart_unicode from util import ErrorList, ValidationError -from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple try: from decimal import Decimal, DecimalException @@ -22,7 +22,7 @@ __all__ = ( 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', - 'RegexField', 'EmailField', 'URLField', 'BooleanField', + 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField', 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', @@ -348,6 +348,55 @@ except ImportError: # 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): diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 5da85a69c4..906978c86f 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -57,9 +57,10 @@ class BaseForm(StrAndUnicode): # 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 @@ class BaseForm(StrAndUnicode): 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 @@ -179,10 +180,10 @@ class BaseForm(StrAndUnicode): 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 @@ -283,7 +284,7 @@ class BoundField(StrAndUnicode): """ 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): diff --git a/django/newforms/models.py b/django/newforms/models.py index 7a86e30b1d..247a0eea6b 100644 --- a/django/newforms/models.py +++ b/django/newforms/models.py @@ -34,7 +34,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True continue if fields and f.name not in fields: continue - setattr(instance, f.name, cleaned_data[f.name]) + f.save_form_data(instance, cleaned_data[f.name]) # Wrap up the saving of m2m data as a function def save_m2m(): opts = instance.__class__._meta @@ -43,7 +43,7 @@ def save_instance(form, instance, fields=None, fail_message='saved', commit=True 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]) if commit: # If we are committing, save the instance and the m2m data immediately instance.save() diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py index e9b9b55470..f985124389 100644 --- a/django/newforms/widgets.py +++ b/django/newforms/widgets.py @@ -47,7 +47,7 @@ class Widget(object): 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 @@ class MultipleHiddenInput(HiddenInput): 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 MultipleHiddenInput(HiddenInput): 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 @@ class NullBooleanSelect(Select): 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 @@ class SelectMultiple(Widget): 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) @@ -377,8 +384,8 @@ class MultiWidget(Widget): 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): """ diff --git a/docs/newforms.txt b/docs/newforms.txt index dae145434b..5d1da96128 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -710,6 +710,47 @@ For example:: +Binding uploaded files to a form +-------------------------------- + +Dealing with forms that have ``FileField`` and ``ImageField`` fields +is a little more complicated than a normal form. + +Firstly, in order to upload files, you'll need to make sure that your +``