Skip to content

Commit

Permalink
Initial work on the Admin API (#2485)
Browse files Browse the repository at this point in the history
* feat(admin/api): Added Admin API

* test(admin/api): Tests for admin API

* feat(admin/api): Show all pages in admin API (except root)

* refactor(admin/api): Restructure to match wagtail.api module

* feat(admin/api): Added "status" to pages

* feat(admin/api): Added "has_children" to pages

* feat(admin/api): Added thumbnail to images

* doc(admin/api): Fixes to docstring

* feat(admin/api): Changed representation of page meta status

Now includes live and has_unpublished_changes split out into separate fields

* feat(api): Added get_model_listing_urlpath to router

* feat(admin/api): Replaced page has_children with children

This allows us to return the count and listing_url of the child pages

* feat(admin/api): Bump API version to v2

Since the API is based on WagtailAPI v2. This should hopefully reduce risk of confusion.

There won't be an Admin API v1.

* Fixed some import paths

* Added extra fields into tests

* Updated admin API tests for items rename

* Revert "feat(api/2): Removed meta from top of listings"

This reverts commit fb61033.

* Rewrite admin meta fields

* Fixed admin api tests post rebase

* Control which fields are shown in the admin API

User defined api_fields no longer affect the admin API

* Use unrestricted child_of/descendant_of filters

* Revert "Control which fields are shown in the admin API"

This reverts commit 90a6758.

* Updates to admin API for compatibility with changes to api v2 meta field handling

* Added type information to admin API

* Added latest_revision_created_at field

* Added has_children filter

* Add parent field

* Removed duplicate tests between admin api and public api

* isort
  • Loading branch information
kaedroho committed Apr 17, 2016
1 parent 82fa77d commit 3282fdb
Show file tree
Hide file tree
Showing 13 changed files with 1,019 additions and 2 deletions.
9 changes: 9 additions & 0 deletions wagtail/api/v2/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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')

Expand Down
8 changes: 6 additions & 2 deletions wagtail/api/v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ def get_attribute(self, instance):
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):
Expand Down Expand Up @@ -95,7 +97,9 @@ def get_attribute(self, instance):
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 DocumentDownloadUrlField(Field):
Expand Down
Empty file.
110 changes: 110 additions & 0 deletions wagtail/wagtailadmin/api/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import absolute_import, unicode_literals

from collections import OrderedDict

from wagtail.api.v2.endpoints import DocumentsAPIEndpoint, ImagesAPIEndpoint, 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 AdminImageSerializer, 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',
'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


class ImagesAdminAPIEndpoint(ImagesAPIEndpoint):
base_serializer_class = AdminImageSerializer

extra_body_fields = ImagesAPIEndpoint.extra_body_fields + [
'thumbnail',
]

default_fields = ImagesAPIEndpoint.default_fields + [
'width',
'height',
'thumbnail',
]



class DocumentsAdminAPIEndpoint(DocumentsAPIEndpoint):
pass
26 changes: 26 additions & 0 deletions wagtail/wagtailadmin/api/filters.py
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions wagtail/wagtailadmin/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from __future__ import absolute_import, unicode_literals

from collections import OrderedDict

from rest_framework.fields import Field

from wagtail.api.v2.serializers import ImageSerializer, PageSerializer
from wagtail.api.v2.utils import get_full_url
from wagtail.wagtailcore.models import Page
from wagtail.wagtailimages.models import SourceImageIOError


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 AdminPageSerializer(PageSerializer):
status = PageStatusField(read_only=True)
children = PageChildrenField(read_only=True)

meta_fields = PageSerializer.meta_fields + [
'status',
'children',
]


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)
16 changes: 16 additions & 0 deletions wagtail/wagtailadmin/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import absolute_import, unicode_literals

from django.conf.urls import url

from wagtail.api.v2.router import WagtailAPIRouter

from .endpoints import DocumentsAdminAPIEndpoint, ImagesAdminAPIEndpoint, PagesAdminAPIEndpoint

v1 = WagtailAPIRouter('wagtailadmin_api_v1')
v1.register_endpoint('pages', PagesAdminAPIEndpoint)
v1.register_endpoint('images', ImagesAdminAPIEndpoint)
v1.register_endpoint('documents', DocumentsAdminAPIEndpoint)

urlpatterns = [
url(r'^v2beta/', v1.urls),
]
Empty file.
Loading

0 comments on commit 3282fdb

Please sign in to comment.