Ticket #7048: 7048_r13926.diff

File 7048_r13926.diff, 26.8 KB (added by Jannis Leidel, 9 years ago)

UI updates and a few other issues fixed

  • django/contrib/admin/media/css/widgets.css

    diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css
    index 620e082..a761a7b 100644
    a b p.file-upload { 
    198198    margin-left: 5px;
    199199}
    200200
     201span.clearable-file-input label {
     202        color: #333;
     203    font-size: 11px;
     204        display: inline;
     205    float: none;
     206}
     207
    201208/* CALENDARS & CLOCKS */
    202209
    203210.calendarbox, .clockbox {
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index 2c7ac5c..eb7f217 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)
     88class AdminFileWidget(forms.ClearableFileInput):
     89    template_with_initial = (u'<p class="file-upload">%s</p>'
     90                            % forms.ClearableFileInput.template_with_initial)
     91    template_with_clear = (u'<span class="clearable-file-input">%s</span>'
     92                           % forms.ClearableFileInput.template_with_clear)
    9493
    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))
    10294
    10395class ForeignKeyRawIdWidget(forms.TextInput):
    10496    """
  • django/db/models/fields/files.py

    diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
    index 6dfeddb..9ee523b 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 false 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..03455e0 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        return data
     184
    170185    def widget_attrs(self, widget):
    171186        """
    172187        Given a Widget instance (*not* a Widget class), returns a dictionary of
    class EmailField(CharField): 
    434449    default_validators = [validators.validate_email]
    435450
    436451class FileField(Field):
    437     widget = FileInput
     452    widget = ClearableFileInput
    438453    default_error_messages = {
    439454        'invalid': _(u"No file was submitted. Check the encoding type on the form."),
    440455        'missing': _(u"No file was submitted."),
    441456        'empty': _(u"The submitted file is empty."),
    442457        'max_length': _(u'Ensure this filename has at most %(max)d characters (it has %(length)d).'),
     458        'contradiction': _(u'Please either submit a file or check the clear checkbox, not both.')
    443459    }
    444460
    445461    def __init__(self, *args, **kwargs):
    class FileField(Field): 
    468484        return data
    469485
    470486    def clean(self, data, initial=None):
     487        # If the widget got contradictory inputs, we raise a validation error
     488        if data is FILE_INPUT_CONTRADICTION:
     489            raise ValidationError(self.error_messages['contradiction'])
     490        # False means the field value should be cleared; further validation is
     491        # not needed.
     492        if data is False:
     493            if not self.required:
     494                return False
     495            # If the field is required, clearing is not possible (the widget
     496            # shouldn't return False data in that case anyway). False is not
     497            # in validators.EMPTY_VALUES; if a False value makes it this far
     498            # it should be validated from here on out as None (so it will be
     499            # caught by the required check).
     500            data = None
    471501        if not data and initial:
    472502            return initial
    473503        return super(FileField, self).clean(data)
    474504
     505    def bound_data(self, data, initial):
     506        if data in (None, FILE_INPUT_CONTRADICTION):
     507            return initial
     508        return data
     509
    475510class ImageField(FileField):
    476511    default_error_messages = {
    477512        '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 13ef1a7..0377de4 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..cb12586 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 %(clear_template)s<br />%(input_text)s: %(input)s'
     298
     299    template_with_clear = u'%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
     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        return name + '-clear'
     307
     308    def clear_checkbox_id(self, name):
     309        """
     310        Given the name of the clear checkbox input, return the HTML id for it.
     311        """
     312        return name + '_id'
     313
     314    def render(self, name, value, attrs=None):
     315        substitutions = {
     316            'initial_text': self.initial_text,
     317            'input_text': self.input_text,
     318            'clear_template': '',
     319            'clear_checkbox_label': self.clear_checkbox_label,
     320        }
     321        template = u'%(input)s'
     322        substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs)
     323
     324        if value and hasattr(value, "url"):
     325            template = self.template_with_initial
     326            substitutions['initial'] = (u'<a target="_blank" href="%s">%s</a>'
     327                                        % (value.url, value))
     328            if not self.is_required:
     329                checkbox_name = self.clear_checkbox_name(name)
     330                checkbox_id = self.clear_checkbox_id(checkbox_name)
     331                substitutions['clear_checkbox_name'] = checkbox_name
     332                substitutions['clear_checkbox_id'] = checkbox_id
     333                substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id})
     334                substitutions['clear_template'] = self.template_with_clear % substitutions
     335
     336        return mark_safe(template % substitutions)
     337
     338    def value_from_datadict(self, data, files, name):
     339        upload = super(ClearableFileInput, self).value_from_datadict(data, files, name)
     340        if not self.is_required and CheckboxInput().value_from_datadict(
     341            data, files, self.clear_checkbox_name(name)):
     342            if upload:
     343                # If the user contradicts themselves (uploads a new file AND
     344                # checks the "clear" checkbox), we return a unique marker
     345                # object that FileField will turn into a ValidationError.
     346                return FILE_INPUT_CONTRADICTION
     347            # False signals to clear any existing value, as opposed to just None
     348            return False
     349        return upload
     350
    289351class Textarea(Widget):
    290352    def __init__(self, attrs=None):
    291353        # 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..c01a736 100644
    a b commonly used groups of widgets: 
    4646
    4747    File upload input: ``<input type='file' ...>``
    4848
     49.. class:: ClearableFileInput
     50
     51    .. versionadded:: 1.3
     52
     53    File upload input: ``<input type='file' ...>``, with an additional checkbox
     54    input to clear the field's value, if the field is not required and has
     55    initial data.
     56
    4957.. class:: DateInput
    5058
    5159    .. 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..20c0df6 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<p class="file-upload">Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <span class="clearable-file-input"><input type="checkbox" name="test-clear" id="test-clear_id" /> <label for="test-clear_id">Clear</label></span><br />Change: <input type="file" name="test" /></p>
    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..10483a7 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> <input type="checkbox" name="myfile-clear" id="myfile-clear_id" /> <label for="myfile-clear_id">Clear</label><br />Change: <input type="file" name="myfile" />')
     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..397651a 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