Django

Code

Changeset 5819

Show
Ignore:
Timestamp:
08/06/07 08:58:56 (1 year ago)
Author:
russellm
Message:

Fixed #3297 -- Implemented FileField? and ImageField? for newforms. Thanks to the many users that contributed to and tested this patch.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/db/models/fields/__init__.py

    r5803 r5819  
    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." 
     
    696699        self.upload_to = upload_to 
    697700        Field.__init__(self, verbose_name, name, **kwargs) 
     701 
     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) 
    698708 
    699709    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): 
     
    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 
    775798class FilePathField(Field): 
    776799    def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs): 
     
    820843                setattr(new_object, self.height_field, getattr(original_object, self.height_field)) 
    821844            new_object.save() 
     845 
     846    def formfield(self, **kwargs): 
     847        defaults = {'form_class': forms.ImageField} 
     848        return super(ImageField, self).formfield(**defaults) 
    822849 
    823850class IntegerField(Field): 
  • django/trunk/django/db/models/fields/related.py

    r5725 r5819  
    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()} 
  • django/trunk/django/newforms/extras/widgets.py

    r5263 r5819  
    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: 
  • django/trunk/django/newforms/fields.py

    r5803 r5819  
    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: 
     
    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', 
     
    349349    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' 
    350350 
     351class 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 
     364class 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 
     383class 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         
    351400class URLField(RegexField): 
    352401    def __init__(self, max_length=None, min_length=None, verify_exists=False, 
  • django/trunk/django/newforms/forms.py

    r5782 r5819  
    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 
     
    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() 
     
    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) 
     
    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 
  • django/trunk/django/newforms/models.py

    r5804 r5819  
    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(): 
     
    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 
  • django/trunk/django/newforms/widgets.py

    r5782 r5819  
    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 
     
    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) 
     
    121121class FileInput(Input): 
    122122    input_type = 'file' 
     123 
     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) 
    123130 
    124131class Textarea(Widget): 
     
    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) 
     
    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) 
     
    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): 
  • django/trunk/docs/newforms.txt

    r5813 r5819  
    711711    </form> 
    712712 
     713Binding uploaded files to a form 
     714-------------------------------- 
     715 
     716Dealing with forms that have ``FileField`` and ``ImageField`` fields 
     717is a little more complicated than a normal form. 
     718 
     719Firstly, 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 
     725Secondly, when you use the form, you need to bind the file data. File 
     726data is handled separately to normal form data, so when your form 
     727contains a ``FileField`` and ``ImageField``, you will need to specify 
     728a second argument when you bind your form. So if we extend our 
     729ContactForm to include an ``ImageField`` called ``mugshot``, we 
     730need 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 
     741In practice, you will usually specify ``request.FILES`` as the source 
     742of file data (just like you use ``request.POST`` as the source of 
     743form data):: 
     744 
     745    # Bound form with an image field, data from the request 
     746    >>> f = ContactFormWithMugshot(request.POST, request.FILES) 
     747 
     748Constructing an unbound form is the same as always -- just omit both 
     749form data *and* file data: 
     750 
     751    # Unbound form with a image field 
     752    >>> f = ContactFormWithMugshot() 
     753 
    713754Subclassing forms 
    714755----------------- 
     
    11001141given 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 
     1152An ``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 
     1162The string representation of an ``UploadedFile`` is the same as the filename 
     1163attribute. 
     1164 
     1165When 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 
     1180Using an ImageField requires that the `Python Imaging Library`_ is installed. 
     1181 
     1182When 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~~~~~~~~~~~~~~~~ 
     
    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`` 
  • django/trunk/tests/regressiontests/forms/tests.py

    r5782 r5819  
    174174# FileInput Widget ############################################################ 
    175175 
     176FileInput widgets don't ever show the value, because the old value is of no use 
     177if you are updating the form or if the provided file generated an error. 
    176178>>> w = FileInput() 
    177179>>> w.render('email', '') 
     
    180182u'<input type="file" name="email" />' 
    181183>>> w.render('email', 'test@example.com') 
    182 u'<input type="file" name="email" value="test@example.com" />' 
     184u'<input type="file" name="email" />' 
    183185>>> w.render('email', 'some "quoted" & ampersanded value') 
    184 u'<input type="file" name="email" value="some &quot;quoted&quot; &amp; ampersanded value" />' 
     186u'<input type="file" name="email" />' 
    185187>>> w.render('email', 'test@example.com', attrs={'class': 'fun'}) 
    186 u'<input type="file" name="email" value="test@example.com" class="fun" />' 
     188u'<input type="file" name="email" class="fun" />' 
    187189 
    188190You can also pass 'attrs' to the constructor: 
     
    191193u'<input type="file" class="fun" name="email" />' 
    192194>>> w.render('email', 'foo@example.com') 
    193 u'<input type="file" class="fun" value="foo@example.com" name="email" />' 
     195u'<input type="file" class="fun" name="email" />' 
    194196 
    195197>>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}) 
    196 u'<input type="file" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />' 
     198u'<input type="file" class="fun" name="email" />' 
    197199 
    198200# Textarea Widget ############################################################# 
     
    15321534... 
    15331535ValidationError: [u'Ensure this value has at most 15 characters (it has 20).'] 
     1536 
     1537# FileField ################################################################## 
     1538 
     1539>>> f = FileField() 
     1540>>> f.clean('') 
     1541Traceback (most recent call last): 
     1542... 
     1543ValidationError: [u'This field is required.'] 
     1544 
     1545>>> f.clean(None) 
     1546Traceback (most recent call last): 
     1547... 
     1548ValidationError: [u'This field is required.'] 
     1549 
     1550>>> f.clean({}) 
     1551Traceback (most recent call last): 
     1552... 
     1553ValidationError: [u'No file was submitted.'] 
     1554 
     1555>>> f.clean('some content that is not a file') 
     1556Traceback (most recent call last): 
     1557... 
     1558ValidationError: [u'No file was submitted. Check the encoding type on the form.'] 
     1559 
     1560>>> f.clean({'filename': 'name', 'content':None}) 
     1561Traceback (most recent call last): 
     1562... 
     1563ValidationError: [u'The submitted file is empty.'] 
     1564 
     1565>>> f.clean({'filename': 'name', 'content':''}) 
     1566Traceback (most recent call last): 
     1567... 
     1568ValidationError: [u'The submitted file is empty.'] 
     1569 
     1570>>> type(f.clean({'filename': 'name', 'content':'Some File Content'})) 
     1571<class 'django.newforms.fields.UploadedFile'> 
    15341572 
    15351573# URLField ################################################################## 
     
    25742612>>> class MyForm(Form): 
    25752613...     def __init__(self, data=None, auto_id=False, field_list=[]): 
    2576 ...         Form.__init__(self, data, auto_id
     2614...         Form.__init__(self, data, auto_id=auto_id
    25772615...         for field in field_list: 
    25782616...             self.fields[field[0]] = field[1] 
     
    25922630...     default_field_2 = CharField() 
    25932631...     def __init__(self, data=None, auto_id=False, field_list=[]): 
    2594 ...         Form.__init__(self, data, auto_id
     2632...         Form.__init__(self, data, auto_id=auto_id
    25952633...         for field in field_list: 
    25962634...             self.fields[field[0]] = field[1] 
     
    32473285</select> 
    32483286 
     3287# Forms with FileFields ################################################ 
     3288 
     3289FileFields are a special case because they take their data from the request.FILES, 
     3290not request.POST.  
     3291 
     3292>>> class FileForm(Form): 
     3293...     file1 = FileField() 
     3294>>> f = FileForm(auto_id=False) 
     3295>>> print f 
     3296<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr> 
     3297 
     3298>>> f = FileForm(data={}, files={}, auto_id=False) 
     3299>>> print f 
     3300<tr><th>File1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="file" name="file1" /></td></tr> 
     3301 
     3302>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':''}}, auto_id=False) 
     3303>>> print f 
     3304<tr><th>File1:</th><td><ul class="errorlist"><li>The submitted file is empty.</li></ul><input type="file" name="file1" /></td></tr> 
     3305 
     3306>>> f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False) 
     3307>>> print f 
     3308<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> 
     3309 
     3310>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False) 
     3311>>> print f 
     3312<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr> 
     3313>>> f.is_valid() 
     3314True 
     3315 
    32493316# Basic form processing in a view ############################################# 
    32503317