1import collections
2import datetime
3import json
4
5from django.contrib.auth import get_user_model
6from django.urls import reverse
7from django.utils import timezone
8
9from wagtail.api.v2.tests.test_pages import TestPageDetail, TestPageListing
10from wagtail.core import hooks
11from wagtail.core.models import Locale, Page
12from wagtail.tests.demosite import models
13from wagtail.tests.testapp.models import SimplePage, StreamPage
14from wagtail.users.models import UserProfile
15
16from .utils import AdminAPITestCase
17
18
19def get_total_page_count():
20    # Need to take away 1 as the root page is invisible over the API by default
21    return Page.objects.count() - 1
22
23
24class TestAdminPageListing(AdminAPITestCase, TestPageListing):
25    fixtures = ['demosite.json']
26
27    def get_response(self, **params):
28        return self.client.get(reverse('wagtailadmin_api:pages:listing'), params)
29
30    def get_page_id_list(self, content):
31        return [page['id'] for page in content['items']]
32
33    # BASIC TESTS
34
35    def test_basic(self):
36        response = self.get_response()
37
38        self.assertEqual(response.status_code, 200)
39        self.assertEqual(response['Content-type'], 'application/json')
40
41        # Will crash if the JSON is invalid
42        content = json.loads(response.content.decode('UTF-8'))
43
44        # Check that the meta section is there
45        self.assertIn('meta', content)
46        self.assertIsInstance(content['meta'], dict)
47
48        # Check that the total count is there and correct
49        self.assertIn('total_count', content['meta'])
50        self.assertIsInstance(content['meta']['total_count'], int)
51        self.assertEqual(content['meta']['total_count'], get_total_page_count())
52
53        # Check that the items section is there
54        self.assertIn('items', content)
55        self.assertIsInstance(content['items'], list)
56
57        # Check that each page has a meta section with type, detail_url, html_url, status and children attributes
58        for page in content['items']:
59            self.assertIn('meta', page)
60            self.assertIsInstance(page['meta'], dict)
61            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'html_url', 'status', 'children', 'slug', 'first_published_at', 'latest_revision_created_at'})
62
63        # Check the type info
64        self.assertIsInstance(content['__types'], dict)
65        self.assertEqual(set(content['__types'].keys()), {
66            'demosite.EventPage',
67            'demosite.StandardIndexPage',
68            'demosite.PersonPage',
69            'demosite.HomePage',
70            'demosite.StandardPage',
71            'demosite.EventIndexPage',
72            'demosite.ContactPage',
73            'demosite.BlogEntryPage',
74            'demosite.BlogIndexPage',
75        })
76        self.assertEqual(set(content['__types']['demosite.EventPage'].keys()), {'verbose_name', 'verbose_name_plural'})
77        self.assertEqual(content['__types']['demosite.EventPage']['verbose_name'], 'event page')
78        self.assertEqual(content['__types']['demosite.EventPage']['verbose_name_plural'], 'event pages')
79
80    # Not applicable to the admin API
81    test_unpublished_pages_dont_appear_in_list = None
82    test_private_pages_dont_appear_in_list = None
83
84    def test_unpublished_pages_appear_in_list(self):
85        total_count = get_total_page_count()
86
87        page = models.BlogEntryPage.objects.get(id=16)
88        page.unpublish()
89
90        response = self.get_response()
91        content = json.loads(response.content.decode('UTF-8'))
92        self.assertEqual(content['meta']['total_count'], total_count)
93
94    def test_private_pages_appear_in_list(self):
95        total_count = get_total_page_count()
96
97        page = models.BlogIndexPage.objects.get(id=5)
98        page.view_restrictions.create(password='test')
99
100        new_total_count = get_total_page_count()
101        self.assertEqual(total_count, total_count)
102
103        response = self.get_response()
104        content = json.loads(response.content.decode('UTF-8'))
105        self.assertEqual(content['meta']['total_count'], new_total_count)
106
107    def test_get_in_non_content_language(self):
108        # set logged-in user's admin UI language to Swedish
109        user = get_user_model().objects.get(email='test@email.com')
110        UserProfile.objects.update_or_create(user=user, defaults={'preferred_language': 'se'})
111
112        response = self.get_response()
113
114        self.assertEqual(response.status_code, 200)
115        self.assertEqual(response['Content-type'], 'application/json')
116
117        # Will crash if the JSON is invalid
118        content = json.loads(response.content.decode('UTF-8'))
119        self.assertIn('meta', content)
120
121    # FIELDS
122
123    # Not applicable to the admin API
124    test_parent_field_gives_error = None
125
126    def test_fields(self):
127        response = self.get_response(type='demosite.BlogEntryPage', fields='title,date,feed_image')
128        content = json.loads(response.content.decode('UTF-8'))
129
130        for page in content['items']:
131            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title', 'date', 'feed_image'})
132
133    def test_fields_default(self):
134        response = self.get_response(type='demosite.BlogEntryPage')
135        content = json.loads(response.content.decode('UTF-8'))
136
137        for page in content['items']:
138            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title'})
139            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'html_url', 'children', 'status', 'slug', 'first_published_at', 'latest_revision_created_at'})
140
141    def test_remove_meta_fields(self):
142        response = self.get_response(fields='-html_url')
143        content = json.loads(response.content.decode('UTF-8'))
144
145        for page in content['items']:
146            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title'})
147            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'slug', 'first_published_at', 'latest_revision_created_at', 'status', 'children'})
148
149    def test_remove_all_meta_fields(self):
150        response = self.get_response(fields='-type,-detail_url,-slug,-first_published_at,-html_url,-latest_revision_created_at,-status,-children')
151        content = json.loads(response.content.decode('UTF-8'))
152
153        for page in content['items']:
154            self.assertEqual(set(page.keys()), {'id', 'title', 'admin_display_title'})
155
156    def test_remove_fields(self):
157        response = self.get_response(fields='-title,-admin_display_title')
158        content = json.loads(response.content.decode('UTF-8'))
159
160        for page in content['items']:
161            self.assertEqual(set(page.keys()), {'id', 'meta'})
162
163    def test_remove_id_field(self):
164        response = self.get_response(fields='-id')
165        content = json.loads(response.content.decode('UTF-8'))
166
167        for page in content['items']:
168            self.assertEqual(set(page.keys()), {'meta', 'title', 'admin_display_title'})
169
170    def test_all_fields(self):
171        response = self.get_response(type='demosite.BlogEntryPage', fields='*')
172        content = json.loads(response.content.decode('UTF-8'))
173
174        for page in content['items']:
175            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
176            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'children', 'descendants', 'ancestors', 'translations', 'status', 'latest_revision_created_at'})
177
178    def test_all_fields_then_remove_something(self):
179        response = self.get_response(type='demosite.BlogEntryPage', fields='*,-title,-admin_display_title,-date,-seo_title,-status')
180        content = json.loads(response.content.decode('UTF-8'))
181
182        for page in content['items']:
183            self.assertEqual(set(page.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
184            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'children', 'descendants', 'ancestors', 'translations', 'latest_revision_created_at'})
185
186    def test_all_nested_fields(self):
187        response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(*)')
188        content = json.loads(response.content.decode('UTF-8'))
189
190        for page in content['items']:
191            self.assertEqual(set(page['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
192
193    def test_fields_foreign_key(self):
194        # Only the base the detail_url is different here from the public API
195        response = self.get_response(type='demosite.BlogEntryPage', fields='title,date,feed_image')
196        content = json.loads(response.content.decode('UTF-8'))
197
198        for page in content['items']:
199            feed_image = page['feed_image']
200
201            if feed_image is not None:
202                self.assertIsInstance(feed_image, dict)
203                self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
204                self.assertIsInstance(feed_image['id'], int)
205                self.assertIsInstance(feed_image['meta'], dict)
206                self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url', 'download_url'})
207                self.assertEqual(feed_image['meta']['type'], 'wagtailimages.Image')
208                self.assertEqual(feed_image['meta']['detail_url'], 'http://localhost/admin/api/main/images/%d/' % feed_image['id'])
209
210    def test_fields_parent(self):
211        response = self.get_response(type='demosite.BlogEntryPage', fields='parent')
212        content = json.loads(response.content.decode('UTF-8'))
213
214        for page in content['items']:
215            parent = page['meta']['parent']
216
217            # All blog entry pages have the same parent
218            self.assertDictEqual(parent, {
219                'id': 5,
220                'meta': {
221                    'type': 'demosite.BlogIndexPage',
222                    'detail_url': 'http://localhost/admin/api/main/pages/5/',
223                    'html_url': 'http://localhost/blog-index/',
224                },
225                'title': "Blog index"
226            })
227
228    def test_fields_descendants(self):
229        response = self.get_response(fields='descendants')
230        content = json.loads(response.content.decode('UTF-8'))
231
232        for page in content['items']:
233            descendants = page['meta']['descendants']
234            self.assertEqual(set(descendants.keys()), {'count', 'listing_url'})
235            self.assertIsInstance(descendants['count'], int)
236            self.assertEqual(descendants['listing_url'], 'http://localhost/admin/api/main/pages/?descendant_of=%d' % page['id'])
237
238    def test_fields_child_relation(self):
239        response = self.get_response(type='demosite.BlogEntryPage', fields='title,related_links')
240        content = json.loads(response.content.decode('UTF-8'))
241
242        for page in content['items']:
243            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title', 'related_links'})
244            self.assertIsInstance(page['related_links'], list)
245
246    def test_fields_ordering(self):
247        response = self.get_response(type='demosite.BlogEntryPage', fields='date,title,feed_image,related_links')
248
249        # Will crash if the JSON is invalid
250        content = json.loads(response.content.decode('UTF-8'))
251
252        # Test field order
253        content = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(response.content.decode('UTF-8'))
254        field_order = [
255            'id',
256            'meta',
257            'title',
258            'admin_display_title',
259            'date',
260            'feed_image',
261            'related_links',
262        ]
263        self.assertEqual(list(content['items'][0].keys()), field_order)
264
265    def test_fields_tags(self):
266        response = self.get_response(type='demosite.BlogEntryPage', fields='tags')
267        content = json.loads(response.content.decode('UTF-8'))
268
269        for page in content['items']:
270            self.assertEqual(set(page.keys()), {'id', 'meta', 'tags', 'title', 'admin_display_title'})
271            self.assertIsInstance(page['tags'], list)
272
273    def test_fields_translations(self):
274        # Add a translation of the homepage
275        french = Locale.objects.create(language_code='fr')
276        homepage = Page.objects.get(depth=2)
277        french_homepage = homepage.copy_for_translation(french)
278
279        response = self.get_response(fields='translations')
280        content = json.loads(response.content.decode('UTF-8'))
281
282        for page in content['items']:
283            translations = page['meta']['translations']
284
285            if page['id'] == homepage.id:
286                self.assertEqual(len(translations), 1)
287                self.assertEqual(translations[0]['id'], french_homepage.id)
288                self.assertEqual(translations[0]['meta']['locale'], 'fr')
289
290            elif page['id'] == french_homepage.id:
291                self.assertEqual(len(translations), 1)
292                self.assertEqual(translations[0]['id'], homepage.id)
293                self.assertEqual(translations[0]['meta']['locale'], 'en')
294
295            else:
296                self.assertEqual(translations, [])
297
298    # CHILD OF FILTER
299
300    # Not applicable to the admin API
301    test_child_of_page_thats_not_in_same_site_gives_error = None
302
303    def test_child_of_root(self):
304        # Only return the homepage as that's the only child of the "root" node
305        # in the tree. This is different to the public API which pretends the
306        # homepage of the current site is the root page.
307        response = self.get_response(child_of='root')
308        content = json.loads(response.content.decode('UTF-8'))
309
310        page_id_list = self.get_page_id_list(content)
311        self.assertEqual(page_id_list, [2])
312
313    def test_child_of_page_1(self):
314        # Public API doesn't allow this, as it's the root page
315        response = self.get_response(child_of=1)
316        json.loads(response.content.decode('UTF-8'))
317
318        self.assertEqual(response.status_code, 200)
319
320    # DESCENDANT OF FILTER
321
322    # Not applicable to the admin API
323    test_descendant_of_page_thats_not_in_same_site_gives_error = None
324
325    def test_descendant_of_root(self):
326        response = self.get_response(descendant_of='root')
327        content = json.loads(response.content.decode('UTF-8'))
328
329        page_id_list = self.get_page_id_list(content)
330        self.assertEqual(page_id_list, [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12])
331
332    def test_descendant_of_root_doesnt_give_error(self):
333        # Public API doesn't allow this
334        response = self.get_response(descendant_of=1)
335        json.loads(response.content.decode('UTF-8'))
336
337        self.assertEqual(response.status_code, 200)
338
339    # FOR EXPLORER FILTER
340
341    def make_simple_page(self, parent, title):
342        return parent.add_child(instance=SimplePage(title=title, content='Simple page'))
343
344    def test_for_explorer_filter(self):
345        movies = self.make_simple_page(Page.objects.get(pk=1), 'Movies')
346        visible_movies = [
347            self.make_simple_page(movies, 'The Way of the Dragon'),
348            self.make_simple_page(movies, 'Enter the Dragon'),
349            self.make_simple_page(movies, 'Dragons Forever'),
350        ]
351        hidden_movies = [
352            self.make_simple_page(movies, 'The Hidden Fortress'),
353            self.make_simple_page(movies, 'Crouching Tiger, Hidden Dragon'),
354            self.make_simple_page(movies, 'Crouching Tiger, Hidden Dragon: Sword of Destiny'),
355        ]
356
357        response = self.get_response(child_of=movies.pk, for_explorer=1)
358        content = json.loads(response.content.decode('UTF-8'))
359        page_id_list = self.get_page_id_list(content)
360        self.assertEqual(page_id_list, [page.pk for page in visible_movies])
361
362        response = self.get_response(child_of=movies.pk)
363        content = json.loads(response.content.decode('UTF-8'))
364        page_id_list = self.get_page_id_list(content)
365        self.assertEqual(page_id_list, [page.pk for page in visible_movies + hidden_movies])
366
367    def test_for_explorer_no_child_of(self):
368        response = self.get_response(for_explorer=1)
369        self.assertEqual(response.status_code, 400)
370        content = json.loads(response.content.decode('UTF-8'))
371        self.assertEqual(content, {
372            'message': 'filtering by for_explorer without child_of is not supported',
373        })
374
375    def test_for_explorer_construct_explorer_page_queryset_ordering(self):
376        def set_custom_ordering(parent_page, pages, request):
377            return pages.order_by('-title')
378
379        with hooks.register_temporarily('construct_explorer_page_queryset', set_custom_ordering):
380            response = self.get_response(for_explorer=True, child_of=2)
381
382        content = json.loads(response.content.decode('UTF-8'))
383        page_id_list = self.get_page_id_list(content)
384
385        self.assertEqual(page_id_list, [6, 20, 4, 12, 5])
386
387    # HAS CHILDREN FILTER
388
389    def test_has_children_filter(self):
390        response = self.get_response(has_children='true')
391        content = json.loads(response.content.decode('UTF-8'))
392
393        page_id_list = self.get_page_id_list(content)
394        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20])
395
396    def test_has_children_filter_off(self):
397        response = self.get_response(has_children='false')
398        content = json.loads(response.content.decode('UTF-8'))
399
400        page_id_list = self.get_page_id_list(content)
401        self.assertEqual(page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12])
402
403    def test_has_children_filter_int(self):
404        response = self.get_response(has_children=1)
405        content = json.loads(response.content.decode('UTF-8'))
406
407        page_id_list = self.get_page_id_list(content)
408        self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20])
409
410    def test_has_children_filter_int_off(self):
411        response = self.get_response(has_children=0)
412        content = json.loads(response.content.decode('UTF-8'))
413
414        page_id_list = self.get_page_id_list(content)
415        self.assertEqual(page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12])
416
417    def test_has_children_filter_invalid_integer(self):
418        response = self.get_response(has_children=3)
419        content = json.loads(response.content.decode('UTF-8'))
420
421        self.assertEqual(response.status_code, 400)
422        self.assertEqual(content, {'message': "has_children must be 'true' or 'false'"})
423
424    def test_has_children_filter_invalid_value(self):
425        response = self.get_response(has_children='yes')
426        content = json.loads(response.content.decode('UTF-8'))
427
428        self.assertEqual(response.status_code, 400)
429        self.assertEqual(content, {'message': "has_children must be 'true' or 'false'"})
430
431    # TYPE FILTER
432
433    def test_type_filter_items_are_all_blog_entries(self):
434        response = self.get_response(type='demosite.BlogEntryPage')
435        content = json.loads(response.content.decode('UTF-8'))
436
437        for page in content['items']:
438            self.assertEqual(page['meta']['type'], 'demosite.BlogEntryPage')
439
440            # No specific fields available by default
441            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title'})
442
443    def test_type_filter_multiple(self):
444        response = self.get_response(type='demosite.BlogEntryPage,demosite.EventPage')
445        content = json.loads(response.content.decode('UTF-8'))
446
447        blog_page_seen = False
448        event_page_seen = False
449
450        for page in content['items']:
451            self.assertIn(page['meta']['type'], ['demosite.BlogEntryPage', 'demosite.EventPage'])
452
453            if page['meta']['type'] == 'demosite.BlogEntryPage':
454                blog_page_seen = True
455            elif page['meta']['type'] == 'demosite.EventPage':
456                event_page_seen = True
457
458            # Only generic fields available
459            self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title'})
460
461        self.assertTrue(blog_page_seen, "No blog pages were found in the items")
462        self.assertTrue(event_page_seen, "No event pages were found in the items")
463
464
465class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
466    fixtures = ['demosite.json']
467
468    def get_response(self, page_id, **params):
469        return self.client.get(reverse('wagtailadmin_api:pages:detail', args=(page_id, )), params)
470
471    def test_basic(self):
472        response = self.get_response(16)
473
474        self.assertEqual(response.status_code, 200)
475        self.assertEqual(response['Content-type'], 'application/json')
476
477        # Will crash if the JSON is invalid
478        content = json.loads(response.content.decode('UTF-8'))
479
480        # Check the id field
481        self.assertIn('id', content)
482        self.assertEqual(content['id'], 16)
483
484        # Check that the meta section is there
485        self.assertIn('meta', content)
486        self.assertIsInstance(content['meta'], dict)
487
488        # Check the meta type
489        self.assertIn('type', content['meta'])
490        self.assertEqual(content['meta']['type'], 'demosite.BlogEntryPage')
491
492        # Check the meta detail_url
493        self.assertIn('detail_url', content['meta'])
494        self.assertEqual(content['meta']['detail_url'], 'http://localhost/admin/api/main/pages/16/')
495
496        # Check the meta html_url
497        self.assertIn('html_url', content['meta'])
498        self.assertEqual(content['meta']['html_url'], 'http://localhost/blog-index/blog-post/')
499
500        # Check the meta status
501
502        self.assertIn('status', content['meta'])
503        self.assertEqual(content['meta']['status'], {
504            'status': 'live',
505            'live': True,
506            'has_unpublished_changes': False
507        })
508
509        # Check the meta children
510
511        self.assertIn('children', content['meta'])
512        self.assertEqual(content['meta']['children'], {
513            'count': 0,
514            'listing_url': 'http://localhost/admin/api/main/pages/?child_of=16'
515        })
516
517        # Check the parent field
518        self.assertIn('parent', content['meta'])
519        self.assertIsInstance(content['meta']['parent'], dict)
520        self.assertEqual(set(content['meta']['parent'].keys()), {'id', 'meta', 'title'})
521        self.assertEqual(content['meta']['parent']['id'], 5)
522        self.assertIsInstance(content['meta']['parent']['meta'], dict)
523        self.assertEqual(set(content['meta']['parent']['meta'].keys()), {'type', 'detail_url', 'html_url'})
524        self.assertEqual(content['meta']['parent']['meta']['type'], 'demosite.BlogIndexPage')
525        self.assertEqual(content['meta']['parent']['meta']['detail_url'], 'http://localhost/admin/api/main/pages/5/')
526        self.assertEqual(content['meta']['parent']['meta']['html_url'], 'http://localhost/blog-index/')
527
528        # Check that the custom fields are included
529        self.assertIn('date', content)
530        self.assertIn('body', content)
531        self.assertIn('tags', content)
532        self.assertIn('feed_image', content)
533        self.assertIn('related_links', content)
534        self.assertIn('carousel_items', content)
535
536        # Check that the date was serialised properly
537        self.assertEqual(content['date'], '2013-12-02')
538
539        # Check that the tags were serialised properly
540        self.assertEqual(content['tags'], ['bird', 'wagtail'])
541
542        # Check that the feed image was serialised properly
543        self.assertIsInstance(content['feed_image'], dict)
544        self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta', 'title'})
545        self.assertEqual(content['feed_image']['id'], 7)
546        self.assertIsInstance(content['feed_image']['meta'], dict)
547        self.assertEqual(set(content['feed_image']['meta'].keys()), {'type', 'detail_url', 'download_url'})
548        self.assertEqual(content['feed_image']['meta']['type'], 'wagtailimages.Image')
549        self.assertEqual(content['feed_image']['meta']['detail_url'], 'http://localhost/admin/api/main/images/7/')
550
551        # Check that the child relations were serialised properly
552        self.assertEqual(content['related_links'], [])
553        for carousel_item in content['carousel_items']:
554            self.assertEqual(set(carousel_item.keys()), {'id', 'meta', 'embed_url', 'link', 'caption', 'image'})
555            self.assertEqual(set(carousel_item['meta'].keys()), {'type'})
556
557        # Check the type info
558        self.assertIsInstance(content['__types'], dict)
559        self.assertEqual(set(content['__types'].keys()), {
560            'wagtailcore.Page',
561            'demosite.HomePage',
562            'demosite.BlogIndexPage',
563            'demosite.BlogEntryPageCarouselItem',
564            'demosite.BlogEntryPage',
565            'wagtailimages.Image'
566        })
567        self.assertEqual(set(content['__types']['demosite.BlogIndexPage'].keys()), {'verbose_name', 'verbose_name_plural'})
568        self.assertEqual(content['__types']['demosite.BlogIndexPage']['verbose_name'], 'blog index page')
569        self.assertEqual(content['__types']['demosite.BlogIndexPage']['verbose_name_plural'], 'blog index pages')
570
571    # Overriden from public API tests
572    def test_meta_parent_id_doesnt_show_root_page(self):
573        # Root page is visible in the admin API
574        response = self.get_response(2)
575        content = json.loads(response.content.decode('UTF-8'))
576
577        self.assertIsNotNone(content['meta']['parent'])
578
579    def test_field_ordering(self):
580        # Need to override this as the admin API has a __types field
581
582        response = self.get_response(16)
583
584        # Will crash if the JSON is invalid
585        content = json.loads(response.content.decode('UTF-8'))
586
587        # Test field order
588        content = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(response.content.decode('UTF-8'))
589        field_order = [
590            'id',
591            'meta',
592            'title',
593            'admin_display_title',
594            'body',
595            'tags',
596            'date',
597            'feed_image',
598            'feed_image_thumbnail',
599            'carousel_items',
600            'related_links',
601            '__types',
602        ]
603        self.assertEqual(list(content.keys()), field_order)
604
605    def test_meta_status_draft(self):
606        # Unpublish the page
607        Page.objects.get(id=16).unpublish()
608
609        response = self.get_response(16)
610        content = json.loads(response.content.decode('UTF-8'))
611
612        self.assertIn('status', content['meta'])
613        self.assertEqual(content['meta']['status'], {
614            'status': 'draft',
615            'live': False,
616            'has_unpublished_changes': True
617        })
618
619    def test_meta_status_live_draft(self):
620        # Save revision without republish
621        Page.objects.get(id=16).save_revision()
622
623        response = self.get_response(16)
624        content = json.loads(response.content.decode('UTF-8'))
625
626        self.assertIn('status', content['meta'])
627        self.assertEqual(content['meta']['status'], {
628            'status': 'live + draft',
629            'live': True,
630            'has_unpublished_changes': True
631        })
632
633    def test_meta_status_scheduled(self):
634        # Unpublish and save revision with go live date in the future
635        Page.objects.get(id=16).unpublish()
636        tomorrow = timezone.now() + datetime.timedelta(days=1)
637        Page.objects.get(id=16).save_revision(approved_go_live_at=tomorrow)
638
639        response = self.get_response(16)
640        content = json.loads(response.content.decode('UTF-8'))
641
642        self.assertIn('status', content['meta'])
643        self.assertEqual(content['meta']['status'], {
644            'status': 'scheduled',
645            'live': False,
646            'has_unpublished_changes': True
647        })
648
649    def test_meta_status_expired(self):
650        # Unpublish and set expired flag
651        Page.objects.get(id=16).unpublish()
652        Page.objects.filter(id=16).update(expired=True)
653
654        response = self.get_response(16)
655        content = json.loads(response.content.decode('UTF-8'))
656
657        self.assertIn('status', content['meta'])
658        self.assertEqual(content['meta']['status'], {
659            'status': 'expired',
660            'live': False,
661            'has_unpublished_changes': True
662        })
663
664    def test_meta_children_for_parent(self):
665        # Homepage should have children
666        response = self.get_response(2)
667        content = json.loads(response.content.decode('UTF-8'))
668
669        self.assertIn('children', content['meta'])
670        self.assertEqual(content['meta']['children'], {
671            'count': 5,
672            'listing_url': 'http://localhost/admin/api/main/pages/?child_of=2'
673        })
674
675    def test_meta_descendants(self):
676        # Homepage should have children
677        response = self.get_response(2)
678        content = json.loads(response.content.decode('UTF-8'))
679
680        self.assertIn('descendants', content['meta'])
681        self.assertEqual(content['meta']['descendants'], {
682            'count': 18,
683            'listing_url': 'http://localhost/admin/api/main/pages/?descendant_of=2'
684        })
685
686    def test_meta_ancestors(self):
687        # Homepage should have children
688        response = self.get_response(16)
689        content = json.loads(response.content.decode('UTF-8'))
690
691        self.assertIn('ancestors', content['meta'])
692        self.assertIsInstance(content['meta']['ancestors'], list)
693        self.assertEqual(len(content['meta']['ancestors']), 3)
694        self.assertEqual(content['meta']['ancestors'][0].keys(), {'id', 'meta', 'title', 'admin_display_title'})
695        self.assertEqual(content['meta']['ancestors'][0]['title'], 'Root')
696        self.assertEqual(content['meta']['ancestors'][1]['title'], 'Home page')
697        self.assertEqual(content['meta']['ancestors'][2]['title'], 'Blog index')
698
699    # FIELDS
700
701    def test_remove_all_meta_fields(self):
702        response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-descendants,-latest_revision_created_at,-children,-ancestors,-show_in_menus,-seo_title,-parent,-status,-search_description')
703        content = json.loads(response.content.decode('UTF-8'))
704
705        self.assertNotIn('meta', set(content.keys()))
706        self.assertIn('id', set(content.keys()))
707
708    def test_remove_all_fields(self):
709        response = self.get_response(16, fields='_,id,type')
710        content = json.loads(response.content.decode('UTF-8'))
711
712        self.assertEqual(set(content.keys()), {'id', 'meta', '__types'})
713        self.assertEqual(set(content['meta'].keys()), {'type'})
714
715    def test_all_nested_fields(self):
716        response = self.get_response(16, fields='feed_image(*)')
717        content = json.loads(response.content.decode('UTF-8'))
718
719        self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
720
721    def test_fields_foreign_key(self):
722        response = self.get_response(16)
723        content = json.loads(response.content.decode('UTF-8'))
724
725        feed_image = content['feed_image']
726
727        self.assertIsInstance(feed_image, dict)
728        self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
729        self.assertIsInstance(feed_image['id'], int)
730        self.assertIsInstance(feed_image['meta'], dict)
731        self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url', 'download_url'})
732        self.assertEqual(feed_image['meta']['type'], 'wagtailimages.Image')
733        self.assertEqual(feed_image['meta']['detail_url'], 'http://localhost/admin/api/main/images/%d/' % feed_image['id'])
734
735
736class TestAdminPageDetailWithStreamField(AdminAPITestCase):
737    fixtures = ['test.json']
738
739    def setUp(self):
740        super().setUp()
741
742        self.homepage = Page.objects.get(url_path='/home/')
743
744    def make_stream_page(self, body):
745        stream_page = StreamPage(
746            title='stream page',
747            slug='stream-page',
748            body=body
749        )
750        return self.homepage.add_child(instance=stream_page)
751
752    def test_can_fetch_streamfield_content(self):
753        stream_page = self.make_stream_page('[{"type": "text", "value": "foo"}]')
754
755        response_url = reverse('wagtailadmin_api:pages:detail', args=(stream_page.id, ))
756        response = self.client.get(response_url)
757
758        self.assertEqual(response.status_code, 200)
759        self.assertEqual(response['content-type'], 'application/json')
760
761        content = json.loads(response.content.decode('utf-8'))
762
763        self.assertIn('id', content)
764        self.assertEqual(content['id'], stream_page.id)
765        self.assertIn('body', content)
766        self.assertEqual(len(content['body']), 1)
767        self.assertEqual(content['body'][0]['type'], 'text')
768        self.assertEqual(content['body'][0]['value'], 'foo')
769        self.assertTrue(content['body'][0]['id'])
770
771    def test_image_block(self):
772        stream_page = self.make_stream_page('[{"type": "image", "value": 1}]')
773
774        response_url = reverse('wagtailadmin_api:pages:detail', args=(stream_page.id, ))
775        response = self.client.get(response_url)
776        content = json.loads(response.content.decode('utf-8'))
777
778        # ForeignKeys in a StreamField shouldn't be translated into dictionary representation
779        self.assertEqual(content['body'][0]['type'], 'image')
780        self.assertEqual(content['body'][0]['value'], 1)
781
782
783class TestCustomAdminDisplayTitle(AdminAPITestCase):
784    fixtures = ['test.json']
785
786    def setUp(self):
787        super().setUp()
788
789        self.event_page = Page.objects.get(url_path='/home/events/saint-patrick/')
790
791    def test_custom_admin_display_title_shown_on_detail_page(self):
792        api_url = reverse('wagtailadmin_api:pages:detail', args=(self.event_page.id, ))
793        response = self.client.get(api_url)
794        content = json.loads(response.content.decode('utf-8'))
795
796        self.assertEqual(content['title'], "Saint Patrick")
797        self.assertEqual(content['admin_display_title'], "Saint Patrick (single event)")
798
799    def test_custom_admin_display_title_shown_on_listing(self):
800        api_url = reverse('wagtailadmin_api:pages:listing')
801        response = self.client.get(api_url)
802        content = json.loads(response.content.decode('utf-8'))
803
804        matching_items = [item for item in content['items'] if item['id'] == self.event_page.id]
805        self.assertEqual(1, len(matching_items))
806        self.assertEqual(matching_items[0]['title'], "Saint Patrick")
807        self.assertEqual(matching_items[0]['admin_display_title'], "Saint Patrick (single event)")
808
809
810# Overwrite imported test cases do Django doesn't run them
811TestPageDetail = None
812TestPageListing = None
813