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