Ticket #7048: 7048_r13753.diff

File 7048_r13753.diff, 26.8 KB (added by Carl Meyer, 14 years ago)
  • django/contrib/admin/media/css/forms.css

    diff --git a/django/contrib/admin/media/css/forms.css b/django/contrib/admin/media/css/forms.css
    index 35d0ed7..a95602c 100644
    a b fieldset.collapsed .collapse-toggle {  
    150150    display: inline !important;
    151151}
    152152
     153/* CLEARABLE FILE INPUTS */
     154
     155.clearable-file-input {
     156    display: block;
     157    padding-left: 9.818181em; /* should match the 9em that inputs get pushed to the side by labels */
     158}
     159
     160.clearable-file-input label {
     161    display: inline;
     162    float: none;
     163    color: #333;
     164    font-size: 11px;
     165}
     166
    153167/* MONOSPACE TEXTAREAS */
    154168
    155169fieldset.monospace textarea {
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index 2c7ac5c..dc63801 100644
    a b class AdminRadioFieldRenderer(RadioFieldRenderer):  
    8585class AdminRadioSelect(forms.RadioSelect):
    8686    renderer = AdminRadioFieldRenderer
    8787
    88 class AdminFileWidget(forms.FileInput):
    89     """
    90     A FileField Widget that shows its current value if it has one.
    91     """
    92     def __init__(self, attrs={}):
    93         super(AdminFileWidget, self).__init__(attrs)
    94 
    95     def render(self, name, value, attrs=None):
    96         output = []
    97         if value and hasattr(value, "url"):
    98             output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \
    99                 (_('Currently:'), value.url, value, _('Change:')))
    100         output.append(super(AdminFileWidget, self).render(name, value, attrs))
    101         return mark_safe(u''.join(output))
     88class AdminFileWidget(forms.ClearableFileInput):
     89    template_with_clear = (u'<span class="clearable-file-input">%s</span>'
     90                           % forms.ClearableFileInput.template_with_clear)
    10291
    10392class ForeignKeyRawIdWidget(forms.TextInput):
    10493    """
  • django/db/models/fields/files.py

    diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
    index 6dfeddb..b97b9e2 100644
    a b class FileField(Field):  
    282282        return os.path.join(self.get_directory_name(), self.get_filename(filename))
    283283
    284284    def save_form_data(self, instance, data):
    285         if data:
     285        # Important: None means "no change", other falsy value means "clear"
     286        # This subtle distinction (rather than a more explicit marker) is
     287        # needed because we need to consume values that are also sane for a
     288        # regular (non Model-) Form to find in its cleaned_data dictionary.
     289        if data is not None:
     290            # This value will be converted to unicode and stored in the
     291            # database, so leaving False as-is is not acceptable.
     292            if not data:
     293                data = ''
    286294            setattr(instance, self.name, data)
    287295
    288296    def formfield(self, **kwargs):
  • django/forms/fields.py

    diff --git a/django/forms/fields.py b/django/forms/fields.py
    index de14a5c..fcfecab 100644
    a b from django.core.validators import EMPTY_VALUES  
    2727
    2828from util import ErrorList
    2929from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \
    30         FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \
    31         DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
     30        ClearableFileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \
     31        DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget, \
     32        FILE_INPUT_CONTRADICTION
    3233
    3334__all__ = (
    3435    'Field', 'CharField', 'IntegerField',
    class Field(object):  
    108109        if self.localize:
    109110            widget.is_localized = True
    110111
     112        # Let the widget know whether it should display as required.
     113        widget.is_required = self.required
     114
    111115        # Hook into self.widget_attrs() for any Field-specific HTML attributes.
    112116        extra_attrs = self.widget_attrs(widget)
    113117        if extra_attrs:
    class Field(object):  
    167171        self.run_validators(value)
    168172        return value
    169173
     174    def bound_data(self, data, initial):
     175        """
     176        Return the value that should be shown for this field on render of a
     177        bound form, given the submitted POST data for the field and the initial
     178        data, if any.
     179
     180        For most fields, this will simply be data; FileFields need to handle it
     181        a bit differently.
     182
     183        """
     184        return data
     185
    170186    def widget_attrs(self, widget):
    171187        """
    172188        Given a Widget instance (*not* a Widget class), returns a dictionary of
    class EmailField(CharField):  
    434450    default_validators = [validators.validate_email]
    435451
    436452class FileField(Field):
    437     widget = FileInput
     453    widget = ClearableFileInput
    438454    default_error_messages = {
    439455        'invalid': _(u"No file was submitted. Check the encoding type on the form."),
    440456        'missing': _(u"No file was submitted."),
    441457        'empty': _(u"The submitted file is empty."),
    442458        'max_length': _(u'Ensure this filename has at most %(max)d characters (it has %(length)d).'),
     459        'contradiction': _(u'Please either submit a file or check the clear checkbox, not both.')
    443460    }
    444461
    445462    def __init__(self, *args, **kwargs):
    class FileField(Field):  
    468485        return data
    469486
    470487    def clean(self, data, initial=None):
     488        # If the widget got contradictory inputs, we raise a validation error
     489        if data is FILE_INPUT_CONTRADICTION:
     490            raise ValidationError(self.error_messages['contradiction'])
     491        # False means the field value should be cleared; further validation is
     492        # not needed.
     493        if data is False:
     494            if not self.required:
     495                return False
     496            # If the field is required, clearing is not possible (the widget
     497            # shouldn't return False data in that case anyway).  False is not
     498            # in validators.EMPTY_VALUES; if a False value makes it this far it
     499            # should be validated from here on out as None (so it will be
     500            # caught by the required check).
     501            data = None
    471502        if not data and initial:
    472503            return initial
    473504        return super(FileField, self).clean(data)
    474505
     506    def bound_data(self, data, initial):
     507        if data in (None, FILE_INPUT_CONTRADICTION):
     508            return initial
     509        return data
     510
    475511class ImageField(FileField):
    476512    default_error_messages = {
    477513        'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
  • django/forms/forms.py

    diff --git a/django/forms/forms.py b/django/forms/forms.py
    index 7b2bc66..bd25024 100644
    a b class BoundField(StrAndUnicode):  
    437437            if callable(data):
    438438                data = data()
    439439        else:
    440             if isinstance(self.field, FileField) and self.data is None:
    441                 data = self.form.initial.get(self.name, self.field.initial)
    442             else:
    443                 data = self.data
     440            data = self.field.bound_data(
     441                self.data, self.form.initial.get(self.name, self.field.initial))
    444442        data = self.field.prepare_value(data)
    445443
    446444        if not only_initial:
  • django/forms/widgets.py

    diff --git a/django/forms/widgets.py b/django/forms/widgets.py
    index 2e16c35..ca32d1b 100644
    a b from itertools import chain  
    77from django.conf import settings
    88from django.utils.datastructures import MultiValueDict, MergeDict
    99from django.utils.html import escape, conditional_escape
    10 from django.utils.translation import ugettext
     10from django.utils.translation import ugettext, ugettext_lazy
    1111from django.utils.encoding import StrAndUnicode, force_unicode
    1212from django.utils.safestring import mark_safe
    1313from django.utils import datetime_safe, formats
    from urlparse import urljoin  
    1818
    1919__all__ = (
    2020    'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
    21     'HiddenInput', 'MultipleHiddenInput',
     21    'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
    2222    'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
    2323    'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
    2424    'CheckboxSelectMultiple', 'MultiWidget',
    class Widget(object):  
    134134    is_hidden = False          # Determines whether this corresponds to an <input type="hidden">.
    135135    needs_multipart_form = False # Determines does this widget need multipart-encrypted form
    136136    is_localized = False
     137    is_required = False
    137138
    138139    def __init__(self, attrs=None):
    139140        if attrs is not None:
    class FileInput(Input):  
    286287            return False
    287288        return True
    288289
     290FILE_INPUT_CONTRADICTION = object()
     291
     292class ClearableFileInput(FileInput):
     293    initial_text = ugettext_lazy('Currently')
     294    input_text = ugettext_lazy('Change')
     295    clear_checkbox_label = ugettext_lazy('Clear')
     296
     297    template_with_initial = u'%(initial_text)s: %(initial)s <br />%(input_text)s: %(input)s'
     298
     299    template_with_clear = template_with_initial + u'<br /><label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s:</label> %(clear)s'
     300
     301    def clear_checkbox_name(self, name):
     302        """
     303        Given the name of the file input, return the name of the clear checkbox
     304        input.
     305
     306        """
     307        return name + '_clear'
     308
     309    def clear_checkbox_id(self, name):
     310        """
     311        Given the name of the clear checkbox input, return the HTML id for it.
     312
     313        """
     314        return name + '_id'
     315
     316    def render(self, name, value, attrs=None):
     317        substitutions = {'initial_text': self.initial_text,
     318                         'input_text': self.input_text,
     319                         'clear_checkbox_label': self.clear_checkbox_label}
     320
     321        template = u'%(input)s'
     322        substitutions['input'] = super(ClearableFileInput, self).render(
     323            name, value, attrs)
     324
     325        if value and hasattr(value, "url"):
     326            template = self.template_with_initial
     327            substitutions['initial'] = (u'<a target="_blank" href="%s">%s</a>'
     328                                        % (value.url, value))
     329            if not self.is_required:
     330                template = self.template_with_clear
     331                checkbox_name = self.clear_checkbox_name(name)
     332                checkbox_id = self.clear_checkbox_id(checkbox_name)
     333                substitutions['clear_checkbox_name'] = checkbox_name
     334                substitutions['clear_checkbox_id'] = checkbox_id
     335                substitutions['clear'] = CheckboxInput().render(
     336                    checkbox_name, False, attrs={'id': checkbox_id})
     337
     338        return mark_safe(template % substitutions)
     339
     340    def value_from_datadict(self, data, files, name):
     341        upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
     342        if not self.is_required and CheckboxInput().value_from_datadict(
     343            data, files, self.clear_checkbox_name(name)):
     344            if upload:
     345                # If the user contradicts themselves (uploads a new file AND
     346                # checks the "clear" checkbox), we return a unique marker
     347                # object that FileField will turn into a ValidationError.
     348                return FILE_INPUT_CONTRADICTION
     349            # False signals to clear any existing value, as opposed to just None
     350            return False
     351        return upload
     352
    289353class Textarea(Widget):
    290354    def __init__(self, attrs=None):
    291355        # The 'rows' and 'cols' attributes are required for HTML correctness.
  • docs/ref/forms/fields.txt

    diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
    index 0dd9095..d2b1c30 100644
    a b given length.  
    507507
    508508.. class:: FileField(**kwargs)
    509509
    510     * Default widget: ``FileInput``
     510    * Default widget: ``ClearableFileInput``
    511511    * Empty value: ``None``
    512512    * Normalizes to: An ``UploadedFile`` object that wraps the file content
    513513      and file name into a single object.
    These control the range of values permitted in the field.  
    573573
    574574.. class:: ImageField(**kwargs)
    575575
    576     * Default widget: ``FileInput``
     576    * Default widget: ``ClearableFileInput``
    577577    * Empty value: ``None``
    578578    * Normalizes to: An ``UploadedFile`` object that wraps the file content
    579579      and file name into a single object.
  • docs/ref/forms/widgets.txt

    diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
    index 05215d4..3135dd5 100644
    a b commonly used groups of widgets:  
    4646
    4747    File upload input: ``<input type='file' ...>``
    4848
     49.. class:: ClearableFileInput
     50
     51    File upload input: ``<input type='file' ...>``, with an additional checkbox
     52    input to clear the field's value, if the field is not required and has
     53    initial data.
     54
    4955.. class:: DateInput
    5056
    5157    .. versionadded:: 1.1
  • docs/releases/1.3.txt

    diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt
    index 51de6fd..aaee5d8 100644
    a b custom widget to your form that sets the ``render_value`` argument::  
    4242        username = forms.CharField(max_length=100)
    4343        password = forms.PasswordField(widget=forms.PasswordInput(render_value=True))
    4444
     45Clearable default widget for FileField
     46~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     47
     48Django 1.3 now includes a ``ClearableFileInput`` form widget in addition to
     49``FileInput``. ``ClearableFileInput`` renders with a checkbox to clear the
     50field's value (if the field has a value and is not required); ``FileInput``
     51provided no means for clearing an existing file from a ``FileField``.
     52
     53``ClearableFileInput`` is now the default widget for a ``FileField``, so
     54existing forms including ``FileField`` without assigning a custom widget will
     55need to account for the possible extra checkbox in the rendered form output.
     56
     57To return to the previous rendering (without the ability to clear the
     58``FileField``), use the ``FileInput`` widget in place of
     59``ClearableFileInput``. For instance, in a ``ModelForm`` for a hypothetical
     60``Document`` model with a ``FileField`` named ``document``::
     61
     62    from django import forms
     63    from myapp.models import Document
     64
     65    class DocumentForm(forms.ModelForm):
     66        class Meta:
     67            model = Document
     68            widgets = {'document': forms.FileInput}
     69
    4570.. _deprecated-features-1.3:
    4671
    4772Features deprecated in 1.3
  • tests/regressiontests/admin_widgets/models.py

    diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
    index 59d625b..4a93ee1 100644
    a b HTML escaped.  
    116116
    117117>>> w = AdminFileWidget()
    118118>>> print conditional_escape(w.render('test', album.cover_art))
    119 Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <br />Change: <input type="file" name="test" />
     119<span class="clearable-file-input">Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <br />Change: <input type="file" name="test" /><br /><label for="test_clear_id">Clear:</label> <input type="checkbox" name="test_clear" id="test_clear_id" /></span>
    120120>>> print conditional_escape(w.render('test', SimpleUploadedFile('test', 'content')))
    121121<input type="file" name="test" />
    122122
  • tests/regressiontests/forms/fields.py

    diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py
    index e4f2c26..d29e2e8 100644
    a b class FieldsTests(TestCase):  
    5757        except error, e:
    5858            self.assertEqual(message, str(e))
    5959
     60    def test_field_sets_widget_is_required(self):
     61        self.assertEqual(Field(required=True).widget.is_required, True)
     62        self.assertEqual(Field(required=False).widget.is_required, False)
     63
    6064    # CharField ###################################################################
    6165
    6266    def test_charfield_0(self):
  • tests/regressiontests/forms/tests.py

    diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
    index 7a91cb7..1871d4f 100644
    a b from media import media_tests  
    3939
    4040from fields import FieldsTests
    4141from validators import TestFieldWithValidators
    42 from widgets import WidgetTests
     42from widgets import WidgetTests, ClearableFileInputTests
    4343
    4444from input_formats import *
    4545
  • tests/regressiontests/forms/widgets.py

    diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py
    index 59b33d5..b305263 100644
    a b u'<input type="hidden" name="date_0" value="17.09.2007" /><input type="hidden" n  
    12691269from django.utils import copycompat as copy
    12701270from unittest import TestCase
    12711271from django import forms
     1272from django.core.files.uploadedfile import SimpleUploadedFile
    12721273
    12731274
    12741275class SelectAndTextWidget(forms.MultiWidget):
    class WidgetTests(TestCase):  
    13231324        self.assertFalse(form.is_valid())
    13241325        form = SplitDateRequiredForm({'field': ['', '']})
    13251326        self.assertFalse(form.is_valid())
     1327
     1328
     1329class FakeFieldFile(object):
     1330    """
     1331    Quacks like a FieldFile (has a .url and unicode representation), but
     1332    doesn't require us to care about storages etc.
     1333
     1334    """
     1335    url = 'something'
     1336
     1337    def __unicode__(self):
     1338        return self.url
     1339
     1340class ClearableFileInputTests(TestCase):
     1341    def test_clear_input_renders(self):
     1342        """
     1343        A ClearableFileInput with is_required False and rendered with
     1344        an initial value that is a file renders a clear checkbox.
     1345
     1346        """
     1347        widget = forms.ClearableFileInput()
     1348        widget.is_required = False
     1349        self.assertEqual(widget.render('myfile', FakeFieldFile()),
     1350                         u'Currently: <a target="_blank" href="something">something</a> <br />Change: <input type="file" name="myfile" /><br /><label for="myfile_clear_id">Clear:</label> <input type="checkbox" name="myfile_clear" id="myfile_clear_id" />')
     1351
     1352    def test_clear_input_renders_only_if_not_required(self):
     1353        """
     1354        A ClearableFileInput with is_required=False does not render a clear
     1355        checkbox.
     1356
     1357        """
     1358        widget = forms.ClearableFileInput()
     1359        widget.is_required = True
     1360        self.assertEqual(widget.render('myfile', FakeFieldFile()),
     1361                         u'Currently: <a target="_blank" href="something">something</a> <br />Change: <input type="file" name="myfile" />')
     1362
     1363    def test_clear_input_renders_only_if_initial(self):
     1364        """
     1365        A ClearableFileInput instantiated with no initial value does not render
     1366        a clear checkbox.
     1367
     1368        """
     1369        widget = forms.ClearableFileInput()
     1370        widget.is_required = False
     1371        self.assertEqual(widget.render('myfile', None),
     1372                         u'<input type="file" name="myfile" />')
     1373
     1374    def test_clear_input_checked_returns_false(self):
     1375        """
     1376        ClearableFileInput.value_from_datadict returns False if the clear
     1377        checkbox is checked, if not required.
     1378
     1379        """
     1380        widget = forms.ClearableFileInput()
     1381        widget.is_required = False
     1382        self.assertEqual(widget.value_from_datadict(
     1383                data={'myfile_clear': True},
     1384                files={},
     1385                name='myfile'), False)
     1386
     1387    def test_clear_input_checked_returns_false_only_if_not_required(self):
     1388        """
     1389        ClearableFileInput.value_from_datadict never returns False if the field
     1390        is required.
     1391
     1392        """
     1393        widget = forms.ClearableFileInput()
     1394        widget.is_required = True
     1395        f = SimpleUploadedFile('something.txt', 'content')
     1396        self.assertEqual(widget.value_from_datadict(
     1397                data={'myfile_clear': True},
     1398                files={'myfile': f},
     1399                name='myfile'), f)
  • tests/regressiontests/model_fields/models.py

    diff --git a/tests/regressiontests/model_fields/models.py b/tests/regressiontests/model_fields/models.py
    index 45cd223..1dc1649 100644
    a b class BooleanModel(models.Model):  
    6767    string = models.CharField(max_length=10, default='abc')
    6868
    6969###############################################################################
     70# FileField
     71
     72class Document(models.Model):
     73    myfile = models.FileField(upload_to='unused')
     74
     75###############################################################################
    7076# ImageField
    7177
    7278# If PIL available, do these tests.
  • tests/regressiontests/model_fields/tests.py

    diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py
    index 72a7d4d..58c32b5 100644
    a b import django.test  
    66from django import forms
    77from django.db import models
    88from django.core.exceptions import ValidationError
     9from django.db.models.fields.files import FieldFile
    910
    10 from models import Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, NullBooleanModel, BooleanModel
     11from models import Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, NullBooleanModel, BooleanModel, Document
    1112
    1213# If PIL available, do these tests.
    1314if Image:
    class TypeCoercionTests(django.test.TestCase):  
    311312    def test_lookup_integer_in_textfield(self):
    312313        self.assertEquals(Post.objects.filter(body=24).count(), 0)
    313314
     315class FileFieldTests(unittest.TestCase):
     316    def test_clearable(self):
     317        """
     318        Test that FileField.save_form_data will clear its instance attribute
     319        value if passed False.
     320
     321        """
     322        d = Document(myfile='something.txt')
     323        self.assertEqual(d.myfile, 'something.txt')
     324        field = d._meta.get_field('myfile')
     325        field.save_form_data(d, False)
     326        self.assertEqual(d.myfile, '')
     327
     328    def test_unchanged(self):
     329        """
     330        Test that FileField.save_form_data considers None to mean "no change"
     331        rather than "clear".
     332
     333        """
     334        d = Document(myfile='something.txt')
     335        self.assertEqual(d.myfile, 'something.txt')
     336        field = d._meta.get_field('myfile')
     337        field.save_form_data(d, None)
     338        self.assertEqual(d.myfile, 'something.txt')
     339
     340    def test_changed(self):
     341        """
     342        Test that FileField.save_form_data, if passed a truthy value, updates
     343        its instance attribute.
     344
     345        """
     346        d = Document(myfile='something.txt')
     347        self.assertEqual(d.myfile, 'something.txt')
     348        field = d._meta.get_field('myfile')
     349        field.save_form_data(d, 'else.txt')
     350        self.assertEqual(d.myfile, 'else.txt')
  • tests/regressiontests/model_forms_regress/models.py

    diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py
    index 4f9811a..75b8a40 100644
    a b class Author1(models.Model):  
    5757
    5858class Homepage(models.Model):
    5959    url = models.URLField(verify_exists=False)
     60
     61class Document(models.Model):
     62    myfile = models.FileField(upload_to='unused', blank=True)
  • tests/regressiontests/model_forms_regress/tests.py

    diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py
    index baf769c..2da240e 100644
    a b  
     1import unittest
    12from datetime import date
    23
    34from django import db
    from django import forms  
    56from django.forms.models import modelform_factory, ModelChoiceField
    67from django.conf import settings
    78from django.test import TestCase
    8 from django.core.exceptions import FieldError
     9from django.core.exceptions import FieldError, ValidationError
     10from django.core.files.uploadedfile import SimpleUploadedFile
    911
    1012from models import Person, RealPerson, Triple, FilePathModel, Article, \
    11     Publication, CustomFF, Author, Author1, Homepage
     13    Publication, CustomFF, Author, Author1, Homepage, Document
    1214
    1315
    1416class ModelMultipleChoiceFieldTests(TestCase):
    class InvalidFieldAndFactory(TestCase):  
    333335        self.assertRaises(FieldError, modelform_factory,
    334336                          Person, fields=['no-field', 'name'])
    335337
     338
     339class DocumentForm(forms.ModelForm):
     340    class Meta:
     341        model = Document
     342
     343class FileFieldTests(unittest.TestCase):
     344    def test_clean_false(self):
     345        """
     346        If the ``clean`` method on a non-required FileField receives False as
     347        the data (meaning clear the field value), it returns False, regardless
     348        of the value of ``initial``.
     349
     350        """
     351        f = forms.FileField(required=False)
     352        self.assertEqual(f.clean(False), False)
     353        self.assertEqual(f.clean(False, 'initial'), False)
     354
     355    def test_clean_false_required(self):
     356        """
     357        If the ``clean`` method on a required FileField receives False as the
     358        data, it has the same effect as None: initial is returned if non-empty,
     359        otherwise the validation catches the lack of a required value.
     360
     361        """
     362        f = forms.FileField(required=True)
     363        self.assertEqual(f.clean(False, 'initial'), 'initial')
     364        self.assertRaises(ValidationError, f.clean, False)
     365
     366    def test_full_clear(self):
     367        """
     368        Integration happy-path test that a model FileField can actually be set
     369        and cleared via a ModelForm.
     370
     371        """
     372        form = DocumentForm()
     373        self.assert_('name="myfile"' in unicode(form))
     374        self.assert_('myfile_clear' not in unicode(form))
     375        form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', 'content')})
     376        self.assert_(form.is_valid())
     377        doc = form.save(commit=False)
     378        self.assertEqual(doc.myfile.name, 'something.txt')
     379        form = DocumentForm(instance=doc)
     380        self.assert_('myfile_clear' in unicode(form))
     381        form = DocumentForm(instance=doc, data={'myfile_clear': 'true'})
     382        doc = form.save(commit=False)
     383        self.assertEqual(bool(doc.myfile), False)
     384
     385    def test_clear_and_file_contradiction(self):
     386        """
     387        If the user submits a new file upload AND checks the clear checkbox,
     388        they get a validation error, and the bound redisplay of the form still
     389        includes the current file and the clear checkbox.
     390
     391        """
     392        form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', 'content')})
     393        self.assert_(form.is_valid())
     394        doc = form.save(commit=False)
     395        form = DocumentForm(instance=doc,
     396                            files={'myfile': SimpleUploadedFile('something.txt', 'content')},
     397                            data={'myfile_clear': 'true'})
     398        self.assert_(not form.is_valid())
     399        self.assertEqual(form.errors['myfile'],
     400                         [u'Please either submit a file or check the clear checkbox, not both.'])
     401        rendered = unicode(form)
     402        self.assert_('something.txt' in rendered)
     403        self.assert_('myfile_clear' in rendered)
Back to Top