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