test_widgets.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. import json
  2. from django import forms
  3. from django.test import TestCase
  4. from django.test.utils import override_settings
  5. from wagtail.admin import widgets
  6. from wagtail.admin.forms.tags import TagField
  7. from wagtail.models import Page
  8. from wagtail.test.testapp.forms import AdminStarDateInput
  9. from wagtail.test.testapp.models import EventPage, RestaurantTag, SimplePage
  10. class TestAdminPageChooserWidget(TestCase):
  11. def setUp(self):
  12. self.root_page = Page.objects.get(id=2)
  13. # Add child page
  14. self.child_page = SimplePage(
  15. title="foobarbaz",
  16. content="hello",
  17. )
  18. self.root_page.add_child(instance=self.child_page)
  19. def test_not_hidden(self):
  20. widget = widgets.AdminPageChooser()
  21. self.assertFalse(widget.is_hidden)
  22. def test_adapt(self):
  23. widget = widgets.AdminPageChooser()
  24. js_args = widgets.PageChooserAdapter().js_args(widget)
  25. self.assertInHTML(
  26. """<input id="__ID__" name="__NAME__" type="hidden" />""", js_args[0]
  27. )
  28. self.assertIn(">Choose a page<", js_args[0])
  29. self.assertEqual(js_args[1], "__ID__")
  30. self.assertEqual(
  31. js_args[2],
  32. {
  33. "can_choose_root": False,
  34. "model_names": ["wagtailcore.page"],
  35. "user_perms": None,
  36. },
  37. )
  38. def test_adapt_with_target_model(self):
  39. widget = widgets.AdminPageChooser(target_models=[SimplePage, EventPage])
  40. js_args = widgets.PageChooserAdapter().js_args(widget)
  41. self.assertEqual(
  42. js_args[2]["model_names"], ["tests.simplepage", "tests.eventpage"]
  43. )
  44. def test_adapt_with_can_choose_root(self):
  45. widget = widgets.AdminPageChooser(can_choose_root=True)
  46. js_args = widgets.PageChooserAdapter().js_args(widget)
  47. self.assertTrue(js_args[2]["can_choose_root"])
  48. def test_render_html(self):
  49. # render_html is mostly an internal API, but we do want to support calling it with None as
  50. # a value, to render a blank field without the JS initialiser (so that we can call that
  51. # separately in our own context and hold on to the return value)
  52. widget = widgets.AdminPageChooser()
  53. html = widget.render_html("test", None, {})
  54. self.assertInHTML("""<input name="test" type="hidden" />""", html)
  55. self.assertIn(">Choose a page<", html)
  56. def test_render_js_init(self):
  57. widget = widgets.AdminPageChooser()
  58. html = widget.render("test", None, {"id": "test-id"})
  59. self.assertIn(
  60. 'createPageChooser("test-id", null, {"model_names": ["wagtailcore.page"], "can_choose_root": false, "user_perms": null});',
  61. html,
  62. )
  63. def test_render_js_init_with_user_perm(self):
  64. widget = widgets.AdminPageChooser(user_perms="copy_to")
  65. html = widget.render("test", None, {"id": "test-id"})
  66. self.assertIn(
  67. 'createPageChooser("test-id", null, {"model_names": ["wagtailcore.page"], "can_choose_root": false, "user_perms": "copy_to"});',
  68. html,
  69. )
  70. def test_render_with_value(self):
  71. widget = widgets.AdminPageChooser()
  72. html = widget.render("test", self.child_page, {"id": "test-id"})
  73. self.assertInHTML(
  74. """<input id="test-id" name="test" type="hidden" value="%d" />"""
  75. % self.child_page.id,
  76. html,
  77. )
  78. # SimplePage has a custom get_admin_display_title method which should be reflected here
  79. self.assertInHTML("foobarbaz (simple page)", html)
  80. self.assertIn(
  81. 'createPageChooser("test-id", %d, {"model_names": ["wagtailcore.page"], "can_choose_root": false, "user_perms": null});'
  82. % self.root_page.id,
  83. html,
  84. )
  85. def test_render_with_target_model(self):
  86. widget = widgets.AdminPageChooser(target_models=[SimplePage])
  87. html = widget.render("test", None, {"id": "test-id"})
  88. self.assertIn(
  89. 'createPageChooser("test-id", null, {"model_names": ["tests.simplepage"], "can_choose_root": false, "user_perms": null});',
  90. html,
  91. )
  92. html = widget.render("test", self.child_page, {"id": "test-id"})
  93. self.assertIn(">Choose a page (Simple Page)<", html)
  94. def test_render_with_target_model_as_single_instance(self):
  95. widget = widgets.AdminPageChooser(target_models=SimplePage)
  96. html = widget.render("test", None, {"id": "test-id"})
  97. self.assertIn(
  98. 'createPageChooser("test-id", null, {"model_names": ["tests.simplepage"], "can_choose_root": false, "user_perms": null});',
  99. html,
  100. )
  101. html = widget.render("test", self.child_page, {"id": "test-id"})
  102. self.assertIn(">Choose a page (Simple Page)<", html)
  103. def test_render_with_target_model_as_single_string(self):
  104. widget = widgets.AdminPageChooser(target_models="tests.SimplePage")
  105. html = widget.render("test", None, {"id": "test-id"})
  106. self.assertIn(
  107. 'createPageChooser("test-id", null, {"model_names": ["tests.simplepage"], "can_choose_root": false, "user_perms": null});',
  108. html,
  109. )
  110. html = widget.render("test", self.child_page, {"id": "test-id"})
  111. self.assertIn(">Choose a page (Simple Page)<", html)
  112. def test_render_with_multiple_target_models(self):
  113. target_models = [SimplePage, "tests.eventpage"]
  114. widget = widgets.AdminPageChooser(target_models=target_models)
  115. html = widget.render("test", None, {"id": "test-id"})
  116. self.assertIn(
  117. 'createPageChooser("test-id", null, {"model_names": ["tests.simplepage", "tests.eventpage"], "can_choose_root": false, "user_perms": null});',
  118. html,
  119. )
  120. html = widget.render("test", self.child_page, {"id": "test-id"})
  121. self.assertIn(">Choose a page<", html)
  122. def test_render_js_init_with_can_choose_root(self):
  123. widget = widgets.AdminPageChooser(can_choose_root=True)
  124. html = widget.render("test", self.child_page, {"id": "test-id"})
  125. self.assertIn(
  126. 'createPageChooser("test-id", %d, {"model_names": ["wagtailcore.page"], "can_choose_root": true, "user_perms": null});'
  127. % self.root_page.id,
  128. html,
  129. )
  130. class TestAdminDateInput(TestCase):
  131. def test_adapt(self):
  132. widget = widgets.AdminDateInput()
  133. js_args = widgets.AdminDateInputAdapter().js_args(widget)
  134. self.assertEqual(js_args[0], {"dayOfWeekStart": 0, "format": "Y-m-d"})
  135. def test_adapt_with_custom_format(self):
  136. widget = widgets.AdminDateInput(format="%d.%m.%Y")
  137. js_args = widgets.AdminDateInputAdapter().js_args(widget)
  138. self.assertEqual(js_args[0], {"dayOfWeekStart": 0, "format": "d.m.Y"})
  139. def test_render_js_init(self):
  140. widget = widgets.AdminDateInput()
  141. html = widget.render("test", None, attrs={"id": "test-id"})
  142. self.assertInHTML(
  143. '<input type="text" name="test" autocomplete="off" id="test-id" />', html
  144. )
  145. # we should see the JS initialiser code:
  146. # initDateChooser("test-id", {"dayOfWeekStart": 0, "format": "Y-m-d"});
  147. # except that we can't predict the order of the config options
  148. self.assertIn('initDateChooser("test\\u002Did", {', html)
  149. self.assertIn('"dayOfWeekStart": 0', html)
  150. self.assertIn('"format": "Y-m-d"', html)
  151. def test_render_js_init_with_format(self):
  152. widget = widgets.AdminDateInput(format="%d.%m.%Y.")
  153. html = widget.render("test", None, attrs={"id": "test-id"})
  154. self.assertIn(
  155. '"format": "d.m.Y."',
  156. html,
  157. )
  158. @override_settings(WAGTAIL_DATE_FORMAT="%d.%m.%Y.")
  159. def test_render_js_init_with_format_from_settings(self):
  160. widget = widgets.AdminDateInput()
  161. html = widget.render("test", None, attrs={"id": "test-id"})
  162. self.assertIn(
  163. '"format": "d.m.Y."',
  164. html,
  165. )
  166. def test_media_inheritance(self):
  167. """
  168. Widgets inheriting from AdminDateInput should have their media definitions merged
  169. with AdminDateInput's
  170. """
  171. widget = AdminStarDateInput()
  172. media_html = str(widget.media)
  173. self.assertIn("wagtailadmin/js/date-time-chooser.js", media_html)
  174. self.assertIn("vendor/star_date.js", media_html)
  175. class TestAdminTimeInput(TestCase):
  176. def test_adapt(self):
  177. widget = widgets.AdminTimeInput()
  178. js_args = widgets.AdminTimeInputAdapter().js_args(widget)
  179. self.assertEqual(js_args[0], {"format": "H:i", "formatTime": "H:i"})
  180. def test_adapt_with_custom_format(self):
  181. widget = widgets.AdminTimeInput(format="%H:%M:%S")
  182. js_args = widgets.AdminTimeInputAdapter().js_args(widget)
  183. self.assertEqual(js_args[0], {"format": "H:i:s", "formatTime": "H:i:s"})
  184. def test_render_js_init(self):
  185. widget = widgets.AdminTimeInput()
  186. html = widget.render("test", None, attrs={"id": "test-id"})
  187. self.assertInHTML(
  188. '<input type="text" name="test" autocomplete="off" id="test-id" />', html
  189. )
  190. # we should see the JS initialiser code:
  191. # initDateChooser("test-id", {"dayOfWeekStart": 0, "format": "Y-m-d"});
  192. # except that we can't predict the order of the config options
  193. self.assertIn('initTimeChooser("test\\u002Did", {', html)
  194. self.assertIn('"format": "H:i"', html)
  195. def test_render_js_init_with_format(self):
  196. widget = widgets.AdminTimeInput(format="%H:%M:%S")
  197. html = widget.render("test", None, attrs={"id": "test-id"})
  198. self.assertIn(
  199. '"format": "H:i:s"',
  200. html,
  201. )
  202. @override_settings(WAGTAIL_TIME_FORMAT="%H:%M:%S")
  203. def test_render_js_init_with_format_from_settings(self):
  204. widget = widgets.AdminTimeInput()
  205. html = widget.render("test", None, attrs={"id": "test-id"})
  206. self.assertIn(
  207. '"format": "H:i:s"',
  208. html,
  209. )
  210. class TestAdminDateTimeInput(TestCase):
  211. def test_adapt(self):
  212. widget = widgets.AdminDateTimeInput()
  213. js_args = widgets.AdminDateTimeInputAdapter().js_args(widget)
  214. self.assertEqual(
  215. js_args[0],
  216. {"dayOfWeekStart": 0, "format": "Y-m-d H:i", "formatTime": "H:i"},
  217. )
  218. def test_adapt_with_custom_format(self):
  219. widget = widgets.AdminDateTimeInput(
  220. format="%d.%m.%Y. %H:%M", time_format="%H:%M %p"
  221. )
  222. js_args = widgets.AdminDateTimeInputAdapter().js_args(widget)
  223. self.assertEqual(
  224. js_args[0],
  225. {"dayOfWeekStart": 0, "format": "d.m.Y. H:i", "formatTime": "H:i A"},
  226. )
  227. def test_render_js_init(self):
  228. widget = widgets.AdminDateTimeInput()
  229. html = widget.render("test", None, attrs={"id": "test-id"})
  230. self.assertInHTML(
  231. '<input type="text" name="test" autocomplete="off" id="test-id" />', html
  232. )
  233. # we should see the JS initialiser code:
  234. # initDateTimeChooser("test-id", {"dayOfWeekStart": 0, "format": "Y-m-d H:i"});
  235. # except that we can't predict the order of the config options
  236. self.assertIn('initDateTimeChooser("test\\u002Did", {', html)
  237. self.assertIn('"dayOfWeekStart": 0', html)
  238. self.assertIn('"format": "Y-m-d H:i"', html)
  239. self.assertIn('"formatTime": "H:i"', html)
  240. def test_render_js_init_with_format(self):
  241. widget = widgets.AdminDateTimeInput(
  242. format="%d.%m.%Y. %H:%M", time_format="%H:%M %p"
  243. )
  244. html = widget.render("test", None, attrs={"id": "test-id"})
  245. self.assertIn(
  246. '"format": "d.m.Y. H:i"',
  247. html,
  248. )
  249. self.assertIn(
  250. '"formatTime": "H:i A"',
  251. html,
  252. )
  253. @override_settings(
  254. WAGTAIL_DATETIME_FORMAT="%d.%m.%Y. %H:%M", WAGTAIL_TIME_FORMAT="%H:%M %p"
  255. )
  256. def test_render_js_init_with_format_from_settings(self):
  257. widget = widgets.AdminDateTimeInput()
  258. html = widget.render("test", None, attrs={"id": "test-id"})
  259. self.assertIn(
  260. '"format": "d.m.Y. H:i"',
  261. html,
  262. )
  263. self.assertIn(
  264. '"formatTime": "H:i A"',
  265. html,
  266. )
  267. class TestAdminTagWidget(TestCase):
  268. def get_js_init_params(self, html):
  269. """Returns a list of the params passed in to initTagField from the supplied HTML"""
  270. # example - ["test_id", "/admin/tag-autocomplete/", {'allowSpaces': True}]
  271. start = "initTagField("
  272. end = ");"
  273. items_after_init = html.split(start)[1]
  274. if items_after_init:
  275. params_raw = items_after_init.split(end)[0]
  276. if params_raw:
  277. # stuff parameter string into an array so that we can unpack it as JSON
  278. return json.loads("[%s]" % params_raw)
  279. return []
  280. def get_help_text_html_element(self, html):
  281. """Return a help text html element with content as string"""
  282. start = """<input type="text" name="tags">"""
  283. end = "<script>"
  284. items_after_input_tag = html.split(start)[1]
  285. if items_after_input_tag:
  286. help_text_element = items_after_input_tag.split(end)[0].strip()
  287. return help_text_element
  288. return []
  289. def test_render_js_init_basic(self):
  290. """Checks that the 'initTagField' is correctly added to the inline script for tag widgets"""
  291. widget = widgets.AdminTagWidget()
  292. html = widget.render("tags", None, attrs={"id": "alpha"})
  293. params = self.get_js_init_params(html)
  294. self.assertEqual(
  295. params,
  296. [
  297. "alpha",
  298. "/admin/tag-autocomplete/",
  299. {"allowSpaces": True, "tagLimit": None, "autocompleteOnly": False},
  300. ],
  301. )
  302. @override_settings(TAG_SPACES_ALLOWED=False)
  303. def test_render_js_init_no_spaces_allowed(self):
  304. """Checks that the 'initTagField' includes the correct value based on TAG_SPACES_ALLOWED in settings"""
  305. widget = widgets.AdminTagWidget()
  306. html = widget.render("tags", None, attrs={"id": "alpha"})
  307. params = self.get_js_init_params(html)
  308. self.assertEqual(
  309. params,
  310. [
  311. "alpha",
  312. "/admin/tag-autocomplete/",
  313. {"allowSpaces": False, "tagLimit": None, "autocompleteOnly": False},
  314. ],
  315. )
  316. @override_settings(TAG_LIMIT=5)
  317. def test_render_js_init_with_tag_limit(self):
  318. """Checks that the 'initTagField' includes the correct value based on TAG_LIMIT in settings"""
  319. widget = widgets.AdminTagWidget()
  320. html = widget.render("tags", None, attrs={"id": "alpha"})
  321. params = self.get_js_init_params(html)
  322. self.assertEqual(
  323. params,
  324. [
  325. "alpha",
  326. "/admin/tag-autocomplete/",
  327. {"allowSpaces": True, "tagLimit": 5, "autocompleteOnly": False},
  328. ],
  329. )
  330. def test_render_js_init_with_tag_model(self):
  331. """
  332. Checks that 'initTagField' is passed the correct autocomplete URL for the custom model,
  333. and sets autocompleteOnly according to that model's free_tagging attribute
  334. """
  335. widget = widgets.AdminTagWidget(tag_model=RestaurantTag)
  336. html = widget.render("tags", None, attrs={"id": "alpha"})
  337. params = self.get_js_init_params(html)
  338. self.assertEqual(
  339. params,
  340. [
  341. "alpha",
  342. "/admin/tag-autocomplete/tests/restauranttag/",
  343. {"allowSpaces": True, "tagLimit": None, "autocompleteOnly": True},
  344. ],
  345. )
  346. def test_render_with_free_tagging_false(self):
  347. """Checks that free_tagging=False is passed to the inline script"""
  348. widget = widgets.AdminTagWidget(free_tagging=False)
  349. html = widget.render("tags", None, attrs={"id": "alpha"})
  350. params = self.get_js_init_params(html)
  351. self.assertEqual(
  352. params,
  353. [
  354. "alpha",
  355. "/admin/tag-autocomplete/",
  356. {"allowSpaces": True, "tagLimit": None, "autocompleteOnly": True},
  357. ],
  358. )
  359. def test_render_with_free_tagging_true(self):
  360. """free_tagging=True on the widget can also override the tag model setting free_tagging=False"""
  361. widget = widgets.AdminTagWidget(tag_model=RestaurantTag, free_tagging=True)
  362. html = widget.render("tags", None, attrs={"id": "alpha"})
  363. params = self.get_js_init_params(html)
  364. self.assertEqual(
  365. params,
  366. [
  367. "alpha",
  368. "/admin/tag-autocomplete/tests/restauranttag/",
  369. {"allowSpaces": True, "tagLimit": None, "autocompleteOnly": False},
  370. ],
  371. )
  372. @override_settings(TAG_SPACES_ALLOWED=True)
  373. def test_tags_help_text_spaces_allowed(self):
  374. """Checks that the tags help text html element content is correct when TAG_SPACES_ALLOWED is True"""
  375. widget = widgets.AdminTagWidget()
  376. help_text = widget.get_context(None, None, {})["widget"]["help_text"]
  377. html = widget.render("tags", None, {})
  378. help_text_html_element = self.get_help_text_html_element(html)
  379. self.assertEqual(
  380. help_text,
  381. 'Multi-word tags with spaces will automatically be enclosed in double quotes (").',
  382. )
  383. self.assertHTMLEqual(
  384. help_text_html_element,
  385. """<p class="help">%s</p>""" % help_text,
  386. )
  387. @override_settings(TAG_SPACES_ALLOWED=False)
  388. def test_tags_help_text_no_spaces_allowed(self):
  389. """Checks that the tags help text html element content is correct when TAG_SPACES_ALLOWED is False"""
  390. widget = widgets.AdminTagWidget()
  391. help_text = widget.get_context(None, None, {})["widget"]["help_text"]
  392. html = widget.render("tags", None, {})
  393. help_text_html_element = self.get_help_text_html_element(html)
  394. self.assertEqual(
  395. help_text, "Tags can only consist of a single word, no spaces allowed."
  396. )
  397. self.assertHTMLEqual(
  398. help_text_html_element,
  399. """<p class="help">%s</p>""" % help_text,
  400. )
  401. class TestTagField(TestCase):
  402. def setUp(self):
  403. RestaurantTag.objects.create(name="Italian", slug="italian")
  404. RestaurantTag.objects.create(name="Indian", slug="indian")
  405. def test_tag_whitelisting(self):
  406. class RestaurantTagForm(forms.Form):
  407. # RestaurantTag sets free_tagging=False at the model level
  408. tags = TagField(tag_model=RestaurantTag)
  409. form = RestaurantTagForm({"tags": "Italian, delicious"})
  410. self.assertTrue(form.is_valid())
  411. self.assertEqual(form.cleaned_data["tags"], ["Italian"])
  412. def test_override_free_tagging(self):
  413. class RestaurantTagForm(forms.Form):
  414. tags = TagField(tag_model=RestaurantTag, free_tagging=True)
  415. form = RestaurantTagForm({"tags": "Italian, delicious"})
  416. self.assertTrue(form.is_valid())
  417. self.assertEqual(set(form.cleaned_data["tags"]), {"Italian", "delicious"})
  418. def test_tag_over_one_hundred_characters(self):
  419. class RestaurantTagForm(forms.Form):
  420. tags = TagField(tag_model=RestaurantTag)
  421. tag_name = ""
  422. for _ in range(101):
  423. tag_name += "a"
  424. form = RestaurantTagForm({"tags": tag_name})
  425. self.assertFalse(form.is_valid())
  426. class TestFilteredSelect(TestCase):
  427. def test_render(self):
  428. widget = widgets.FilteredSelect(
  429. choices=[
  430. (None, "----"),
  431. ("FR", "France", ["EU"]),
  432. ("JP", "Japan", ["AS"]),
  433. ("RU", "Russia", ["AS", "EU"]),
  434. ],
  435. filter_field="id_continent",
  436. )
  437. html = widget.render("country", "JP")
  438. self.assertHTMLEqual(
  439. html,
  440. """
  441. <select name="country" data-widget="filtered-select" data-filter-field="id_continent">
  442. <option value="">----</option>
  443. <option value="FR" data-filter-value="EU">France</option>
  444. <option value="JP" selected data-filter-value="AS">Japan</option>
  445. <option value="RU" data-filter-value="AS,EU">Russia</option>
  446. </select>
  447. """,
  448. )
  449. def test_optgroups(self):
  450. widget = widgets.FilteredSelect(
  451. choices=[
  452. (None, "----"),
  453. (
  454. "Big countries",
  455. [
  456. ("FR", "France", ["EU"]),
  457. ("JP", "Japan", ["AS"]),
  458. ("RU", "Russia", ["AS", "EU"]),
  459. ("MOON", "The moon"),
  460. ],
  461. ),
  462. (
  463. "Small countries",
  464. [
  465. ("AZ", "Azerbaijan", ["AS"]),
  466. ("LI", "Liechtenstein", ["EU"]),
  467. ],
  468. ),
  469. ("SK", "Slovakia", ["EU"]),
  470. ],
  471. filter_field="id_continent",
  472. )
  473. html = widget.render("country", "JP")
  474. self.assertHTMLEqual(
  475. html,
  476. """
  477. <select name="country" data-widget="filtered-select" data-filter-field="id_continent">
  478. <option value="">----</option>
  479. <optgroup label="Big countries">
  480. <option value="FR" data-filter-value="EU">France</option>
  481. <option value="JP" selected data-filter-value="AS">Japan</option>
  482. <option value="RU" data-filter-value="AS,EU">Russia</option>
  483. <option value="MOON">The moon</option>
  484. </optgroup>
  485. <optgroup label="Small countries">
  486. <option value="AZ" data-filter-value="AS">Azerbaijan</option>
  487. <option value="LI" data-filter-value="EU">Liechtenstein</option>
  488. </optgroup>
  489. <option value="SK" data-filter-value="EU">Slovakia</option>
  490. </select>
  491. """,
  492. )