瀏覽代碼

Fixed #7664 -- Allowed customizing suffixes of MultiWidget.widgets' names.

David Smith 5 年之前
父節點
當前提交
27746ab28a
共有 4 個文件被更改,包括 82 次插入6 次删除
  1. 15 5
      django/forms/widgets.py
  2. 21 1
      docs/ref/forms/widgets.txt
  3. 3 0
      docs/releases/3.1.txt
  4. 43 0
      tests/forms_tests/widget_tests/test_multiwidget.py

+ 15 - 5
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):

+ 21 - 1
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'])
+            '<input type="text" name="name_0" value="john"><input type="text" name="name_1" value="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'])
+            '<input type="text" name="name" value="john"><input type="text" name="name_last" value="paul">'
+
+        .. versionchanged::3.1
+
+            Support for using a dictionary was added.
 
     And one required method:
 

+ 3 - 0
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
 ~~~~~~~~~~~~~
 

+ 43 - 0
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=(
+            '<input type="text" name="name" value="John">'
+            '<input type="text" name="name_big" value="George" class="big">'
+            '<input type="text" name="name_small" value="Paul" class="small">'
+        ))
 
     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.