Ticket #3297: filefield-5779.diff

File filefield-5779.diff, 18.2 KB (added by Russell Keith-Magee, 12 years ago)

File and Image fields, patch against trunk [5779]

  • django/db/models/fields/__init__.py

     
    376376            return self._choices
    377377    choices = property(_get_choices)
    378378
     379    def save_form_data(self, instance, data):
     380        setattr(instance, self.name, data)
     381       
    379382    def formfield(self, form_class=forms.CharField, **kwargs):
    380383        "Returns a django.newforms.Field instance for this database Field."
    381384        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
     
    692695        self.upload_to = upload_to
    693696        Field.__init__(self, verbose_name, name, **kwargs)
    694697
     698    def get_db_prep_save(self, value):
     699        "Returns field's value prepared for saving into a database."
     700        # Need to convert UploadedFile objects provided via a form to unicode for database insertion
     701        return value and unicode(value) or None
     702
    695703    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
    696704        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
    697705        if not self.blank:
     
    768776        f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
    769777        return os.path.normpath(f)
    770778
     779    def save_form_data(self, instance, data):
     780        if data:
     781            getattr(instance, "save_%s_file" % self.name)(os.path.join(self.upload_to, data.filename), data.content, save=False)
     782       
     783    def formfield(self, **kwargs):
     784        defaults = {'form_class': forms.FileField}
     785        # If a file has been provided previously, then the form doesn't require
     786        # that a new file is provided this time.
     787        if 'initial' in kwargs:
     788            defaults['required'] = False
     789        defaults.update(kwargs)
     790        return super(FileField, self).formfield(**defaults)
     791
    771792class FilePathField(Field):
    772793    def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
    773794        self.path, self.match, self.recursive = path, match, recursive
     
    816837                setattr(new_object, self.height_field, getattr(original_object, self.height_field))
    817838            new_object.save()
    818839
     840    def formfield(self, **kwargs):
     841        defaults = {'form_class': forms.ImageField}
     842        return super(ImageField, self).formfield(**defaults)
     843
    819844class IntegerField(Field):
    820845    empty_strings_allowed = False
    821846    def get_manipulator_field_objs(self):
  • django/db/models/fields/related.py

     
    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

     
    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/models.py

     
    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    if commit:
    3939        instance.save()
    4040        for f in opts.many_to_many:
    4141            if fields and f.name not in fields:
    4242                continue
    4343            if f.name in cleaned_data:
    44                 setattr(instance, f.attname, cleaned_data[f.name])
     44                f.save_form_data(instance, cleaned_data[f.name])
    4545    # GOTCHA: If many-to-many data is given and commit=False, the many-to-many
    4646    # data will be lost. This happens because a many-to-many options cannot be
    4747    # set on an object until after it's saved. Maybe we should raise an
  • django/newforms/fields.py

     
    77import time
    88
    99from django.utils.translation import ugettext
    10 from django.utils.encoding import smart_unicode
     10from django.utils.encoding import StrAndUnicode, smart_unicode
    1111
    1212from util import ErrorList, ValidationError
    13 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
     13from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple
    1414
    1515try:
    1616    from decimal import Decimal, DecimalException
     
    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',
     
    347347    # It's OK if Django settings aren't configured.
    348348    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
    349349
     350class UploadedFile(StrAndUnicode):
     351    "A wrapper for files uploaded in a FileField"
     352    def __init__(self, filename, content):
     353        self.filename = filename
     354        self.content = content
     355       
     356    def __unicode__(self):
     357        """
     358        The unicode representation is the filename, so that the pre-database-insertion
     359        logic can use UploadedFile objects
     360        """
     361        return self.filename
     362
     363class FileField(Field):
     364    widget = FileInput
     365    def __init__(self, *args, **kwargs):
     366        super(FileField, self).__init__(*args, **kwargs)
     367
     368    def clean(self, data):
     369        super(FileField, self).clean(data)
     370        if not self.required and data in EMPTY_VALUES:
     371            return None
     372        try:
     373            f = UploadedFile(data['filename'], data['content'])
     374        except TypeError:
     375            raise ValidationError(gettext(u"No file was submitted. Check the encoding type on the form."))
     376        except KeyError:
     377            raise ValidationError(gettext(u"No file was submitted."))
     378        if not f.content:
     379            raise ValidationError(gettext(u"The submitted file is empty."))
     380        return f
     381
     382class ImageField(FileField):
     383    def clean(self, data):
     384        """
     385        Checks that the file-upload field data contains a valid image (GIF, JPG,
     386        PNG, possibly others -- whatever the Python Imaging Library supports).
     387        """
     388        f = super(ImageField, self).clean(data)
     389        if f is None:
     390            return None
     391        from PIL import Image
     392        from cStringIO import StringIO
     393        try:
     394            Image.open(StringIO(f.content))
     395        except IOError: # Python Imaging Library doesn't recognize it as an image
     396            raise ValidationError(gettext(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."))
     397        return f
     398       
    350399class URLField(RegexField):
    351400    def __init__(self, max_length=None, min_length=None, verify_exists=False,
    352401            validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
  • django/newforms/forms.py

     
    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 {}
     
    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
     
    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
     
    284285        """
    285286        Returns the data for this BoundField, or None if it wasn't given.
    286287        """
    287         return self.field.widget.value_from_datadict(self.form.data, self.html_name)
     288        return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name)
    288289    data = property(_data)
    289290
    290291    def label_tag(self, contents=None, attrs=None):
  • django/newforms/widgets.py

     
    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.
     
    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)
     
    121121class 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
    124131class Textarea(Widget):
    125132    def __init__(self, attrs=None):
    126133        # The 'rows' and 'cols' attributes are required for HTML correctness.
     
    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
     
    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)
     
    356363        return id_
    357364    id_for_label = classmethod(id_for_label)
    358365
    359     def value_from_datadict(self, data, name):
    360         return [widget.value_from_datadict(data, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
     366    def value_from_datadict(self, data, files, name):
     367        return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
    361368
    362369    def format_output(self, rendered_widgets):
    363370        """
  • tests/regressiontests/forms/tests.py

     
    172172
    173173# FileInput Widget ############################################################
    174174
     175FileInput widgets don't ever show the value, because the old value is of no use
     176if you are updating the form or if the provided file generated an error.
    175177>>> w = FileInput()
    176178>>> w.render('email', '')
    177179u'<input type="file" name="email" />'
    178180>>> w.render('email', None)
    179181u'<input type="file" name="email" />'
    180182>>> w.render('email', 'test@example.com')
    181 u'<input type="file" name="email" value="test@example.com" />'
     183u'<input type="file" name="email" />'
    182184>>> w.render('email', 'some "quoted" & ampersanded value')
    183 u'<input type="file" name="email" value="some &quot;quoted&quot; &amp; ampersanded value" />'
     185u'<input type="file" name="email" />'
    184186>>> w.render('email', 'test@example.com', attrs={'class': 'fun'})
    185 u'<input type="file" name="email" value="test@example.com" class="fun" />'
     187u'<input type="file" name="email" class="fun" />'
    186188
    187189You can also pass 'attrs' to the constructor:
    188190>>> w = FileInput(attrs={'class': 'fun'})
    189191>>> w.render('email', '')
    190192u'<input type="file" class="fun" name="email" />'
    191193>>> w.render('email', 'foo@example.com')
    192 u'<input type="file" class="fun" value="foo@example.com" name="email" />'
     194u'<input type="file" class="fun" name="email" />'
    193195
    194196>>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'})
    195 u'<input type="file" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />'
     197u'<input type="file" class="fun" name="email" />'
    196198
    197199# Textarea Widget #############################################################
    198200
     
    15201522...
    15211523ValidationError: [u'Ensure this value has at most 15 characters (it has 20).']
    15221524
     1525# FileField ##################################################################
     1526
     1527>>> f = FileField()
     1528>>> f.clean('')
     1529Traceback (most recent call last):
     1530...
     1531ValidationError: [u'This field is required.']
     1532
     1533>>> f.clean(None)
     1534Traceback (most recent call last):
     1535...
     1536ValidationError: [u'This field is required.']
     1537
     1538>>> f.clean({})
     1539Traceback (most recent call last):
     1540...
     1541ValidationError: [u'No file was submitted.']
     1542
     1543>>> f.clean('some content that is not a file')
     1544Traceback (most recent call last):
     1545...
     1546ValidationError: [u'No file was submitted. Check the encoding type on the form.']
     1547
     1548>>> f.clean({'filename': 'name', 'content':None})
     1549Traceback (most recent call last):
     1550...
     1551ValidationError: [u'The submitted file is empty.']
     1552
     1553>>> f.clean({'filename': 'name', 'content':''})
     1554Traceback (most recent call last):
     1555...
     1556ValidationError: [u'The submitted file is empty.']
     1557
     1558>>> type(f.clean({'filename': 'name', 'content':'Some File Content'}))
     1559<class 'django.newforms.fields.UploadedFile'>
     1560
    15231561# URLField ##################################################################
    15241562
    15251563>>> f = URLField()
     
    25612599the next.
    25622600>>> class MyForm(Form):
    25632601...     def __init__(self, data=None, auto_id=False, field_list=[]):
    2564 ...         Form.__init__(self, data, auto_id)
     2602...         Form.__init__(self, data, auto_id=auto_id)
    25652603...         for field in field_list:
    25662604...             self.fields[field[0]] = field[1]
    25672605>>> field_list = [('field1', CharField()), ('field2', CharField())]
     
    25792617...     default_field_1 = CharField()
    25802618...     default_field_2 = CharField()
    25812619...     def __init__(self, data=None, auto_id=False, field_list=[]):
    2582 ...         Form.__init__(self, data, auto_id)
     2620...         Form.__init__(self, data, auto_id=auto_id)
    25832621...         for field in field_list:
    25842622...             self.fields[field[0]] = field[1]
    25852623>>> field_list = [('field1', CharField()), ('field2', CharField())]
     
    32343272<option value="3" selected="selected">No</option>
    32353273</select>
    32363274
     3275# Forms with FileFields ################################################
     3276
     3277FileFields are a special case because they take their data from the request.FILES,
     3278not request.POST.
     3279
     3280>>> class FileForm(Form):
     3281...     file1 = FileField()
     3282>>> f = FileForm(auto_id=False)
     3283>>> print f
     3284<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>
     3285
     3286>>> f = FileForm(data={}, files={}, auto_id=False)
     3287>>> print f
     3288<tr><th>File1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="file" name="file1" /></td></tr>
     3289
     3290>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':''}}, auto_id=False)
     3291>>> print f
     3292<tr><th>File1:</th><td><ul class="errorlist"><li>The submitted file is empty.</li></ul><input type="file" name="file1" /></td></tr>
     3293
     3294>>> f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False)
     3295>>> print f
     3296<tr><th>File1:</th><td><ul class="errorlist"><li>No file was submitted. Check the encoding type on the form.</li></ul><input type="file" name="file1" /></td></tr>
     3297
     3298>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False)
     3299>>> print f
     3300<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>
     3301>>> f.is_valid()
     3302True
     3303
    32373304# Basic form processing in a view #############################################
    32383305
    32393306>>> from django.template import Template, Context
Back to Top