mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #27186 -- Fixed model form default fallback for MultiWidget, FileInput, SplitDateTimeWidget, SelectDateWidget, and SplitArrayWidget.
Thanks Matt Westcott for the review.
This commit is contained in:
		| @@ -103,6 +103,12 @@ class SplitArrayWidget(forms.Widget): | ||||
|         return [self.widget.value_from_datadict(data, files, '%s_%s' % (name, index)) | ||||
|                 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_): | ||||
|         # See the comment for RadioSelect.id_for_label() | ||||
|         if id_: | ||||
|   | ||||
| @@ -54,8 +54,8 @@ def construct_instance(form, instance, fields=None, exclude=None): | ||||
|             continue | ||||
|         # 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. | ||||
|         if (f.has_default() and form.add_prefix(f.name) not in form.data and | ||||
|                 not getattr(form[f.name].field.widget, 'dont_use_model_field_default_for_empty_data', False)): | ||||
|         if (f.has_default() and | ||||
|                 form[f.name].field.widget.value_omitted_from_data(form.data, form.files, form.add_prefix(f.name))): | ||||
|             continue | ||||
|         # Defer saving file-type fields until after the other fields, so a | ||||
|         # callable upload_to can use the values from other fields. | ||||
|   | ||||
| @@ -236,6 +236,9 @@ class Widget(six.with_metaclass(RenameWidgetMethods)): | ||||
|         """ | ||||
|         return data.get(name) | ||||
|  | ||||
|     def value_omitted_from_data(self, data, files, name): | ||||
|         return name not in data | ||||
|  | ||||
|     def id_for_label(self, id_): | ||||
|         """ | ||||
|         Returns the HTML ID attribute of this Widget for use by a <label>, | ||||
| @@ -351,6 +354,9 @@ class FileInput(Input): | ||||
|         "File widgets take data from FILES, not POST" | ||||
|         return files.get(name) | ||||
|  | ||||
|     def value_omitted_from_data(self, data, files, name): | ||||
|         return name not in files | ||||
|  | ||||
|  | ||||
| FILE_INPUT_CONTRADICTION = object() | ||||
|  | ||||
| @@ -481,10 +487,6 @@ def boolean_check(v): | ||||
|  | ||||
|  | ||||
| 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): | ||||
|         super(CheckboxInput, self).__init__(attrs) | ||||
|         # check_test is a callable that takes a value and returns True | ||||
| @@ -510,6 +512,11 @@ class CheckboxInput(Widget): | ||||
|             value = values.get(value.lower(), 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): | ||||
|     allow_multiple_selected = False | ||||
| @@ -876,6 +883,12 @@ class MultiWidget(Widget): | ||||
|     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)] | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|         Given a list of rendered widgets (as strings), returns a Unicode string | ||||
| @@ -1061,6 +1074,12 @@ class SelectDateWidget(Widget): | ||||
|                 return '%s-%s-%s' % (y, m, d) | ||||
|         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): | ||||
|         if 'id' in self.attrs: | ||||
|             id_ = self.attrs['id'] | ||||
|   | ||||
| @@ -270,6 +270,21 @@ foundation for custom widgets. | ||||
|         customize it and add expensive processing, you should implement some | ||||
|         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`` | ||||
| --------------- | ||||
|  | ||||
|   | ||||
| @@ -17,3 +17,8 @@ Bugfixes | ||||
|  | ||||
| * Disabled system check for URL patterns beginning with a '/' when | ||||
|   ``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. | ||||
|   | ||||
| @@ -301,6 +301,8 @@ to the ``error_messages`` dictionary of the ``ModelForm``’s inner ``Meta`` cla | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| .. _topics-modelform-save: | ||||
|  | ||||
| The ``save()`` method | ||||
| --------------------- | ||||
|  | ||||
| @@ -335,11 +337,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 | ||||
| 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` (or any custom widget with | ||||
| ``dont_use_model_field_default_for_empty_data=True``) 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 ``BooleanField``. | ||||
| :class:`~django.forms.CheckboxInput` (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`. | ||||
|  | ||||
| .. versionchanged:: 1.10.1 | ||||
|  | ||||
| @@ -347,6 +349,10 @@ for a ``BooleanField``. | ||||
|     :class:`~django.forms.CheckboxInput` which means that unchecked checkboxes | ||||
|     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 | ||||
| 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 | ||||
|   | ||||
| @@ -85,3 +85,7 @@ class CheckboxInputTest(WidgetTest): | ||||
|     def test_value_from_datadict_string_int(self): | ||||
|         value = self.widget.value_from_datadict({'testing': '0'}, {}, 'testing') | ||||
|         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', '', 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" />' | ||||
|         )) | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|         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'), | ||||
|             '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) | ||||
|   | ||||
							
								
								
									
										12
									
								
								tests/forms_tests/widget_tests/test_widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tests/forms_tests/widget_tests/test_widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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')) | ||||
|     title = models.CharField(max_length=30) | ||||
|     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) | ||||
|     category = models.IntegerField(choices=CATEGORY_CHOICES, default=default_category) | ||||
|     active = models.BooleanField(default=True) | ||||
|     file = models.FileField(default='default.txt') | ||||
|  | ||||
|  | ||||
| class Author(models.Model): | ||||
|   | ||||
| @@ -617,6 +617,58 @@ class ModelFormBaseTest(TestCase): | ||||
|         m1 = mf1.save(commit=False) | ||||
|         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 Meta: | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class PostgreSQLModel(models.Model): | ||||
|  | ||||
|  | ||||
| class IntegerArrayModel(PostgreSQLModel): | ||||
|     field = ArrayField(models.IntegerField()) | ||||
|     field = ArrayField(models.IntegerField(), default=[], blank=True) | ||||
|  | ||||
|  | ||||
| class NullableIntegerArrayModel(PostgreSQLModel): | ||||
|   | ||||
| @@ -21,7 +21,9 @@ from .models import ( | ||||
|  | ||||
| try: | ||||
|     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: | ||||
|     pass | ||||
|  | ||||
| @@ -756,3 +758,26 @@ class TestSplitFormField(PostgreSQLTestCase): | ||||
|             '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).', | ||||
|         ]) | ||||
|  | ||||
|     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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user