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.