From 27746ab28ac88fc213456e63302b408312d95122 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 13 Feb 2020 22:12:46 +0000 Subject: [PATCH] Fixed #7664 -- Allowed customizing suffixes of MultiWidget.widgets' names. --- django/forms/widgets.py | 20 ++++++--- docs/ref/forms/widgets.txt | 22 +++++++++- docs/releases/3.1.txt | 3 ++ .../widget_tests/test_multiwidget.py | 43 +++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 40ac1d3162..a210125454 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -799,6 +799,13 @@ class MultiWidget(Widget): template_name = 'django/forms/widgets/multiwidget.html' def __init__(self, widgets, attrs=None): + if isinstance(widgets, dict): + self.widgets_names = [ + ('_%s' % name) if name else '' for name in widgets + ] + widgets = widgets.values() + else: + self.widgets_names = ['_%s' % i for i in range(len(widgets))] self.widgets = [w() if isinstance(w, type) else w for w in widgets] super().__init__(attrs) @@ -820,10 +827,10 @@ class MultiWidget(Widget): input_type = final_attrs.pop('type', None) id_ = final_attrs.get('id') subwidgets = [] - for i, widget in enumerate(self.widgets): + for i, (widget_name, widget) in enumerate(zip(self.widgets_names, self.widgets)): if input_type is not None: widget.input_type = input_type - widget_name = '%s_%s' % (name, i) + widget_name = name + widget_name try: widget_value = value[i] except IndexError: @@ -843,12 +850,15 @@ class MultiWidget(Widget): return id_ 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 + widget_name) + for widget_name, widget in zip(self.widgets_names, 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) + widget.value_omitted_from_data(data, files, name + widget_name) + for widget_name, widget in zip(self.widgets_names, self.widgets) ) def decompress(self, value): diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 02077f631f..5a54051cda 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -354,7 +354,27 @@ foundation for custom widgets. .. attribute:: MultiWidget.widgets - An iterable containing the widgets needed. + An iterable containing the widgets needed. For example:: + + >>> from django.forms import MultiWidget, TextInput + >>> widget = MultiWidget(widgets=[TextInput, TextInput]) + >>> widget.render('name', ['john', 'paul']) + '' + + You may provide a dictionary in order to specify custom suffixes for + the ``name`` attribute on each subwidget. In this case, for each + ``(key, widget)`` pair, the key will be appended to the ``name`` of the + widget in order to generate the attribute value. You may provide the + empty string (`''`) for a single key, in order to suppress the suffix + for one widget. For example:: + + >>> widget = MultiWidget(widgets={'': TextInput, 'last': TextInput}) + >>> widget.render('name', ['john', 'lennon']) + '' + + .. versionchanged::3.1 + + Support for using a dictionary was added. And one required method: diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 9961aebbab..630dd4bc35 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -270,6 +270,9 @@ Forms now uses ``DATE_INPUT_FORMATS`` in addition to ``DATETIME_INPUT_FORMATS`` when converting a field input to a ``datetime`` value. +* :attr:`.MultiWidget.widgets` now accepts a dictionary which allows + customizing subwidget ``name`` attributes. + Generic Views ~~~~~~~~~~~~~ diff --git a/tests/forms_tests/widget_tests/test_multiwidget.py b/tests/forms_tests/widget_tests/test_multiwidget.py index 783eb78b85..0e5ee8f73f 100644 --- a/tests/forms_tests/widget_tests/test_multiwidget.py +++ b/tests/forms_tests/widget_tests/test_multiwidget.py @@ -79,6 +79,19 @@ class DeepCopyWidget(MultiWidget): class MultiWidgetTest(WidgetTest): + def test_subwidgets_name(self): + widget = MultiWidget( + widgets={ + '': TextInput(), + 'big': TextInput(attrs={'class': 'big'}), + 'small': TextInput(attrs={'class': 'small'}), + }, + ) + self.check_html(widget, 'name', ['John', 'George', 'Paul'], html=( + '' + '' + '' + )) def test_text_inputs(self): widget = MyMultiWidget( @@ -133,6 +146,36 @@ class MultiWidgetTest(WidgetTest): 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_value_from_datadict_subwidgets_name(self): + widget = MultiWidget(widgets={'x': TextInput(), '': TextInput()}) + tests = [ + ({}, [None, None]), + ({'field': 'x'}, [None, 'x']), + ({'field_x': 'y'}, ['y', None]), + ({'field': 'x', 'field_x': 'y'}, ['y', 'x']), + ] + for data, expected in tests: + with self.subTest(data): + self.assertEqual( + widget.value_from_datadict(data, {}, 'field'), + expected, + ) + + def test_value_omitted_from_data_subwidgets_name(self): + widget = MultiWidget(widgets={'x': TextInput(), '': TextInput()}) + tests = [ + ({}, True), + ({'field': 'x'}, False), + ({'field_x': 'y'}, False), + ({'field': 'x', 'field_x': 'y'}, False), + ] + for data, expected in tests: + with self.subTest(data): + self.assertIs( + widget.value_omitted_from_data(data, {}, 'field'), + expected, + ) + def test_needs_multipart_true(self): """ needs_multipart_form should be True if any widgets need it.