[1.10.x] Fixed #27186 -- Fixed model form default fallback for MultiWidget, FileInput, SplitDateTimeWidget, SelectDateWidget, and SplitArrayWidget.
Thanks Matt Westcott for the review.
Backport of 3507d4e773
from master
This commit is contained in:
parent
190cd0e49f
commit
0b59ea3343
|
@ -103,6 +103,12 @@ class SplitArrayWidget(forms.Widget):
|
||||||
return [self.widget.value_from_datadict(data, files, '%s_%s' % (name, index))
|
return [self.widget.value_from_datadict(data, files, '%s_%s' % (name, index))
|
||||||
for index in range(self.size)]
|
for index in range(self.size)]
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
return all(
|
||||||
|
self.widget.value_omitted_from_data(data, files, '%s_%s' % (name, index))
|
||||||
|
for index in range(self.size)
|
||||||
|
)
|
||||||
|
|
||||||
def id_for_label(self, id_):
|
def id_for_label(self, id_):
|
||||||
# See the comment for RadioSelect.id_for_label()
|
# See the comment for RadioSelect.id_for_label()
|
||||||
if id_:
|
if id_:
|
||||||
|
|
|
@ -54,8 +54,8 @@ def construct_instance(form, instance, fields=None, exclude=None):
|
||||||
continue
|
continue
|
||||||
# Leave defaults for fields that aren't in POST data, except for
|
# Leave defaults for fields that aren't in POST data, except for
|
||||||
# checkbox inputs because they don't appear in POST data if not checked.
|
# checkbox inputs because they don't appear in POST data if not checked.
|
||||||
if (f.has_default() and form.add_prefix(f.name) not in form.data and
|
if (f.has_default() and
|
||||||
not getattr(form[f.name].field.widget, 'dont_use_model_field_default_for_empty_data', False)):
|
form[f.name].field.widget.value_omitted_from_data(form.data, form.files, form.add_prefix(f.name))):
|
||||||
continue
|
continue
|
||||||
# Defer saving file-type fields until after the other fields, so a
|
# Defer saving file-type fields until after the other fields, so a
|
||||||
# callable upload_to can use the values from other fields.
|
# callable upload_to can use the values from other fields.
|
||||||
|
|
|
@ -237,6 +237,9 @@ class Widget(six.with_metaclass(RenameWidgetMethods)):
|
||||||
"""
|
"""
|
||||||
return data.get(name)
|
return data.get(name)
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
return name not in data
|
||||||
|
|
||||||
def id_for_label(self, id_):
|
def id_for_label(self, id_):
|
||||||
"""
|
"""
|
||||||
Returns the HTML ID attribute of this Widget for use by a <label>,
|
Returns the HTML ID attribute of this Widget for use by a <label>,
|
||||||
|
@ -350,6 +353,9 @@ class FileInput(Input):
|
||||||
"File widgets take data from FILES, not POST"
|
"File widgets take data from FILES, not POST"
|
||||||
return files.get(name)
|
return files.get(name)
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
return name not in files
|
||||||
|
|
||||||
|
|
||||||
FILE_INPUT_CONTRADICTION = object()
|
FILE_INPUT_CONTRADICTION = object()
|
||||||
|
|
||||||
|
@ -480,10 +486,6 @@ def boolean_check(v):
|
||||||
|
|
||||||
|
|
||||||
class CheckboxInput(Widget):
|
class CheckboxInput(Widget):
|
||||||
# Don't use model field defaults for fields that aren't in POST data,
|
|
||||||
# because checkboxes don't appear in POST data if not checked.
|
|
||||||
dont_use_model_field_default_for_empty_data = True
|
|
||||||
|
|
||||||
def __init__(self, attrs=None, check_test=None):
|
def __init__(self, attrs=None, check_test=None):
|
||||||
super(CheckboxInput, self).__init__(attrs)
|
super(CheckboxInput, self).__init__(attrs)
|
||||||
# check_test is a callable that takes a value and returns True
|
# check_test is a callable that takes a value and returns True
|
||||||
|
@ -511,6 +513,11 @@ class CheckboxInput(Widget):
|
||||||
value = values.get(value.lower(), value)
|
value = values.get(value.lower(), value)
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
# HTML checkboxes don't appear in POST data if not checked, so it's
|
||||||
|
# never known if the value is actually omitted.
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Select(Widget):
|
class Select(Widget):
|
||||||
allow_multiple_selected = False
|
allow_multiple_selected = False
|
||||||
|
@ -871,6 +878,12 @@ class MultiWidget(Widget):
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
|
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
return all(
|
||||||
|
widget.value_omitted_from_data(data, files, name + '_%s' % i)
|
||||||
|
for i, widget in enumerate(self.widgets)
|
||||||
|
)
|
||||||
|
|
||||||
def format_output(self, rendered_widgets):
|
def format_output(self, rendered_widgets):
|
||||||
"""
|
"""
|
||||||
Given a list of rendered widgets (as strings), returns a Unicode string
|
Given a list of rendered widgets (as strings), returns a Unicode string
|
||||||
|
@ -1056,6 +1069,12 @@ class SelectDateWidget(Widget):
|
||||||
return '%s-%s-%s' % (y, m, d)
|
return '%s-%s-%s' % (y, m, d)
|
||||||
return data.get(name)
|
return data.get(name)
|
||||||
|
|
||||||
|
def value_omitted_from_data(self, data, files, name):
|
||||||
|
return not any(
|
||||||
|
('{}_{}'.format(name, interval) in data)
|
||||||
|
for interval in ('year', 'month', 'day')
|
||||||
|
)
|
||||||
|
|
||||||
def create_select(self, name, field, value, val, choices, none_value):
|
def create_select(self, name, field, value, val, choices, none_value):
|
||||||
if 'id' in self.attrs:
|
if 'id' in self.attrs:
|
||||||
id_ = self.attrs['id']
|
id_ = self.attrs['id']
|
||||||
|
|
|
@ -271,6 +271,21 @@ foundation for custom widgets.
|
||||||
customize it and add expensive processing, you should implement some
|
customize it and add expensive processing, you should implement some
|
||||||
caching mechanism yourself.
|
caching mechanism yourself.
|
||||||
|
|
||||||
|
.. method:: value_omitted_from_data(data, files, name)
|
||||||
|
|
||||||
|
.. versionadded:: 1.10.2
|
||||||
|
|
||||||
|
Given ``data`` and ``files`` dictionaries and this widget's name,
|
||||||
|
returns whether or not there's data or files for the widget.
|
||||||
|
|
||||||
|
The method's result affects whether or not a field in a model form
|
||||||
|
:ref:`falls back to its default <topics-modelform-save>`.
|
||||||
|
|
||||||
|
A special case is :class:`~django.forms.CheckboxInput`, which always
|
||||||
|
returns ``False`` because an unchecked checkbox doesn't appear in the
|
||||||
|
data of an HTML form submission, so it's unknown whether or not the
|
||||||
|
user actually submitted a value.
|
||||||
|
|
||||||
``MultiWidget``
|
``MultiWidget``
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
@ -17,3 +17,8 @@ Bugfixes
|
||||||
|
|
||||||
* Disabled system check for URL patterns beginning with a '/' when
|
* Disabled system check for URL patterns beginning with a '/' when
|
||||||
``APPEND_SLASH=False`` (:ticket:`27238`).
|
``APPEND_SLASH=False`` (:ticket:`27238`).
|
||||||
|
|
||||||
|
* Fixed model form ``default`` fallback for ``MultiWidget``, ``FileInput``,
|
||||||
|
``SplitDateTimeWidget``, ``SelectDateWidget``, and ``SplitArrayWidget``
|
||||||
|
(:ticket:`27186`). Custom widgets affected by this issue may need to
|
||||||
|
implement a :meth:`~django.forms.Widget.value_omitted_from_data` method.
|
||||||
|
|
|
@ -299,6 +299,8 @@ to the ``error_messages`` dictionary of the ``ModelForm``’s inner ``Meta`` cla
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. _topics-modelform-save:
|
||||||
|
|
||||||
The ``save()`` method
|
The ``save()`` method
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -333,11 +335,11 @@ doesn't validate -- i.e., if ``form.errors`` evaluates to ``True``.
|
||||||
If an optional field doesn't appear in the form's data, the resulting model
|
If an optional field doesn't appear in the form's data, the resulting model
|
||||||
instance uses the model field :attr:`~django.db.models.Field.default`, if
|
instance uses the model field :attr:`~django.db.models.Field.default`, if
|
||||||
there is one, for that field. This behavior doesn't apply to fields that use
|
there is one, for that field. This behavior doesn't apply to fields that use
|
||||||
:class:`~django.forms.CheckboxInput` (or any custom widget with
|
:class:`~django.forms.CheckboxInput` (or any custom widget whose
|
||||||
``dont_use_model_field_default_for_empty_data=True``) since an unchecked
|
:meth:`~django.forms.Widget.value_omitted_from_data` method always returns
|
||||||
checkbox doesn't appear in the data of an HTML form submission. Use a custom
|
``False``) since an unchecked checkbox doesn't appear in the data of an HTML
|
||||||
form field or widget if you're designing an API and want the default fallback
|
form submission. Use a custom form field or widget if you're designing an API
|
||||||
for a ``BooleanField``.
|
and want the default fallback for a :class:`~django.db.models.BooleanField`.
|
||||||
|
|
||||||
.. versionchanged:: 1.10.1
|
.. versionchanged:: 1.10.1
|
||||||
|
|
||||||
|
@ -345,6 +347,10 @@ for a ``BooleanField``.
|
||||||
:class:`~django.forms.CheckboxInput` which means that unchecked checkboxes
|
:class:`~django.forms.CheckboxInput` which means that unchecked checkboxes
|
||||||
receive a value of ``True`` if that's the model field default.
|
receive a value of ``True`` if that's the model field default.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.10.2
|
||||||
|
|
||||||
|
The :meth:`~django.forms.Widget.value_omitted_from_data` method was added.
|
||||||
|
|
||||||
This ``save()`` method accepts an optional ``commit`` keyword argument, which
|
This ``save()`` method accepts an optional ``commit`` keyword argument, which
|
||||||
accepts either ``True`` or ``False``. If you call ``save()`` with
|
accepts either ``True`` or ``False``. If you call ``save()`` with
|
||||||
``commit=False``, then it will return an object that hasn't yet been saved to
|
``commit=False``, then it will return an object that hasn't yet been saved to
|
||||||
|
|
|
@ -85,3 +85,7 @@ class CheckboxInputTest(WidgetTest):
|
||||||
def test_value_from_datadict_string_int(self):
|
def test_value_from_datadict_string_int(self):
|
||||||
value = self.widget.value_from_datadict({'testing': '0'}, {}, 'testing')
|
value = self.widget.value_from_datadict({'testing': '0'}, {}, 'testing')
|
||||||
self.assertIs(value, True)
|
self.assertIs(value, True)
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({'field': 'value'}, {}, 'field'), False)
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), False)
|
||||||
|
|
|
@ -14,3 +14,7 @@ class FileInputTest(WidgetTest):
|
||||||
self.check_html(self.widget, 'email', 'test@example.com', html='<input type="file" name="email" />')
|
self.check_html(self.widget, 'email', 'test@example.com', html='<input type="file" name="email" />')
|
||||||
self.check_html(self.widget, 'email', '', html='<input type="file" name="email" />')
|
self.check_html(self.widget, 'email', '', html='<input type="file" name="email" />')
|
||||||
self.check_html(self.widget, 'email', None, html='<input type="file" name="email" />')
|
self.check_html(self.widget, 'email', None, html='<input type="file" name="email" />')
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({}, {'field': 'value'}, 'field'), False)
|
||||||
|
|
|
@ -118,6 +118,13 @@ class MultiWidgetTest(WidgetTest):
|
||||||
'<input id="bar_1" type="text" class="small" value="lennon" name="name_1" />'
|
'<input id="bar_1" type="text" class="small" value="lennon" name="name_1" />'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
widget = MyMultiWidget(widgets=(TextInput(), TextInput()))
|
||||||
|
self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_0': 'x'}, {}, 'field'), False)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_1': 'y'}, {}, 'field'), False)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_0': 'x', 'field_1': 'y'}, {}, 'field'), False)
|
||||||
|
|
||||||
def test_needs_multipart_true(self):
|
def test_needs_multipart_true(self):
|
||||||
"""
|
"""
|
||||||
needs_multipart_form should be True if any widgets need it.
|
needs_multipart_form should be True if any widgets need it.
|
||||||
|
|
|
@ -477,3 +477,11 @@ class SelectDateWidgetTest(WidgetTest):
|
||||||
w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'),
|
w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'),
|
||||||
'13-08-1899',
|
'13-08-1899',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({'field_month': '12'}, {}, 'field'), False)
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({'field_year': '2000'}, {}, 'field'), False)
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data({'field_day': '1'}, {}, 'field'), False)
|
||||||
|
data = {'field_day': '1', 'field_month': '12', 'field_year': '2000'}
|
||||||
|
self.assertIs(self.widget.value_omitted_from_data(data, {}, 'field'), False)
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.forms import Widget
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetTests(SimpleTestCase):
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
widget = Widget()
|
||||||
|
self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field': 'value'}, {}, 'field'), False)
|
|
@ -120,9 +120,11 @@ class PublicationDefaults(models.Model):
|
||||||
CATEGORY_CHOICES = ((1, 'Games'), (2, 'Comics'), (3, 'Novel'))
|
CATEGORY_CHOICES = ((1, 'Games'), (2, 'Comics'), (3, 'Novel'))
|
||||||
title = models.CharField(max_length=30)
|
title = models.CharField(max_length=30)
|
||||||
date_published = models.DateField(default=datetime.date.today)
|
date_published = models.DateField(default=datetime.date.today)
|
||||||
|
datetime_published = models.DateTimeField(default=datetime.datetime(2000, 1, 1))
|
||||||
mode = models.CharField(max_length=2, choices=MODE_CHOICES, default=default_mode)
|
mode = models.CharField(max_length=2, choices=MODE_CHOICES, default=default_mode)
|
||||||
category = models.IntegerField(choices=CATEGORY_CHOICES, default=default_category)
|
category = models.IntegerField(choices=CATEGORY_CHOICES, default=default_category)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
|
file = models.FileField(default='default.txt')
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
|
|
|
@ -601,6 +601,58 @@ class ModelFormBaseTest(TestCase):
|
||||||
m1 = mf1.save(commit=False)
|
m1 = mf1.save(commit=False)
|
||||||
self.assertEqual(m1.mode, mode)
|
self.assertEqual(m1.mode, mode)
|
||||||
|
|
||||||
|
def test_default_splitdatetime_field(self):
|
||||||
|
class PubForm(forms.ModelForm):
|
||||||
|
datetime_published = forms.SplitDateTimeField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PublicationDefaults
|
||||||
|
fields = ('datetime_published',)
|
||||||
|
|
||||||
|
mf1 = PubForm({})
|
||||||
|
self.assertEqual(mf1.errors, {})
|
||||||
|
m1 = mf1.save(commit=False)
|
||||||
|
self.assertEqual(m1.datetime_published, datetime.datetime(2000, 1, 1))
|
||||||
|
|
||||||
|
mf2 = PubForm({'datetime_published_0': '2010-01-01', 'datetime_published_1': '0:00:00'})
|
||||||
|
self.assertEqual(mf2.errors, {})
|
||||||
|
m2 = mf2.save(commit=False)
|
||||||
|
self.assertEqual(m2.datetime_published, datetime.datetime(2010, 1, 1))
|
||||||
|
|
||||||
|
def test_default_filefield(self):
|
||||||
|
class PubForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = PublicationDefaults
|
||||||
|
fields = ('file',)
|
||||||
|
|
||||||
|
mf1 = PubForm({})
|
||||||
|
self.assertEqual(mf1.errors, {})
|
||||||
|
m1 = mf1.save(commit=False)
|
||||||
|
self.assertEqual(m1.file.name, 'default.txt')
|
||||||
|
|
||||||
|
mf2 = PubForm({}, {'file': SimpleUploadedFile('name', b'foo')})
|
||||||
|
self.assertEqual(mf2.errors, {})
|
||||||
|
m2 = mf2.save(commit=False)
|
||||||
|
self.assertEqual(m2.file.name, 'name')
|
||||||
|
|
||||||
|
def test_selectdatewidget(self):
|
||||||
|
class PubForm(forms.ModelForm):
|
||||||
|
date_published = forms.DateField(required=False, widget=forms.SelectDateWidget)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PublicationDefaults
|
||||||
|
fields = ('date_published',)
|
||||||
|
|
||||||
|
mf1 = PubForm({})
|
||||||
|
self.assertEqual(mf1.errors, {})
|
||||||
|
m1 = mf1.save(commit=False)
|
||||||
|
self.assertEqual(m1.date_published, datetime.date.today())
|
||||||
|
|
||||||
|
mf2 = PubForm({'date_published_year': '2010', 'date_published_month': '1', 'date_published_day': '1'})
|
||||||
|
self.assertEqual(mf2.errors, {})
|
||||||
|
m2 = mf2.save(commit=False)
|
||||||
|
self.assertEqual(m2.date_published, datetime.date(2010, 1, 1))
|
||||||
|
|
||||||
|
|
||||||
class FieldOverridesByFormMetaForm(forms.ModelForm):
|
class FieldOverridesByFormMetaForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -40,7 +40,7 @@ class PostgreSQLModel(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class IntegerArrayModel(PostgreSQLModel):
|
class IntegerArrayModel(PostgreSQLModel):
|
||||||
field = ArrayField(models.IntegerField())
|
field = ArrayField(models.IntegerField(), default=[], blank=True)
|
||||||
|
|
||||||
|
|
||||||
class NullableIntegerArrayModel(PostgreSQLModel):
|
class NullableIntegerArrayModel(PostgreSQLModel):
|
||||||
|
|
|
@ -20,7 +20,9 @@ from .models import (
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField
|
from django.contrib.postgres.forms import (
|
||||||
|
SimpleArrayField, SplitArrayField, SplitArrayWidget,
|
||||||
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -714,3 +716,26 @@ class TestSplitFormField(PostgreSQLTestCase):
|
||||||
'Item 0 in the array did not validate: Ensure this value has at most 2 characters (it has 3).',
|
'Item 0 in the array did not validate: Ensure this value has at most 2 characters (it has 3).',
|
||||||
'Item 2 in the array did not validate: Ensure this value has at most 2 characters (it has 4).',
|
'Item 2 in the array did not validate: Ensure this value has at most 2 characters (it has 4).',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def test_splitarraywidget_value_omitted_from_data(self):
|
||||||
|
class Form(forms.ModelForm):
|
||||||
|
field = SplitArrayField(forms.IntegerField(), required=False, size=2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = IntegerArrayModel
|
||||||
|
fields = ('field',)
|
||||||
|
|
||||||
|
form = Form({'field_0': '1', 'field_1': '2'})
|
||||||
|
self.assertEqual(form.errors, {})
|
||||||
|
obj = form.save(commit=False)
|
||||||
|
self.assertEqual(obj.field, [1, 2])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSplitFormWidget(PostgreSQLTestCase):
|
||||||
|
|
||||||
|
def test_value_omitted_from_data(self):
|
||||||
|
widget = SplitArrayWidget(forms.TextInput(), size=2)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_0': 'value'}, {}, 'field'), False)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_1': 'value'}, {}, 'field'), False)
|
||||||
|
self.assertIs(widget.value_omitted_from_data({'field_0': 'value', 'field_1': 'value'}, {}, 'field'), False)
|
||||||
|
|
Loading…
Reference in New Issue