diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 36485e5011..2e58d449c4 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -714,6 +714,11 @@ class SelectMultiple(Select): getter = data.get return getter(name) + def value_omitted_from_data(self, data, files, name): + # An unselected `` don't appear in the data of an HTML form + submission, so it's unknown whether or not the user submitted a value. .. method:: use_required_attribute(initial) diff --git a/docs/releases/1.10.7.txt b/docs/releases/1.10.7.txt index de9a511f07..9d4f79f5bf 100644 --- a/docs/releases/1.10.7.txt +++ b/docs/releases/1.10.7.txt @@ -11,3 +11,6 @@ Bugfixes * Made admin's ``RelatedFieldWidgetWrapper`` use the wrapped widget's ``value_omitted_from_data()`` method (:ticket:`27905`). + +* Fixed model form ``default`` fallback for ``SelectMultiple`` + (:ticket:`27993`). diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 3ce94c1b0d..1969474b89 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -335,12 +335,14 @@ 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 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 -:class:`~django.forms.CheckboxInput` and -:class:`~django.forms.CheckboxSelectMultiple` (or any custom widget whose +:class:`~django.forms.CheckboxInput`, +:class:`~django.forms.CheckboxSelectMultiple`, or +:class:`~django.forms.SelectMultiple` (or any custom widget whose :meth:`~django.forms.Widget.value_omitted_from_data` method always returns -``False``) since an unchecked checkbox doesn't appear in the data of an HTML -form submission. Use a custom form field or widget if you're designing an API -and want the default fallback for a :class:`~django.db.models.BooleanField`. +``False``) since an unchecked checkbox and unselected ``""" )) + + def test_value_omitted_from_data(self): + widget = self.widget(choices=self.beatles) + self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), False) + self.assertIs(widget.value_omitted_from_data({'field': 'value'}, {}, 'field'), False) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 3509146856..c85eb2a6fa 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -614,6 +614,22 @@ class ModelFormBaseTest(TestCase): self.assertEqual(m1.mode, '') self.assertEqual(m1._meta.get_field('mode').get_default(), 'di') + def test_default_not_populated_on_selectmultiple(self): + class PubForm(forms.ModelForm): + mode = forms.CharField(required=False, widget=forms.SelectMultiple) + + class Meta: + model = PublicationDefaults + fields = ('mode',) + + # Empty data doesn't use the model default because an unselected + # SelectMultiple doesn't have a value in HTML form submission. + mf1 = PubForm({}) + self.assertEqual(mf1.errors, {}) + m1 = mf1.save(commit=False) + self.assertEqual(m1.mode, '') + self.assertEqual(m1._meta.get_field('mode').get_default(), 'di') + def test_prefixed_form_with_default_field(self): class PubForm(forms.ModelForm): prefix = 'form-prefix'