test_multiwidget.py 10.0 KB

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