Initial work on the Admin API (#2507)

This commit is contained in:
Karl Hobley 2016-07-13 11:26:43 +01:00 committed by Mikalai Radchuk
parent bdd80dd33e
commit 2f8d00222d
21 changed files with 1112 additions and 2 deletions

View file

@ -54,6 +54,15 @@ class BaseAPIEndpoint(GenericViewSet):
default_fields = []
name = None # Set on subclass.
def __init__(self, *args, **kwargs):
super(BaseAPIEndpoint, self).__init__(*args, **kwargs)
# seen_types is a mapping of type name strings (format: "app_label.ModelName")
# to model classes. When an object is serialised in the API, its model
# is added to this mapping. This is used by the Admin API which appends a
# summary of the used types to the response.
self.seen_types = OrderedDict()
def get_queryset(self):
return self.model.objects.all().order_by('id')
@ -187,6 +196,15 @@ class BaseAPIEndpoint(GenericViewSet):
url(r'^(?P<pk>\d+)/$', cls.as_view({'get': 'detail_view'}), name='detail'),
]
@classmethod
def get_model_listing_urlpath(cls, model, namespace=''):
if namespace:
url_name = namespace + ':listing'
else:
url_name = 'listing'
return reverse(url_name)
@classmethod
def get_object_detail_urlpath(cls, model, pk, namespace=''):
if namespace:

View file

@ -30,6 +30,20 @@ class WagtailAPIRouter(object):
if issubclass(model, class_.model):
return name, class_
def get_model_listing_urlpath(self, model):
"""
Returns a URL path (excluding scheme and hostname) to the listing
page of a model
Returns None if the model is not represented by any endpoints.
"""
endpoint = self.get_model_endpoint(model)
if endpoint:
endpoint_name, endpoint_class = endpoint[0], endpoint[1]
url_namespace = self.url_namespace + ':' + endpoint_name
return endpoint_class.get_model_listing_urlpath(model, namespace=url_namespace)
def get_object_detail_urlpath(self, model, pk):
"""
Returns a URL path (excluding scheme and hostname) to the detail

View file

@ -40,7 +40,9 @@ class TypeField(Field):
return instance
def to_representation(self, obj):
return type(obj)._meta.app_label + '.' + type(obj).__name__
name = type(obj)._meta.app_label + '.' + type(obj).__name__
self.context['view'].seen_types[name] = type(obj)
return name
class DetailUrlField(Field):
@ -95,7 +97,9 @@ class PageTypeField(Field):
return instance
def to_representation(self, page):
return page.specific_class._meta.app_label + '.' + page.specific_class.__name__
name = page.specific_class._meta.app_label + '.' + page.specific_class.__name__
self.context['view'].seen_types[name] = page.specific_class
return name
class RelatedField(relations.RelatedField):

View file

View file

@ -0,0 +1,92 @@
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from wagtail.api.v2.endpoints import PagesAPIEndpoint
from wagtail.api.v2.filters import (
ChildOfFilter, DescendantOfFilter, FieldsFilter, OrderingFilter, SearchFilter)
from wagtail.api.v2.utils import BadRequestError, filter_page_type, page_models_from_string
from wagtail.wagtailcore.models import Page
from .filters import HasChildrenFilter
from .serializers import AdminPageSerializer
class PagesAdminAPIEndpoint(PagesAPIEndpoint):
base_serializer_class = AdminPageSerializer
# Use unrestricted child_of/descendant_of filters
# Add has_children filter
filter_backends = [
FieldsFilter,
ChildOfFilter,
DescendantOfFilter,
HasChildrenFilter,
OrderingFilter,
SearchFilter,
]
extra_meta_fields = PagesAPIEndpoint.extra_meta_fields + [
'latest_revision_created_at',
'status',
'children',
'descendants',
'parent',
]
default_fields = PagesAPIEndpoint.default_fields + [
'latest_revision_created_at',
'status',
'children',
]
known_query_parameters = PagesAPIEndpoint.known_query_parameters.union([
'has_children'
])
def get_queryset(self):
request = self.request
# Allow pages to be filtered to a specific type
try:
models = page_models_from_string(request.GET.get('type', 'wagtailcore.Page'))
except (LookupError, ValueError):
raise BadRequestError("type doesn't exist")
if not models:
models = [Page]
if len(models) == 1:
queryset = models[0].objects.all()
else:
queryset = Page.objects.all()
# Filter pages by specified models
queryset = filter_page_type(queryset, models)
# Hide root page
# TODO: Add "include_root" flag
queryset = queryset.exclude(depth=1)
return queryset
def get_type_info(self):
types = OrderedDict()
for name, model in self.seen_types.items():
types[name] = OrderedDict([
('verbose_name', model._meta.verbose_name),
('verbose_name_plural', model._meta.verbose_name_plural),
])
return types
def listing_view(self, request):
response = super(PagesAdminAPIEndpoint, self).listing_view(request)
response.data['__types'] = self.get_type_info()
return response
def detail_view(self, request, pk):
response = super(PagesAdminAPIEndpoint, self).detail_view(request, pk)
response.data['__types'] = self.get_type_info()
return response

View file

@ -0,0 +1,26 @@
from __future__ import absolute_import, unicode_literals
from rest_framework.filters import BaseFilterBackend
from wagtail.api.v2.utils import BadRequestError
class HasChildrenFilter(BaseFilterBackend):
"""
Filters the queryset by checking if the pages have children or not.
This is useful when you want to get just the branches or just the leaves.
"""
def filter_queryset(self, request, queryset, view):
if 'has_children' in request.GET:
try:
has_children_filter = int(request.GET['has_children'])
assert has_children_filter is 1 or has_children_filter is 0
except (ValueError, AssertionError):
raise BadRequestError("has_children must be 1 or 0")
if has_children_filter == 1:
return queryset.filter(numchild__gt=0)
else:
return queryset.filter(numchild=0)
return queryset

View file

@ -0,0 +1,90 @@
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from rest_framework.fields import Field
from wagtail.api.v2.serializers import PageSerializer
from wagtail.api.v2.utils import get_full_url
from wagtail.wagtailcore.models import Page
def get_model_listing_url(context, model):
url_path = context['router'].get_model_listing_urlpath(model)
if url_path:
return get_full_url(context['request'], url_path)
class PageStatusField(Field):
"""
Serializes the "status" field.
Example:
"status": {
"status": "live",
"live": true,
"has_unpublished_changes": false
},
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
return OrderedDict([
('status', page.status_string),
('live', page.live),
('has_unpublished_changes', page.has_unpublished_changes),
])
class PageChildrenField(Field):
"""
Serializes the "children" field.
Example:
"children": {
"count": 1,
"listing_url": "/api/v1/pages/?child_of=2"
}
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
return OrderedDict([
('count', page.numchild),
('listing_url', get_model_listing_url(self.context, Page) + '?child_of=' + str(page.id)),
])
class PageDescendantsField(Field):
"""
Serializes the "descendants" field.
Example:
"descendants": {
"count": 10,
"listing_url": "/api/v1/pages/?descendant_of=2"
}
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
return OrderedDict([
('count', page.get_descendants().count()),
('listing_url', get_model_listing_url(self.context, Page) + '?descendant_of=' + str(page.id)),
])
class AdminPageSerializer(PageSerializer):
status = PageStatusField(read_only=True)
children = PageChildrenField(read_only=True)
descendants = PageDescendantsField(read_only=True)
meta_fields = PageSerializer.meta_fields + [
'status',
'children',
'descendants',
]

View file

@ -0,0 +1,18 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.wagtailcore import hooks
from .endpoints import PagesAdminAPIEndpoint
admin_api = WagtailAPIRouter('wagtailadmin_api_v1')
admin_api.register_endpoint('pages', PagesAdminAPIEndpoint)
for fn in hooks.get_hooks('construct_admin_api'):
fn(admin_api)
urlpatterns = [
url(r'^v2beta/', admin_api.urls),
]

View file

@ -0,0 +1,115 @@
from __future__ import absolute_import, unicode_literals
import json
from django.core.urlresolvers import reverse
from wagtail.api.v2.tests.test_documents import TestDocumentDetail, TestDocumentListing
from wagtail.wagtaildocs.models import Document
from .utils import AdminAPITestCase
class TestAdminDocumentListing(AdminAPITestCase, TestDocumentListing):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailadmin_api_v1:documents:listing'), params)
def get_document_id_list(self, content):
return [document['id'] for document in content['items']]
# BASIC TESTS
def test_basic(self):
response = self.get_response()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check that the total count is there and correct
self.assertIn('total_count', content['meta'])
self.assertIsInstance(content['meta']['total_count'], int)
self.assertEqual(content['meta']['total_count'], Document.objects.count())
# Check that the items section is there
self.assertIn('items', content)
self.assertIsInstance(content['items'], list)
# Check that each document has a meta section with type, detail_url and tags attributes
for document in content['items']:
self.assertIn('meta', document)
self.assertIsInstance(document['meta'], dict)
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url', 'download_url', 'tags'})
# Type should always be wagtaildocs.Document
self.assertEqual(document['meta']['type'], 'wagtaildocs.Document')
# Check detail_url
self.assertEqual(document['meta']['detail_url'], 'http://localhost/admin/api/v2beta/documents/%d/' % document['id'])
# Check download_url
self.assertTrue(document['meta']['download_url'].startswith('http://localhost/documents/%d/' % document['id']))
# FIELDS
def test_fields_default(self):
response = self.get_response()
content = json.loads(response.content.decode('UTF-8'))
for document in content['items']:
self.assertEqual(set(document.keys()), {'id', 'meta', 'title'})
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url', 'download_url', 'tags'})
class TestAdminDocumentDetail(AdminAPITestCase, TestDocumentDetail):
fixtures = ['demosite.json']
def get_response(self, image_id, **params):
return self.client.get(reverse('wagtailadmin_api_v1:documents:detail', args=(image_id, )), params)
def test_basic(self):
response = self.get_response(1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check the id field
self.assertIn('id', content)
self.assertEqual(content['id'], 1)
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check the meta type
self.assertIn('type', content['meta'])
self.assertEqual(content['meta']['type'], 'wagtaildocs.Document')
# Check the meta detail_url
self.assertIn('detail_url', content['meta'])
self.assertEqual(content['meta']['detail_url'], 'http://localhost/admin/api/v2beta/documents/1/')
# Check the meta download_url
self.assertIn('download_url', content['meta'])
self.assertEqual(content['meta']['download_url'], 'http://localhost/documents/1/wagtail_by_markyharky.jpg')
# Check the title field
self.assertIn('title', content)
self.assertEqual(content['title'], "Wagtail by mark Harkin")
# Check the tags field
self.assertIn('tags', content['meta'])
self.assertEqual(content['meta']['tags'], [])

View file

@ -0,0 +1,143 @@
from __future__ import absolute_import, unicode_literals
import json
from django.core.urlresolvers import reverse
from wagtail.api.v2.tests.test_images import TestImageDetail, TestImageListing
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.tests.utils import get_test_image_file
from .utils import AdminAPITestCase
class TestAdminImageListing(AdminAPITestCase, TestImageListing):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailadmin_api_v1:images:listing'), params)
def get_image_id_list(self, content):
return [image['id'] for image in content['items']]
# BASIC TESTS
def test_basic(self):
response = self.get_response()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check that the total count is there and correct
self.assertIn('total_count', content['meta'])
self.assertIsInstance(content['meta']['total_count'], int)
self.assertEqual(content['meta']['total_count'], get_image_model().objects.count())
# Check that the items section is there
self.assertIn('items', content)
self.assertIsInstance(content['items'], list)
# Check that each image has a meta section with type, detail_url and tags attributes
for image in content['items']:
self.assertIn('meta', image)
self.assertIsInstance(image['meta'], dict)
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
# Type should always be wagtailimages.Image
self.assertEqual(image['meta']['type'], 'wagtailimages.Image')
# Check detail url
self.assertEqual(image['meta']['detail_url'], 'http://localhost/admin/api/v2beta/images/%d/' % image['id'])
# FIELDS
def test_fields_default(self):
response = self.get_response()
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
class TestAdminImageDetail(AdminAPITestCase, TestImageDetail):
fixtures = ['demosite.json']
def get_response(self, image_id, **params):
return self.client.get(reverse('wagtailadmin_api_v1:images:detail', args=(image_id, )), params)
def test_basic(self):
response = self.get_response(5)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check the id field
self.assertIn('id', content)
self.assertEqual(content['id'], 5)
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check the meta type
self.assertIn('type', content['meta'])
self.assertEqual(content['meta']['type'], 'wagtailimages.Image')
# Check the meta detail_url
self.assertIn('detail_url', content['meta'])
self.assertEqual(content['meta']['detail_url'], 'http://localhost/admin/api/v2beta/images/5/')
# Check the thumbnail
# Note: This is None because the source image doesn't exist
# See test_thumbnail below for working example
self.assertIn('thumbnail', content)
self.assertEqual(content['thumbnail'], {'error': 'SourceImageIOError'})
# Check the title field
self.assertIn('title', content)
self.assertEqual(content['title'], "James Joyce")
# Check the width and height fields
self.assertIn('width', content)
self.assertIn('height', content)
self.assertEqual(content['width'], 500)
self.assertEqual(content['height'], 392)
# Check the tags field
self.assertIn('tags', content['meta'])
self.assertEqual(content['meta']['tags'], [])
def test_thumbnail(self):
# Add a new image with source file
image = get_image_model().objects.create(
title="Test image",
file=get_test_image_file(),
)
response = self.get_response(image.id)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('thumbnail', content)
self.assertEqual(content['thumbnail'], {
'url': '/media/images/test.max-165x165.png',
'width': 165,
'height': 123
})
# Check that source_image_error didn't appear
self.assertNotIn('source_image_error', content['meta'])

View file

@ -0,0 +1,487 @@
from __future__ import absolute_import, unicode_literals
import collections
import datetime
import json
from django.core.urlresolvers import reverse
from django.utils import timezone
from wagtail.api.v2.tests.test_pages import TestPageDetail, TestPageListing
from wagtail.tests.demosite import models
from wagtail.tests.testapp.models import StreamPage
from wagtail.wagtailcore.models import Page
from .utils import AdminAPITestCase
def get_total_page_count():
# Need to take away 1 as the root page is invisible over the API by default
return Page.objects.count() - 1
class TestAdminPageListing(AdminAPITestCase, TestPageListing):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailadmin_api_v1:pages:listing'), params)
def get_page_id_list(self, content):
return [page['id'] for page in content['items']]
# BASIC TESTS
def test_basic(self):
response = self.get_response()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check that the total count is there and correct
self.assertIn('total_count', content['meta'])
self.assertIsInstance(content['meta']['total_count'], int)
self.assertEqual(content['meta']['total_count'], get_total_page_count())
# Check that the items section is there
self.assertIn('items', content)
self.assertIsInstance(content['items'], list)
# Check that each page has a meta section with type, detail_url, html_url, status and children attributes
for page in content['items']:
self.assertIn('meta', page)
self.assertIsInstance(page['meta'], dict)
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'html_url', 'status', 'children', 'slug', 'first_published_at', 'latest_revision_created_at'})
# Check the type info
self.assertIsInstance(content['__types'], dict)
self.assertEqual(set(content['__types'].keys()), {
'demosite.EventPage',
'demosite.StandardIndexPage',
'demosite.PersonPage',
'demosite.HomePage',
'demosite.StandardPage',
'demosite.EventIndexPage',
'demosite.ContactPage',
'demosite.BlogEntryPage',
'demosite.BlogIndexPage',
})
self.assertEqual(set(content['__types']['demosite.EventPage'].keys()), {'verbose_name', 'verbose_name_plural'})
self.assertEqual(content['__types']['demosite.EventPage']['verbose_name'], 'event page')
self.assertEqual(content['__types']['demosite.EventPage']['verbose_name_plural'], 'event pages')
# Not applicable to the admin API
test_unpublished_pages_dont_appear_in_list = None
test_private_pages_dont_appear_in_list = None
def test_unpublished_pages_appear_in_list(self):
total_count = get_total_page_count()
page = models.BlogEntryPage.objects.get(id=16)
page.unpublish()
response = self.get_response()
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content['meta']['total_count'], total_count)
def test_private_pages_appear_in_list(self):
total_count = get_total_page_count()
page = models.BlogIndexPage.objects.get(id=5)
page.view_restrictions.create(password='test')
new_total_count = get_total_page_count()
self.assertEqual(total_count, total_count)
response = self.get_response()
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content['meta']['total_count'], new_total_count)
# FIELDS
def test_fields_default(self):
response = self.get_response(type='demosite.BlogEntryPage')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'title'})
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'html_url', 'children', 'status', 'slug', 'first_published_at', 'latest_revision_created_at'})
def test_fields_foreign_key(self):
# Only the base the detail_url is different here from the public API
response = self.get_response(type='demosite.BlogEntryPage', fields='title,date,feed_image')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
feed_image = page['feed_image']
if feed_image is not None:
self.assertIsInstance(feed_image, dict)
self.assertEqual(set(feed_image.keys()), {'id', 'meta'})
self.assertIsInstance(feed_image['id'], int)
self.assertIsInstance(feed_image['meta'], dict)
self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url'})
self.assertEqual(feed_image['meta']['type'], 'wagtailimages.Image')
self.assertEqual(feed_image['meta']['detail_url'], 'http://localhost/admin/api/v2beta/images/%d/' % feed_image['id'])
def test_fields_parent(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='parent')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
parent = page['meta']['parent']
# All blog entry pages have the same parent
self.assertEqual(parent, {
'id': 5,
'meta': {
'type': 'demosite.BlogIndexPage',
'detail_url': 'http://localhost/admin/api/v2beta/pages/5/',
'html_url': 'http://localhost/blog-index/',
}
})
def test_fields_descendants(self):
response = self.get_response(fields='descendants')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
descendants = page['meta']['descendants']
self.assertEqual(set(descendants.keys()), {'count', 'listing_url'})
self.assertIsInstance(descendants['count'], int)
self.assertEqual(descendants['listing_url'], 'http://localhost/admin/api/v2beta/pages/?descendant_of=%d' % page['id'])
# CHILD OF FILTER
# Not applicable to the admin API
test_child_of_page_thats_not_in_same_site_gives_error = None
def test_child_of_root(self):
# Only return the homepage as that's the only child of the "root" node
# in the tree. This is different to the public API which pretends the
# homepage of the current site is the root page.
response = self.get_response(child_of='root')
content = json.loads(response.content.decode('UTF-8'))
page_id_list = self.get_page_id_list(content)
self.assertEqual(page_id_list, [2])
def test_child_of_page_1(self):
# Public API doesn't allow this, as it's the root page
response = self.get_response(child_of=1)
json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 200)
# DESCENDANT OF FILTER
# Not applicable to the admin API
test_descendant_of_page_thats_not_in_same_site_gives_error = None
def test_descendant_of_root(self):
response = self.get_response(descendant_of='root')
content = json.loads(response.content.decode('UTF-8'))
page_id_list = self.get_page_id_list(content)
self.assertEqual(page_id_list, [2, 4, 8, 9, 5, 16, 18, 19, 6, 10, 15, 17, 21, 22, 23, 20, 13, 14, 12])
def test_descendant_of_root_doesnt_give_error(self):
# Public API doesn't allow this
response = self.get_response(descendant_of=1)
json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 200)
# HAS CHILDREN FILTER
def test_has_children_filter(self):
response = self.get_response(has_children=1)
content = json.loads(response.content.decode('UTF-8'))
page_id_list = self.get_page_id_list(content)
self.assertEqual(page_id_list, [2, 4, 5, 6, 21, 20])
def test_has_children_filter_off(self):
response = self.get_response(has_children=0)
content = json.loads(response.content.decode('UTF-8'))
page_id_list = self.get_page_id_list(content)
self.assertEqual(page_id_list, [8, 9, 16, 18, 19, 10, 15, 17, 22, 23, 13, 14, 12])
def test_has_children_filter_invalid_integer(self):
response = self.get_response(has_children=3)
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "has_children must be 1 or 0"})
def test_has_children_filter_invalid_value(self):
response = self.get_response(has_children='yes')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "has_children must be 1 or 0"})
class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
fixtures = ['demosite.json']
def get_response(self, page_id, **params):
return self.client.get(reverse('wagtailadmin_api_v1:pages:detail', args=(page_id, )), params)
def test_basic(self):
response = self.get_response(16)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Check the id field
self.assertIn('id', content)
self.assertEqual(content['id'], 16)
# Check that the meta section is there
self.assertIn('meta', content)
self.assertIsInstance(content['meta'], dict)
# Check the meta type
self.assertIn('type', content['meta'])
self.assertEqual(content['meta']['type'], 'demosite.BlogEntryPage')
# Check the meta detail_url
self.assertIn('detail_url', content['meta'])
self.assertEqual(content['meta']['detail_url'], 'http://localhost/admin/api/v2beta/pages/16/')
# Check the meta html_url
self.assertIn('html_url', content['meta'])
self.assertEqual(content['meta']['html_url'], 'http://localhost/blog-index/blog-post/')
# Check the meta status
self.assertIn('status', content['meta'])
self.assertEqual(content['meta']['status'], {
'status': 'live',
'live': True,
'has_unpublished_changes': False
})
# Check the meta children
self.assertIn('children', content['meta'])
self.assertEqual(content['meta']['children'], {
'count': 0,
'listing_url': 'http://localhost/admin/api/v2beta/pages/?child_of=16'
})
# Check the parent field
self.assertIn('parent', content['meta'])
self.assertIsInstance(content['meta']['parent'], dict)
self.assertEqual(set(content['meta']['parent'].keys()), {'id', 'meta'})
self.assertEqual(content['meta']['parent']['id'], 5)
self.assertIsInstance(content['meta']['parent']['meta'], dict)
self.assertEqual(set(content['meta']['parent']['meta'].keys()), {'type', 'detail_url', 'html_url'})
self.assertEqual(content['meta']['parent']['meta']['type'], 'demosite.BlogIndexPage')
self.assertEqual(content['meta']['parent']['meta']['detail_url'], 'http://localhost/admin/api/v2beta/pages/5/')
self.assertEqual(content['meta']['parent']['meta']['html_url'], 'http://localhost/blog-index/')
# Check that the custom fields are included
self.assertIn('date', content)
self.assertIn('body', content)
self.assertIn('tags', content)
self.assertIn('feed_image', content)
self.assertIn('related_links', content)
self.assertIn('carousel_items', content)
# Check that the date was serialised properly
self.assertEqual(content['date'], '2013-12-02')
# Check that the tags were serialised properly
self.assertEqual(content['tags'], ['bird', 'wagtail'])
# Check that the feed image was serialised properly
self.assertIsInstance(content['feed_image'], dict)
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta'})
self.assertEqual(content['feed_image']['id'], 7)
self.assertIsInstance(content['feed_image']['meta'], dict)
self.assertEqual(set(content['feed_image']['meta'].keys()), {'type', 'detail_url'})
self.assertEqual(content['feed_image']['meta']['type'], 'wagtailimages.Image')
self.assertEqual(content['feed_image']['meta']['detail_url'], 'http://localhost/admin/api/v2beta/images/7/')
# Check that the child relations were serialised properly
self.assertEqual(content['related_links'], [])
for carousel_item in content['carousel_items']:
self.assertEqual(set(carousel_item.keys()), {'id', 'meta', 'embed_url', 'link', 'caption', 'image'})
self.assertEqual(set(carousel_item['meta'].keys()), {'type'})
# Check the type info
self.assertIsInstance(content['__types'], dict)
self.assertEqual(set(content['__types'].keys()), {
'demosite.BlogIndexPage',
'demosite.BlogEntryPageCarouselItem',
'demosite.BlogEntryPage',
'wagtailimages.Image'
})
self.assertEqual(set(content['__types']['demosite.BlogIndexPage'].keys()), {'verbose_name', 'verbose_name_plural'})
self.assertEqual(content['__types']['demosite.BlogIndexPage']['verbose_name'], 'blog index page')
self.assertEqual(content['__types']['demosite.BlogIndexPage']['verbose_name_plural'], 'blog index pages')
def test_field_ordering(self):
# Need to override this as the admin API has a __types field
response = self.get_response(16)
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
# Test field order
content = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(response.content.decode('UTF-8'))
field_order = [
'id',
'meta',
'title',
'body',
'tags',
'date',
'feed_image',
'carousel_items',
'related_links',
'__types',
]
self.assertEqual(list(content.keys()), field_order)
def test_meta_status_draft(self):
# Unpublish the page
Page.objects.get(id=16).unpublish()
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('status', content['meta'])
self.assertEqual(content['meta']['status'], {
'status': 'draft',
'live': False,
'has_unpublished_changes': True
})
def test_meta_status_live_draft(self):
# Save revision without republish
Page.objects.get(id=16).save_revision()
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('status', content['meta'])
self.assertEqual(content['meta']['status'], {
'status': 'live + draft',
'live': True,
'has_unpublished_changes': True
})
def test_meta_status_scheduled(self):
# Unpublish and save revision with go live date in the future
Page.objects.get(id=16).unpublish()
tomorrow = timezone.now() + datetime.timedelta(days=1)
Page.objects.get(id=16).save_revision(approved_go_live_at=tomorrow)
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('status', content['meta'])
self.assertEqual(content['meta']['status'], {
'status': 'scheduled',
'live': False,
'has_unpublished_changes': True
})
def test_meta_status_expired(self):
# Unpublish and set expired flag
Page.objects.get(id=16).unpublish()
Page.objects.filter(id=16).update(expired=True)
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('status', content['meta'])
self.assertEqual(content['meta']['status'], {
'status': 'expired',
'live': False,
'has_unpublished_changes': True
})
def test_meta_children_for_parent(self):
# Homepage should have children
response = self.get_response(2)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('children', content['meta'])
self.assertEqual(content['meta']['children'], {
'count': 5,
'listing_url': 'http://localhost/admin/api/v2beta/pages/?child_of=2'
})
def test_meta_descendants(self):
# Homepage should have children
response = self.get_response(2)
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('descendants', content['meta'])
self.assertEqual(content['meta']['descendants'], {
'count': 18,
'listing_url': 'http://localhost/admin/api/v2beta/pages/?descendant_of=2'
})
class TestAdminPageDetailWithStreamField(AdminAPITestCase):
fixtures = ['test.json']
def setUp(self):
super(TestAdminPageDetailWithStreamField, self).setUp()
self.homepage = Page.objects.get(url_path='/home/')
def make_stream_page(self, body):
stream_page = StreamPage(
title='stream page',
slug='stream-page',
body=body
)
return self.homepage.add_child(instance=stream_page)
def test_can_fetch_streamfield_content(self):
stream_page = self.make_stream_page('[{"type": "text", "value": "foo"}]')
response_url = reverse('wagtailadmin_api_v1:pages:detail', args=(stream_page.id, ))
response = self.client.get(response_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-type'], 'application/json')
content = json.loads(response.content.decode('utf-8'))
self.assertIn('id', content)
self.assertEqual(content['id'], stream_page.id)
self.assertIn('body', content)
self.assertEqual(content['body'], [{'type': 'text', 'value': 'foo'}])
def test_image_block(self):
stream_page = self.make_stream_page('[{"type": "image", "value": 1}]')
response_url = reverse('wagtailadmin_api_v1:pages:detail', args=(stream_page.id, ))
response = self.client.get(response_url)
content = json.loads(response.content.decode('utf-8'))
# ForeignKeys in a StreamField shouldn't be translated into dictionary representation
self.assertEqual(content['body'], [{'type': 'image', 'value': 1}])

View file

@ -0,0 +1,10 @@
from __future__ import absolute_import, unicode_literals
from django.test import TestCase
from wagtail.tests.utils import WagtailTestUtils
class AdminAPITestCase(TestCase, WagtailTestUtils):
def setUp(self):
self.login()

View file

@ -5,6 +5,7 @@ from wagtail.wagtailadmin.urls import pages as wagtailadmin_pages_urls
from wagtail.wagtailadmin.urls import collections as wagtailadmin_collections_urls
from wagtail.wagtailadmin.urls import password_reset as wagtailadmin_password_reset_urls
from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar
from wagtail.wagtailadmin.api import urls as api_urls
from wagtail.wagtailcore import hooks
from wagtail.utils.urlpatterns import decorate_urlpatterns
from wagtail.wagtailadmin.decorators import require_admin_access
@ -13,6 +14,8 @@ from wagtail.wagtailadmin.decorators import require_admin_access
urlpatterns = [
url(r'^$', home.home, name='wagtailadmin_home'),
url(r'api/', include(api_urls)),
url(r'^failwhale/$', home.error_test, name='wagtailadmin_error_test'),
url(r'^explorer-nav/$', pages.explorer_nav, name='wagtailadmin_explorer_nav'),

View file

@ -0,0 +1,7 @@
from __future__ import absolute_import, unicode_literals
from ..v2.endpoints import DocumentsAPIEndpoint
class DocumentsAdminAPIEndpoint(DocumentsAPIEndpoint):
pass

View file

@ -12,6 +12,7 @@ from wagtail.wagtailadmin.search import SearchArea
from wagtail.wagtailadmin.site_summary import SummaryItem
from wagtail.wagtailcore import hooks
from wagtail.wagtaildocs import admin_urls
from wagtail.wagtaildocs.api.admin.endpoints import DocumentsAdminAPIEndpoint
from wagtail.wagtaildocs.forms import GroupDocumentPermissionFormSet
from wagtail.wagtaildocs.models import get_document_model
from wagtail.wagtaildocs.permissions import permission_policy
@ -25,6 +26,11 @@ def register_admin_urls():
]
@hooks.register('construct_admin_api')
def construct_admin_api(router):
router.register_endpoint('documents', DocumentsAdminAPIEndpoint)
class DocumentsMenuItem(MenuItem):
def is_shown(self, request):
return permission_policy.user_has_any_permission(

View file

@ -0,0 +1,18 @@
from __future__ import absolute_import, unicode_literals
from ..v2.endpoints import ImagesAPIEndpoint
from .serializers import AdminImageSerializer
class ImagesAdminAPIEndpoint(ImagesAPIEndpoint):
base_serializer_class = AdminImageSerializer
extra_body_fields = ImagesAPIEndpoint.extra_body_fields + [
'thumbnail',
]
default_fields = ImagesAPIEndpoint.default_fields + [
'width',
'height',
'thumbnail',
]

View file

@ -0,0 +1,53 @@
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from rest_framework.fields import Field
from ...models import SourceImageIOError
from ..v2.serializers import ImageSerializer
class ImageRenditionField(Field):
"""
A field that generates a rendition with the specified filter spec, and serialises
details of that rendition.
Example:
"thumbnail": {
"url": "/media/images/myimage.max-165x165.jpg",
"width": 165,
"height": 100
}
If there is an error with the source image. The dict will only contain a single
key, "error", indicating this error:
"thumbnail": {
"error": "SourceImageIOError"
}
"""
def __init__(self, filter_spec, *args, **kwargs):
self.filter_spec = filter_spec
super(ImageRenditionField, self).__init__(*args, **kwargs)
def get_attribute(self, instance):
return instance
def to_representation(self, image):
try:
thumbnail = image.get_rendition(self.filter_spec)
return OrderedDict([
('url', thumbnail.url),
('width', thumbnail.width),
('height', thumbnail.height),
])
except SourceImageIOError:
return OrderedDict([
('error', 'SourceImageIOError'),
])
class AdminImageSerializer(ImageSerializer):
thumbnail = ImageRenditionField('max-165x165', read_only=True)

View file

@ -12,6 +12,7 @@ from wagtail.wagtailadmin.search import SearchArea
from wagtail.wagtailadmin.site_summary import SummaryItem
from wagtail.wagtailcore import hooks
from wagtail.wagtailimages import admin_urls, image_operations
from wagtail.wagtailimages.api.admin.endpoints import ImagesAdminAPIEndpoint
from wagtail.wagtailimages.forms import GroupImagePermissionFormSet
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.permissions import permission_policy
@ -25,6 +26,11 @@ def register_admin_urls():
]
@hooks.register('construct_admin_api')
def construct_admin_api(router):
router.register_endpoint('images', ImagesAdminAPIEndpoint)
class ImagesMenuItem(MenuItem):
def is_shown(self, request):
return permission_policy.user_has_any_permission(