1. import copy
    
  2. from datetime import datetime
    
  3. 
    
  4. from django.forms import (
    
  5.     CharField,
    
  6.     FileInput,
    
  7.     Form,
    
  8.     MultipleChoiceField,
    
  9.     MultiValueField,
    
  10.     MultiWidget,
    
  11.     RadioSelect,
    
  12.     SelectMultiple,
    
  13.     SplitDateTimeField,
    
  14.     SplitDateTimeWidget,
    
  15.     TextInput,
    
  16. )
    
  17. 
    
  18. from .base import WidgetTest
    
  19. 
    
  20. 
    
  21. class MyMultiWidget(MultiWidget):
    
  22.     def decompress(self, value):
    
  23.         if value:
    
  24.             return value.split("__")
    
  25.         return ["", ""]
    
  26. 
    
  27. 
    
  28. class ComplexMultiWidget(MultiWidget):
    
  29.     def __init__(self, attrs=None):
    
  30.         widgets = (
    
  31.             TextInput(),
    
  32.             SelectMultiple(choices=WidgetTest.beatles),
    
  33.             SplitDateTimeWidget(),
    
  34.         )
    
  35.         super().__init__(widgets, attrs)
    
  36. 
    
  37.     def decompress(self, value):
    
  38.         if value:
    
  39.             data = value.split(",")
    
  40.             return [
    
  41.                 data[0],
    
  42.                 list(data[1]),
    
  43.                 datetime.strptime(data[2], "%Y-%m-%d %H:%M:%S"),
    
  44.             ]
    
  45.         return [None, None, None]
    
  46. 
    
  47. 
    
  48. class ComplexField(MultiValueField):
    
  49.     def __init__(self, required=True, widget=None, label=None, initial=None):
    
  50.         fields = (
    
  51.             CharField(),
    
  52.             MultipleChoiceField(choices=WidgetTest.beatles),
    
  53.             SplitDateTimeField(),
    
  54.         )
    
  55.         super().__init__(
    
  56.             fields, required=required, widget=widget, label=label, initial=initial
    
  57.         )
    
  58. 
    
  59.     def compress(self, data_list):
    
  60.         if data_list:
    
  61.             return "%s,%s,%s" % (
    
  62.                 data_list[0],
    
  63.                 "".join(data_list[1]),
    
  64.                 data_list[2],
    
  65.             )
    
  66.         return None
    
  67. 
    
  68. 
    
  69. class DeepCopyWidget(MultiWidget):
    
  70.     """
    
  71.     Used to test MultiWidget.__deepcopy__().
    
  72.     """
    
  73. 
    
  74.     def __init__(self, choices=[]):
    
  75.         widgets = [
    
  76.             RadioSelect(choices=choices),
    
  77.             TextInput,
    
  78.         ]
    
  79.         super().__init__(widgets)
    
  80. 
    
  81.     def _set_choices(self, choices):
    
  82.         """
    
  83.         When choices are set for this widget, we want to pass those along to
    
  84.         the Select widget.
    
  85.         """
    
  86.         self.widgets[0].choices = choices
    
  87. 
    
  88.     def _get_choices(self):
    
  89.         """
    
  90.         The choices for this widget are the Select widget's choices.
    
  91.         """
    
  92.         return self.widgets[0].choices
    
  93. 
    
  94.     choices = property(_get_choices, _set_choices)
    
  95. 
    
  96. 
    
  97. class MultiWidgetTest(WidgetTest):
    
  98.     def test_subwidgets_name(self):
    
  99.         widget = MultiWidget(
    
  100.             widgets={
    
  101.                 "": TextInput(),
    
  102.                 "big": TextInput(attrs={"class": "big"}),
    
  103.                 "small": TextInput(attrs={"class": "small"}),
    
  104.             },
    
  105.         )
    
  106.         self.check_html(
    
  107.             widget,
    
  108.             "name",
    
  109.             ["John", "George", "Paul"],
    
  110.             html=(
    
  111.                 '<input type="text" name="name" value="John">'
    
  112.                 '<input type="text" name="name_big" value="George" class="big">'
    
  113.                 '<input type="text" name="name_small" value="Paul" class="small">'
    
  114.             ),
    
  115.         )
    
  116. 
    
  117.     def test_text_inputs(self):
    
  118.         widget = MyMultiWidget(
    
  119.             widgets=(
    
  120.                 TextInput(attrs={"class": "big"}),
    
  121.                 TextInput(attrs={"class": "small"}),
    
  122.             )
    
  123.         )
    
  124.         self.check_html(
    
  125.             widget,
    
  126.             "name",
    
  127.             ["john", "lennon"],
    
  128.             html=(
    
  129.                 '<input type="text" class="big" value="john" name="name_0">'
    
  130.                 '<input type="text" class="small" value="lennon" name="name_1">'
    
  131.             ),
    
  132.         )
    
  133.         self.check_html(
    
  134.             widget,
    
  135.             "name",
    
  136.             ("john", "lennon"),
    
  137.             html=(
    
  138.                 '<input type="text" class="big" value="john" name="name_0">'
    
  139.                 '<input type="text" class="small" value="lennon" name="name_1">'
    
  140.             ),
    
  141.         )
    
  142.         self.check_html(
    
  143.             widget,
    
  144.             "name",
    
  145.             "john__lennon",
    
  146.             html=(
    
  147.                 '<input type="text" class="big" value="john" name="name_0">'
    
  148.                 '<input type="text" class="small" value="lennon" name="name_1">'
    
  149.             ),
    
  150.         )
    
  151.         self.check_html(
    
  152.             widget,
    
  153.             "name",
    
  154.             "john__lennon",
    
  155.             attrs={"id": "foo"},
    
  156.             html=(
    
  157.                 '<input id="foo_0" type="text" class="big" value="john" name="name_0">'
    
  158.                 '<input id="foo_1" type="text" class="small" value="lennon" '
    
  159.                 'name="name_1">'
    
  160.             ),
    
  161.         )
    
  162. 
    
  163.     def test_constructor_attrs(self):
    
  164.         widget = MyMultiWidget(
    
  165.             widgets=(
    
  166.                 TextInput(attrs={"class": "big"}),
    
  167.                 TextInput(attrs={"class": "small"}),
    
  168.             ),
    
  169.             attrs={"id": "bar"},
    
  170.         )
    
  171.         self.check_html(
    
  172.             widget,
    
  173.             "name",
    
  174.             ["john", "lennon"],
    
  175.             html=(
    
  176.                 '<input id="bar_0" type="text" class="big" value="john" name="name_0">'
    
  177.                 '<input id="bar_1" type="text" class="small" value="lennon" '
    
  178.                 'name="name_1">'
    
  179.             ),
    
  180.         )
    
  181. 
    
  182.     def test_constructor_attrs_with_type(self):
    
  183.         attrs = {"type": "number"}
    
  184.         widget = MyMultiWidget(widgets=(TextInput, TextInput()), attrs=attrs)
    
  185.         self.check_html(
    
  186.             widget,
    
  187.             "code",
    
  188.             ["1", "2"],
    
  189.             html=(
    
  190.                 '<input type="number" value="1" name="code_0">'
    
  191.                 '<input type="number" value="2" name="code_1">'
    
  192.             ),
    
  193.         )
    
  194.         widget = MyMultiWidget(
    
  195.             widgets=(TextInput(attrs), TextInput(attrs)), attrs={"class": "bar"}
    
  196.         )
    
  197.         self.check_html(
    
  198.             widget,
    
  199.             "code",
    
  200.             ["1", "2"],
    
  201.             html=(
    
  202.                 '<input type="number" value="1" name="code_0" class="bar">'
    
  203.                 '<input type="number" value="2" name="code_1" class="bar">'
    
  204.             ),
    
  205.         )
    
  206. 
    
  207.     def test_value_omitted_from_data(self):
    
  208.         widget = MyMultiWidget(widgets=(TextInput(), TextInput()))
    
  209.         self.assertIs(widget.value_omitted_from_data({}, {}, "field"), True)
    
  210.         self.assertIs(
    
  211.             widget.value_omitted_from_data({"field_0": "x"}, {}, "field"), False
    
  212.         )
    
  213.         self.assertIs(
    
  214.             widget.value_omitted_from_data({"field_1": "y"}, {}, "field"), False
    
  215.         )
    
  216.         self.assertIs(
    
  217.             widget.value_omitted_from_data(
    
  218.                 {"field_0": "x", "field_1": "y"}, {}, "field"
    
  219.             ),
    
  220.             False,
    
  221.         )
    
  222. 
    
  223.     def test_value_from_datadict_subwidgets_name(self):
    
  224.         widget = MultiWidget(widgets={"x": TextInput(), "": TextInput()})
    
  225.         tests = [
    
  226.             ({}, [None, None]),
    
  227.             ({"field": "x"}, [None, "x"]),
    
  228.             ({"field_x": "y"}, ["y", None]),
    
  229.             ({"field": "x", "field_x": "y"}, ["y", "x"]),
    
  230.         ]
    
  231.         for data, expected in tests:
    
  232.             with self.subTest(data):
    
  233.                 self.assertEqual(
    
  234.                     widget.value_from_datadict(data, {}, "field"),
    
  235.                     expected,
    
  236.                 )
    
  237. 
    
  238.     def test_value_omitted_from_data_subwidgets_name(self):
    
  239.         widget = MultiWidget(widgets={"x": TextInput(), "": TextInput()})
    
  240.         tests = [
    
  241.             ({}, True),
    
  242.             ({"field": "x"}, False),
    
  243.             ({"field_x": "y"}, False),
    
  244.             ({"field": "x", "field_x": "y"}, False),
    
  245.         ]
    
  246.         for data, expected in tests:
    
  247.             with self.subTest(data):
    
  248.                 self.assertIs(
    
  249.                     widget.value_omitted_from_data(data, {}, "field"),
    
  250.                     expected,
    
  251.                 )
    
  252. 
    
  253.     def test_needs_multipart_true(self):
    
  254.         """
    
  255.         needs_multipart_form should be True if any widgets need it.
    
  256.         """
    
  257.         widget = MyMultiWidget(widgets=(TextInput(), FileInput()))
    
  258.         self.assertTrue(widget.needs_multipart_form)
    
  259. 
    
  260.     def test_needs_multipart_false(self):
    
  261.         """
    
  262.         needs_multipart_form should be False if no widgets need it.
    
  263.         """
    
  264.         widget = MyMultiWidget(widgets=(TextInput(), TextInput()))
    
  265.         self.assertFalse(widget.needs_multipart_form)
    
  266. 
    
  267.     def test_nested_multiwidget(self):
    
  268.         """
    
  269.         MultiWidgets can be composed of other MultiWidgets.
    
  270.         """
    
  271.         widget = ComplexMultiWidget()
    
  272.         self.check_html(
    
  273.             widget,
    
  274.             "name",
    
  275.             "some text,JP,2007-04-25 06:24:00",
    
  276.             html=(
    
  277.                 """
    
  278.             <input type="text" name="name_0" value="some text">
    
  279.             <select multiple name="name_1">
    
  280.                 <option value="J" selected>John</option>
    
  281.                 <option value="P" selected>Paul</option>
    
  282.                 <option value="G">George</option>
    
  283.                 <option value="R">Ringo</option>
    
  284.             </select>
    
  285.             <input type="text" name="name_2_0" value="2007-04-25">
    
  286.             <input type="text" name="name_2_1" value="06:24:00">
    
  287.             """
    
  288.             ),
    
  289.         )
    
  290. 
    
  291.     def test_no_whitespace_between_widgets(self):
    
  292.         widget = MyMultiWidget(widgets=(TextInput, TextInput()))
    
  293.         self.check_html(
    
  294.             widget,
    
  295.             "code",
    
  296.             None,
    
  297.             html=('<input type="text" name="code_0"><input type="text" name="code_1">'),
    
  298.             strict=True,
    
  299.         )
    
  300. 
    
  301.     def test_deepcopy(self):
    
  302.         """
    
  303.         MultiWidget should define __deepcopy__() (#12048).
    
  304.         """
    
  305.         w1 = DeepCopyWidget(choices=[1, 2, 3])
    
  306.         w2 = copy.deepcopy(w1)
    
  307.         w2.choices = [4, 5, 6]
    
  308.         # w2 ought to be independent of w1, since MultiWidget ought
    
  309.         # to make a copy of its sub-widgets when it is copied.
    
  310.         self.assertEqual(w1.choices, [1, 2, 3])
    
  311. 
    
  312.     def test_fieldset(self):
    
  313.         class TestForm(Form):
    
  314.             template_name = "forms_tests/use_fieldset.html"
    
  315.             field = ComplexField(widget=ComplexMultiWidget)
    
  316. 
    
  317.         form = TestForm()
    
  318.         self.assertIs(form["field"].field.widget.use_fieldset, True)
    
  319.         self.assertHTMLEqual(
    
  320.             "<div><fieldset><legend>Field:</legend>"
    
  321.             '<input type="text" name="field_0" required id="id_field_0">'
    
  322.             '<select name="field_1" required id="id_field_1" multiple>'
    
  323.             '<option value="J">John</option><option value="P">Paul</option>'
    
  324.             '<option value="G">George</option><option value="R">Ringo</option></select>'
    
  325.             '<input type="text" name="field_2_0" required id="id_field_2_0">'
    
  326.             '<input type="text" name="field_2_1" required id="id_field_2_1">'
    
  327.             "</fieldset></div>",
    
  328.             form.render(),
    
  329.         )