Implement API v2 fields changes (RFC 5) (#2484)

pull/2748/merge
Karl Hobley 2016-07-28 16:22:11 +01:00 zatwierdzone przez Mikalai Radchuk
rodzic 5dfcdfb2a6
commit 6115f84e38
16 zmienionych plików z 1273 dodań i 139 usunięć

Wyświetl plik

@ -4,8 +4,10 @@ from collections import OrderedDict
from django.apps import apps
from django.conf.urls import url
from django.core.exceptions import FieldDoesNotExist
from django.core.urlresolvers import reverse
from django.http import Http404
from modelcluster.fields import ParentalKey
from rest_framework import status
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.response import Response
@ -18,7 +20,8 @@ from .filters import (
SearchFilter)
from .pagination import WagtailPagination
from .serializers import BaseSerializer, PageSerializer, get_serializer_class
from .utils import BadRequestError, filter_page_type, page_models_from_string
from .utils import (
BadRequestError, filter_page_type, page_models_from_string, parse_fields_parameter)
class BaseAPIEndpoint(GenericViewSet):
@ -49,9 +52,11 @@ class BaseAPIEndpoint(GenericViewSet):
# Required by BrowsableAPIRenderer
'format',
])
extra_body_fields = []
extra_meta_fields = []
default_fields = []
body_fields = ['id']
meta_fields = ['type', 'detail_url']
listing_default_fields = ['id', 'type', 'detail_url']
nested_default_fields = ['id', 'type', 'detail_url']
detail_only_fields = []
name = None # Set on subclass.
def __init__(self, *args, **kwargs):
@ -88,35 +93,69 @@ class BaseAPIEndpoint(GenericViewSet):
return Response(data, status=status.HTTP_400_BAD_REQUEST)
return super(BaseAPIEndpoint, self).handle_exception(exc)
def get_body_fields(self, model):
@classmethod
def get_body_fields(cls, model):
"""
This returns a list of field names that are allowed to
be used in the API (excluding the id field)
"""
fields = self.extra_body_fields[:]
fields = cls.body_fields[:]
if hasattr(model, 'api_fields'):
fields.extend(model.api_fields)
return fields
def get_meta_fields(self, model):
@classmethod
def get_meta_fields(cls, model):
"""
This returns a list of field names that are allowed to
be used in the meta section in the API (excluding type and detail_url).
"""
meta_fields = self.extra_meta_fields[:]
meta_fields = cls.meta_fields[:]
if hasattr(model, 'api_meta_fields'):
meta_fields.extend(model.api_meta_fields)
return meta_fields
def get_available_fields(self, model):
return self.get_body_fields(model) + self.get_meta_fields(model)
@classmethod
def get_available_fields(cls, model, db_fields_only=False):
"""
Returns a list of all the fields that can be used in the API for the
specified model class.
def get_default_fields(self, model):
return self.default_fields
Setting db_fields_only to True will remove all fields that do not have
an underlying column in the database (eg, type/detail_url and any custom
fields that are callables)
"""
fields = cls.get_body_fields(model) + cls.get_meta_fields(model)
if db_fields_only:
# Get list of available database fields then remove any fields in our
# list that isn't a database field
database_fields = set()
for field in model._meta.get_fields():
database_fields.add(field.name)
if hasattr(field, 'attname'):
database_fields.add(field.attname)
fields = [field for field in fields if field in database_fields]
return fields
@classmethod
def get_detail_default_fields(cls, model):
return cls.get_available_fields(model)
@classmethod
def get_listing_default_fields(cls, model):
return cls.listing_default_fields[:]
@classmethod
def get_nested_default_fields(cls, model):
return cls.nested_default_fields[:]
def check_query_parameters(self, queryset):
"""
@ -124,12 +163,102 @@ class BaseAPIEndpoint(GenericViewSet):
"""
query_parameters = set(self.request.GET.keys())
# All query paramters must be either a field or an operation
allowed_query_parameters = set(self.get_available_fields(queryset.model)).union(self.known_query_parameters).union({'id'})
# All query paramters must be either a database field or an operation
allowed_query_parameters = set(self.get_available_fields(queryset.model, db_fields_only=True)).union(self.known_query_parameters)
unknown_parameters = query_parameters - allowed_query_parameters
if unknown_parameters:
raise BadRequestError("query parameter is not an operation or a recognised field: %s" % ', '.join(sorted(unknown_parameters)))
@classmethod
def _get_serializer_class(cls, router, model, fields_config, show_details=False, nested=False):
# Get all available fields
body_fields = cls.get_body_fields(model)
meta_fields = cls.get_meta_fields(model)
all_fields = body_fields + meta_fields
# Remove any duplicates
all_fields = list(OrderedDict.fromkeys(all_fields))
if not show_details:
# Remove detail only fields
for field in cls.detail_only_fields:
try:
all_fields.remove(field)
except KeyError:
pass
# Get list of configured fields
if show_details:
fields = set(cls.get_detail_default_fields(model))
elif nested:
fields = set(cls.get_nested_default_fields(model))
else:
fields = set(cls.get_listing_default_fields(model))
# If first field is '*' start with all fields
# If first field is '_' start with no fields
if fields_config and fields_config[0][0] == '*':
fields = set(all_fields)
fields_config = fields_config[1:]
elif fields_config and fields_config[0][0] == '_':
fields = set()
fields_config = fields_config[1:]
mentioned_fields = set()
sub_fields = {}
for field_name, negated, field_sub_fields in fields_config:
if negated:
try:
fields.remove(field_name)
except KeyError:
pass
else:
fields.add(field_name)
if field_sub_fields:
sub_fields[field_name] = field_sub_fields
mentioned_fields.add(field_name)
unknown_fields = mentioned_fields - set(all_fields)
if unknown_fields:
raise BadRequestError("unknown fields: %s" % ', '.join(sorted(unknown_fields)))
# Build nested serialisers
child_serializer_classes = {}
for field_name in fields:
try:
django_field = model._meta.get_field(field_name)
except FieldDoesNotExist:
django_field = None
if django_field and django_field.is_relation:
child_sub_fields = sub_fields.get(field_name, [])
# Inline (aka "child") models should display all fields by default
if isinstance(getattr(django_field, 'field', None), ParentalKey):
if not child_sub_fields or child_sub_fields[0][0] not in ['*', '_']:
child_sub_fields = list(child_sub_fields)
child_sub_fields.insert(0, ('*', False, None))
# Get a serializer class for the related object
child_model = django_field.related_model
child_endpoint_class = router.get_model_endpoint(child_model)
child_endpoint_class = child_endpoint_class[1] if child_endpoint_class else BaseAPIEndpoint
child_serializer_classes[field_name] = child_endpoint_class._get_serializer_class(router, child_model, child_sub_fields, nested=True)
else:
if field_name in sub_fields:
# Sub fields were given for a non-related field
raise BadRequestError("'%s' does not support nested fields" % field_name)
# Reorder fields so it matches the order of all_fields
fields = [field for field in all_fields if field in fields]
return get_serializer_class(model, fields, meta_fields=meta_fields, child_serializer_classes=child_serializer_classes, base=cls.base_serializer_class)
def get_serializer_class(self):
request = self.request
@ -139,37 +268,23 @@ class BaseAPIEndpoint(GenericViewSet):
else:
model = type(self.get_object())
# Get all available fields
body_fields = self.get_body_fields(model)
meta_fields = self.get_meta_fields(model)
all_fields = body_fields + meta_fields
# Remove any duplicates
all_fields = list(OrderedDict.fromkeys(all_fields))
if self.action == 'listing_view':
# Listing views just show the title field and any other allowed field the user specified
if 'fields' in request.GET:
fields = set(request.GET['fields'].split(','))
else:
fields = set(self.get_default_fields(model))
unknown_fields = fields - set(all_fields)
if unknown_fields:
raise BadRequestError("unknown fields: %s" % ', '.join(sorted(unknown_fields)))
# Reorder fields so it matches the order of all_fields
fields = [field for field in all_fields if field in fields]
# Fields
if 'fields' in request.GET:
try:
fields_config = parse_fields_parameter(request.GET['fields'])
except ValueError as e:
raise BadRequestError("fields error: %s" % str(e))
else:
# Detail views show all fields all the time
fields = all_fields
# Use default fields
fields_config = []
# If showing details, add the parent field
if isinstance(self, PagesAPIEndpoint) and self.action == 'detail_view':
fields.insert(2, 'parent')
# Allow "detail_only" (eg parent) fields on detail view
if self.action == 'listing_view':
show_details = False
else:
show_details = True
return get_serializer_class(model, fields, meta_fields=meta_fields, base=self.base_serializer_class)
return self._get_serializer_class(self.request.wagtailapi_router, model, fields_config, show_details=show_details)
def get_serializer_context(self):
"""
@ -229,21 +344,28 @@ class PagesAPIEndpoint(BaseAPIEndpoint):
'child_of',
'descendant_of',
])
extra_body_fields = [
body_fields = BaseAPIEndpoint.body_fields + [
'title',
]
extra_meta_fields = [
meta_fields = BaseAPIEndpoint.meta_fields + [
'html_url',
'slug',
'show_in_menus',
'seo_title',
'search_description',
'first_published_at',
'parent',
]
default_fields = [
listing_default_fields = BaseAPIEndpoint.listing_default_fields + [
'title',
'html_url',
'slug',
'first_published_at',
]
nested_default_fields = BaseAPIEndpoint.nested_default_fields + [
'title',
]
detail_only_fields = ['parent']
name = 'pages'
model = Page

Wyświetl plik

@ -16,7 +16,7 @@ class FieldsFilter(BaseFilterBackend):
This performs field level filtering on the result set
Eg: ?title=James Joyce
"""
fields = set(view.get_available_fields(queryset.model)).union({'id'})
fields = set(view.get_available_fields(queryset.model, db_fields_only=True))
for field_name, value in request.GET.items():
if field_name in fields:
@ -67,7 +67,7 @@ class OrderingFilter(BaseFilterBackend):
reverse_order = False
# Add ordering
if order_by == 'id' or order_by in view.get_available_fields(queryset.model):
if order_by in view.get_available_fields(queryset.model):
queryset = queryset.order_by(order_by)
else:
# Unknown field

Wyświetl plik

@ -20,15 +20,6 @@ def get_object_detail_url(context, model, pk):
return get_full_url(context['request'], url_path)
def get_model_base_serializer_class(context, model):
endpoint = context['router'].get_model_endpoint(model)
if endpoint:
return endpoint[1].base_serializer_class
else:
return BaseSerializer
class TypeField(Field):
"""
Serializes the "type" field of each object.
@ -116,23 +107,16 @@ class RelatedField(relations.RelatedField):
}
}
"""
def __init__(self, *args, **kwargs):
self.serializer_class = kwargs.pop('serializer_class')
super(RelatedField, self).__init__(*args, **kwargs)
def to_representation(self, value):
# Construct a serializer for the related object with just the fields we need
base_meta_serializer_class = get_model_base_serializer_class(self.context, value.__class__)
meta_fields = [
field for field in base_meta_serializer_class.meta_fields
if field in base_meta_serializer_class.default_fields
]
meta_serializer_class = get_serializer_class(value.__class__, meta_fields, base=base_meta_serializer_class)
meta_serializer = meta_serializer_class(context=self.context)
return OrderedDict([
('id', value.pk),
('meta', meta_serializer.to_representation(value)['meta']),
])
serializer = self.serializer_class(context=self.context)
return serializer.to_representation(value)
class PageParentField(RelatedField):
class PageParentField(relations.RelatedField):
"""
Serializes the "parent" field on Page objects.
@ -148,6 +132,11 @@ class PageParentField(RelatedField):
if site_pages.filter(id=parent.id).exists():
return parent
def to_representation(self, value):
serializer_class = get_serializer_class(value.__class__, ['id', 'type', 'detail_url', 'html_url', 'title'], meta_fields=['type', 'detail_url', 'html_url'], base=PageSerializer)
serializer = serializer_class(context=self.context)
return serializer.to_representation(value)
class ChildRelationField(Field):
"""
@ -188,12 +177,11 @@ class ChildRelationField(Field):
]
"""
def __init__(self, *args, **kwargs):
self.child_fields = kwargs.pop('child_fields')
self.serializer_class = kwargs.pop('serializer_class')
super(ChildRelationField, self).__init__(*args, **kwargs)
def to_representation(self, value):
serializer_class = get_serializer_class(value.model, self.child_fields)
serializer = serializer_class(context=self.context)
serializer = self.serializer_class(context=self.context)
return [
serializer.to_representation(child_object)
@ -268,17 +256,6 @@ class BaseSerializer(serializers.ModelSerializer):
type = TypeField(read_only=True)
detail_url = DetailUrlField(read_only=True)
default_fields = [
'id',
'type',
'detail_url',
]
meta_fields = [
'type',
'detail_url',
]
def to_representation(self, instance):
data = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
@ -288,7 +265,8 @@ class BaseSerializer(serializers.ModelSerializer):
fields = [field for field in fields if field.field_name not in self.meta_fields]
# Make sure id is always first. This will be filled in later
data['id'] = None
if 'id' in [field.field_name for field in fields]:
data['id'] = None
# Serialise meta fields
meta = OrderedDict()
@ -305,7 +283,8 @@ class BaseSerializer(serializers.ModelSerializer):
else:
meta[field.field_name] = field.to_representation(attribute)
data['meta'] = meta
if meta:
data['meta'] = meta
# Serialise core fields
for field in fields:
@ -331,21 +310,17 @@ class BaseSerializer(serializers.ModelSerializer):
return super(BaseSerializer, self).build_property_field(field_name, model_class)
def build_relational_field(self, field_name, relation_info):
field_class, field_kwargs = super(BaseSerializer, self).build_relational_field(field_name, relation_info)
field_kwargs['serializer_class'] = self.child_serializer_classes[field_name]
return field_class, field_kwargs
class PageSerializer(BaseSerializer):
type = PageTypeField(read_only=True)
html_url = PageHtmlUrlField(read_only=True)
parent = PageParentField(read_only=True)
default_fields = BaseSerializer.default_fields + [
'html_url',
]
meta_fields = BaseSerializer.meta_fields + [
'html_url',
'parent',
]
def build_relational_field(self, field_name, relation_info):
# Find all relation fields that point to child class and make them use
# the ChildRelationField class.
@ -356,18 +331,19 @@ class PageSerializer(BaseSerializer):
for child_relation in get_all_child_relations(model)
}
if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
return ChildRelationField, {'child_fields': child_relations[field_name].api_fields}
if field_name in child_relations and field_name in self.child_serializer_classes:
return ChildRelationField, {'serializer_class': self.child_serializer_classes[field_name]}
return super(BaseSerializer, self).build_relational_field(field_name, relation_info)
return super(PageSerializer, self).build_relational_field(field_name, relation_info)
def get_serializer_class(model_, fields_, meta_fields=None, base=BaseSerializer):
def get_serializer_class(model_, fields_, meta_fields, child_serializer_classes=None, base=BaseSerializer):
class Meta:
model = model_
fields = base.default_fields + list(fields_)
fields = list(fields_)
return type(str(model_.__name__ + 'Serializer'), (base, ), {
'Meta': Meta,
'meta_fields': base.meta_fields + list(meta_fields or []),
'meta_fields': list(meta_fields),
'child_serializer_classes': child_serializer_classes or {},
})

Wyświetl plik

@ -77,7 +77,52 @@ class TestDocumentListing(TestCase):
for document in content['items']:
self.assertEqual(set(document.keys()), {'id', 'meta', 'title'})
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url', 'download_url'})
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url', 'download_url', 'tags'})
def test_remove_fields(self):
response = self.get_response(fields='-title')
content = json.loads(response.content.decode('UTF-8'))
for document in content['items']:
self.assertEqual(set(document.keys()), {'id', 'meta'})
def test_remove_meta_fields(self):
response = self.get_response(fields='-download_url')
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', 'tags'})
def test_remove_all_meta_fields(self):
response = self.get_response(fields='-type,-detail_url,-tags,-download_url')
content = json.loads(response.content.decode('UTF-8'))
for document in content['items']:
self.assertEqual(set(document.keys()), {'id', 'title'})
def test_remove_id_field(self):
response = self.get_response(fields='-id')
content = json.loads(response.content.decode('UTF-8'))
for document in content['items']:
self.assertEqual(set(document.keys()), {'meta', 'title'})
def test_all_fields(self):
response = self.get_response(fields='*')
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', 'tags', 'download_url'})
def test_all_fields_then_remove_something(self):
response = self.get_response(fields='*,-title,-download_url')
content = json.loads(response.content.decode('UTF-8'))
for document in content['items']:
self.assertEqual(set(document.keys()), {'id', 'meta'})
self.assertEqual(set(document['meta'].keys()), {'type', 'detail_url', 'tags'})
def test_fields_tags(self):
response = self.get_response(fields='tags')
@ -86,6 +131,13 @@ class TestDocumentListing(TestCase):
for document in content['items']:
self.assertIsInstance(document['meta']['tags'], list)
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(fields='uploaded_by_user')
content = json.loads(response.content.decode('UTF-8'))
@ -100,6 +152,13 @@ class TestDocumentListing(TestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
# FILTERING
@ -354,6 +413,71 @@ class TestDocumentDetail(TestCase):
self.assertIn('download_url', content['meta'])
self.assertEqual(content['meta']['download_url'], 'http://api.example.com/documents/1/wagtail_by_markyharky.jpg')
# FIELDS
def test_remove_fields(self):
response = self.get_response(2, fields='-title')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('id', set(content.keys()))
self.assertNotIn('title', set(content.keys()))
def test_remove_meta_fields(self):
response = self.get_response(2, fields='-download_url')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('detail_url', set(content['meta'].keys()))
self.assertNotIn('download_url', set(content['meta'].keys()))
def test_remove_id_field(self):
response = self.get_response(2, fields='-id')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('title', set(content.keys()))
self.assertNotIn('id', set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(2, fields='_,id,type')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content.keys()), {'id', 'meta'})
self.assertEqual(set(content['meta'].keys()), {'type'})
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(2, fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(2, fields='path')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: path"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(2, fields='123,title,abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(2, fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(2, fields='title(foo,bar)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "'title' does not support nested fields"})
@override_settings(
WAGTAILFRONTENDCACHE={

Wyświetl plik

@ -69,11 +69,56 @@ class TestImageListing(TestCase):
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
def test_fields(self):
response = self.get_response(fields='title,width,height')
response = self.get_response(fields='width,height')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'title', 'width', 'height'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
def test_remove_fields(self):
response = self.get_response(fields='-title')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta'})
def test_remove_meta_fields(self):
response = self.get_response(fields='-tags')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'title'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url'})
def test_remove_all_meta_fields(self):
response = self.get_response(fields='-type,-detail_url,-tags')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'title'})
def test_remove_id_field(self):
response = self.get_response(fields='-id')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'meta', 'title'})
def test_all_fields(self):
response = self.get_response(fields='*')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'title', 'width', 'height'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
def test_all_fields_then_remove_something(self):
response = self.get_response(fields='*,-title,-tags')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'width', 'height'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url'})
def test_fields_tags(self):
@ -81,10 +126,17 @@ class TestImageListing(TestCase):
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta'})
self.assertEqual(set(image.keys()), {'id', 'meta', 'title'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
self.assertIsInstance(image['meta']['tags'], list)
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(fields='uploaded_by_user')
content = json.loads(response.content.decode('UTF-8'))
@ -99,6 +151,13 @@ class TestImageListing(TestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
# FILTERING
@ -298,7 +357,6 @@ class TestImageDetail(TestCase):
def get_response(self, image_id, **params):
return self.client.get(reverse('wagtailapi_v2:images:detail', args=(image_id, )), params)
def test_basic(self):
response = self.get_response(5)
@ -349,6 +407,71 @@ class TestImageDetail(TestCase):
self.assertIn('tags', content['meta'])
self.assertEqual(content['meta']['tags'], ['hello', 'world'])
# FIELDS
def test_remove_fields(self):
response = self.get_response(5, fields='-title')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('id', set(content.keys()))
self.assertNotIn('title', set(content.keys()))
def test_remove_meta_fields(self):
response = self.get_response(5, fields='-type')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('detail_url', set(content['meta'].keys()))
self.assertNotIn('type', set(content['meta'].keys()))
def test_remove_id_field(self):
response = self.get_response(5, fields='-id')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('title', set(content.keys()))
self.assertNotIn('id', set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(5, fields='_,id,type')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content.keys()), {'id', 'meta'})
self.assertEqual(set(content['meta'].keys()), {'type'})
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(5, fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(5, fields='path')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: path"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(5, fields='123,title,abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(5, fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(5, fields='title(foo,bar)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "'title' does not support nested fields"})
@override_settings(
WAGTAILFRONTENDCACHE={

Wyświetl plik

@ -154,6 +154,97 @@ class TestPageListing(TestCase):
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'date', 'feed_image'})
def test_remove_fields(self):
response = self.get_response(fields='-title')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta'})
def test_remove_meta_fields(self):
response = self.get_response(fields='-html_url')
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', 'slug', 'first_published_at'})
def test_remove_all_meta_fields(self):
response = self.get_response(fields='-type,-detail_url,-slug,-first_published_at,-html_url')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'title'})
def test_remove_id_field(self):
response = self.get_response(fields='-id')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'meta', 'title'})
def test_all_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='*')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image'})
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'html_url', 'search_description'})
def test_all_fields_then_remove_something(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='*,-title,-date,-seo_title')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image'})
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'html_url', 'search_description'})
def test_remove_all_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='_,id,type')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta'})
self.assertEqual(set(page['meta'].keys()), {'type'})
def test_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(width,height)')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_remove_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(-title)')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page['feed_image'].keys()), {'id', 'meta'})
def test_all_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(*)')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_remove_all_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(_,id)')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page['feed_image'].keys()), {'id'})
def test_nested_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='carousel_items(image(width,height))')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
for carousel_item in page['carousel_items']:
# Note: inline objects default to displaying all fields
self.assertEqual(set(carousel_item.keys()), {'id', 'meta', 'image', 'embed_url', 'caption', 'link'})
self.assertEqual(set(carousel_item['image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_fields_child_relation(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='title,related_links')
content = json.loads(response.content.decode('UTF-8'))
@ -171,7 +262,7 @@ class TestPageListing(TestCase):
if feed_image is not None:
self.assertIsInstance(feed_image, dict)
self.assertEqual(set(feed_image.keys()), {'id', 'meta'})
self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
self.assertIsInstance(feed_image['id'], int)
self.assertIsInstance(feed_image['meta'], dict)
self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url'})
@ -183,7 +274,7 @@ class TestPageListing(TestCase):
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'tags'})
self.assertEqual(set(page.keys()), {'id', 'meta', 'tags', 'title'})
self.assertIsInstance(page['tags'], list)
def test_fields_ordering(self):
@ -204,6 +295,28 @@ class TestPageListing(TestCase):
]
self.assertEqual(list(content['items'][0].keys()), field_order)
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_unknown_nested_fields_give_error(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(123,title,abc)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_parent_field_gives_error(self):
# parent field isn't allowed in listings
response = self.get_response(fields='parent')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: parent"})
def test_fields_without_type_gives_error(self):
response = self.get_response(fields='title,related_links')
content = json.loads(response.content.decode('UTF-8'))
@ -225,6 +338,20 @@ class TestPageListing(TestCase):
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='title(foo,bar)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "'title' does not support nested fields"})
# FILTERING
@ -630,7 +757,7 @@ class TestPageDetail(TestCase):
# 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(set(content['meta']['parent'].keys()), {'id', 'meta', 'title'})
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'})
@ -654,7 +781,7 @@ class TestPageDetail(TestCase):
# 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(set(content['feed_image'].keys()), {'id', 'meta', 'title'})
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'})
@ -704,6 +831,138 @@ class TestPageDetail(TestCase):
self.assertIn('related_links', content)
self.assertEqual(content['feed_image'], None)
# FIELDS
def test_remove_fields(self):
response = self.get_response(16, fields='-title')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('id', set(content.keys()))
self.assertNotIn('title', set(content.keys()))
def test_remove_meta_fields(self):
response = self.get_response(16, fields='-html_url')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('detail_url', set(content['meta'].keys()))
self.assertNotIn('html_url', set(content['meta'].keys()))
def test_remove_all_meta_fields(self):
response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-search_description,-show_in_menus,-parent,-seo_title')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('id', set(content.keys()))
self.assertNotIn('meta', set(content.keys()))
def test_remove_id_field(self):
response = self.get_response(16, fields='-id')
content = json.loads(response.content.decode('UTF-8'))
self.assertIn('title', set(content.keys()))
self.assertNotIn('id', set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(16, fields='_,id,type')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content.keys()), {'id', 'meta'})
self.assertEqual(set(content['meta'].keys()), {'type'})
def test_nested_fields(self):
response = self.get_response(16, fields='feed_image(width,height)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_remove_nested_fields(self):
response = self.get_response(16, fields='feed_image(-title)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta'})
def test_all_nested_fields(self):
response = self.get_response(16, fields='feed_image(*)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_remove_all_nested_fields(self):
response = self.get_response(16, fields='feed_image(_,id)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content['feed_image'].keys()), {'id'})
def test_nested_nested_fields(self):
response = self.get_response(16, fields='carousel_items(image(width,height))')
content = json.loads(response.content.decode('UTF-8'))
for carousel_item in content['carousel_items']:
# Note: inline objects default to displaying all fields
self.assertEqual(set(carousel_item.keys()), {'id', 'meta', 'image', 'embed_url', 'caption', 'link'})
self.assertEqual(set(carousel_item['image'].keys()), {'id', 'meta', 'title', 'width', 'height'})
def test_fields_child_relation_is_list(self):
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
self.assertIsInstance(content['related_links'], list)
def test_fields_foreign_key(self):
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
feed_image = content['feed_image']
self.assertIsInstance(feed_image, dict)
self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
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/api/v2beta/images/%d/' % feed_image['id'])
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(16, fields='title,*')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "fields error: '*' must be in the first position"})
def test_unknown_nested_fields_give_error(self):
response = self.get_response(16, fields='feed_image(123,title,abc)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(16, fields='path')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: path"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(16, fields='123,title,abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(16, fields='-123,-title,-abc')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(16, fields='title(foo,bar)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {'message': "'title' does not support nested fields"})
class TestPageDetailWithStreamField(TestCase):
fixtures = ['test.json']

Wyświetl plik

@ -0,0 +1,237 @@
from __future__ import absolute_import, unicode_literals
from unittest import TestCase
from ..utils import FieldsParameterParseError, parse_fields_parameter
class TestParseFieldsParameter(TestCase):
# GOOD STUFF
def test_valid_single_field(self):
parsed = parse_fields_parameter('test')
self.assertEqual(parsed, [
('test', False, None),
])
def test_valid_multiple_fields(self):
parsed = parse_fields_parameter('test,another_test')
self.assertEqual(parsed, [
('test', False, None),
('another_test', False, None),
])
def test_valid_negated_field(self):
parsed = parse_fields_parameter('-test')
self.assertEqual(parsed, [
('test', True, None),
])
def test_valid_nested_fields(self):
parsed = parse_fields_parameter('test(foo,bar)')
self.assertEqual(parsed, [
('test', False, [
('foo', False, None),
('bar', False, None),
]),
])
def test_valid_star_field(self):
parsed = parse_fields_parameter('*,-test')
self.assertEqual(parsed, [
('*', False, None),
('test', True, None),
])
def test_valid_star_with_additional_field(self):
# Note: '*,test' is not allowed but '*,test(foo)' is
parsed = parse_fields_parameter('*,test(foo)')
self.assertEqual(parsed, [
('*', False, None),
('test', False, [
('foo', False, None),
]),
])
def test_valid_underscore_field(self):
parsed = parse_fields_parameter('_,test')
self.assertEqual(parsed, [
('_', False, None),
('test', False, None),
])
def test_valid_field_with_underscore_in_middle(self):
parsed = parse_fields_parameter('a_test')
self.assertEqual(parsed, [
('a_test', False, None),
])
def test_valid_negated_field_with_underscore_in_middle(self):
parsed = parse_fields_parameter('-a_test')
self.assertEqual(parsed, [
('a_test', True, None),
])
def test_valid_field_with_underscore_at_beginning(self):
parsed = parse_fields_parameter('_test')
self.assertEqual(parsed, [
('_test', False, None),
])
def test_valid_field_with_underscore_at_end(self):
parsed = parse_fields_parameter('test_')
self.assertEqual(parsed, [
('test_', False, None),
])
# BAD STUFF
def test_invalid_char(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test#')
self.assertEqual(str(e.exception), "unexpected char '#' at position 4")
def test_invalid_whitespace_before_identifier(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter(' test')
self.assertEqual(str(e.exception), "unexpected whitespace at position 0")
def test_invalid_whitespace_after_identifier(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test ')
self.assertEqual(str(e.exception), "unexpected whitespace at position 4")
def test_invalid_whitespace_after_comma(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test, test')
self.assertEqual(str(e.exception), "unexpected whitespace at position 5")
def test_invalid_whitespace_before_comma(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test ,test')
self.assertEqual(str(e.exception), "unexpected whitespace at position 4")
def test_invalid_unexpected_negation_operator(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test-')
self.assertEqual(str(e.exception), "unexpected char '-' at position 4")
def test_invalid_unexpected_open_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test,(foo)')
self.assertEqual(str(e.exception), "unexpected char '(' at position 5")
def test_invalid_unexpected_close_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test)')
self.assertEqual(str(e.exception), "unexpected char ')' at position 4")
def test_invalid_unexpected_comma_in_middle(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test,,foo')
self.assertEqual(str(e.exception), "unexpected char ',' at position 5")
def test_invalid_unexpected_comma_at_end(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test,foo,')
self.assertEqual(str(e.exception), "unexpected char ',' at position 9")
def test_invalid_unclosed_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test(foo')
self.assertEqual(str(e.exception), "unexpected end of input (did you miss out a close bracket?)")
def test_invalid_subfields_on_negated_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('-test(foo)')
self.assertEqual(str(e.exception), "unexpected char '(' at position 5")
def test_invalid_star_field_in_wrong_position(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test,*')
self.assertEqual(str(e.exception), "'*' must be in the first position")
def test_invalid_negated_star(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('-*')
self.assertEqual(str(e.exception), "'*' cannot be negated")
def test_invalid_star_with_nesting(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('*(foo,bar)')
self.assertEqual(str(e.exception), "unexpected char '(' at position 1")
def test_invalid_star_with_chars_after(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('*foo')
self.assertEqual(str(e.exception), "unexpected char 'f' at position 1")
def test_invalid_star_with_chars_before(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('foo*')
self.assertEqual(str(e.exception), "unexpected char '*' at position 3")
def test_invalid_star_with_additional_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('*,foo')
self.assertEqual(str(e.exception), "additional fields with '*' doesn't make sense")
def test_invalid_underscore_in_wrong_position(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('test,_')
self.assertEqual(str(e.exception), "'_' must be in the first position")
def test_invalid_negated_underscore(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('-_')
self.assertEqual(str(e.exception), "'_' cannot be negated")
def test_invalid_underscore_with_nesting(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('_(foo,bar)')
self.assertEqual(str(e.exception), "unexpected char '(' at position 1")
def test_invalid_underscore_with_negated_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('_,-foo')
self.assertEqual(str(e.exception), "negated fields with '_' doesn't make sense")
def test_invalid_star_and_underscore(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter('*,_')
self.assertEqual(str(e.exception), "'_' must be in the first position")

Wyświetl plik

@ -53,3 +53,166 @@ def filter_page_type(queryset, page_models):
qs |= queryset.type(model)
return qs
class FieldsParameterParseError(ValueError):
pass
def parse_fields_parameter(fields_str):
"""
Parses the ?fields= GET parameter. As this parameter is supposed to be used
by developers, the syntax is quite tight (eg, not allowing any whitespace).
Having a strict syntax allows us to extend the it at a later date with less
chance of breaking anyone's code.
This function takes a string and returns a list of tuples representing each
top-level field. Each tuple contains three items:
- The name of the field (string)
- Whether the field has been negated (boolean)
- A list of nested fields if there are any, None otherwise
Some examples of how this function works:
>>> parse_fields_parameter("foo")
[
('foo', False, None),
]
>>> parse_fields_parameter("foo,bar")
[
('foo', False, None),
('bar', False, None),
]
>>> parse_fields_parameter("-foo")
[
('foo', True, None),
]
>>> parse_fields_parameter("foo(bar,baz)")
[
('foo', False, [
('bar', False, None),
('baz', False, None),
]),
]
It raises a FieldsParameterParseError (subclass of ValueError) if it
encounters a syntax error
"""
def get_position(current_str):
return len(fields_str) - len(current_str)
def parse_field_identifier(fields_str):
first_char = True
negated = False
ident = ""
while fields_str:
char = fields_str[0]
if char in ['(', ')', ',']:
if not ident:
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
if ident in ['*', '_'] and char == '(':
# * and _ cannot have nested fields
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
return ident, negated, fields_str
elif char == '-':
if not first_char:
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
negated = True
elif char in ['*', '_']:
if ident and char == '*':
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
ident += char
elif char.isalnum() or char == '_':
if ident == '*':
# * can only be on its own
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
ident += char
elif char.isspace():
raise FieldsParameterParseError("unexpected whitespace at position %d" % get_position(fields_str))
else:
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
first_char = False
fields_str = fields_str[1:]
return ident, negated, fields_str
def parse_fields(fields_str, expect_close_bracket=False):
first_ident = None
is_first = True
fields = []
while fields_str:
sub_fields = None
ident, negated, fields_str = parse_field_identifier(fields_str)
# Some checks specific to '*' and '_'
if ident in ['*', '_']:
if not is_first:
raise FieldsParameterParseError("'%s' must be in the first position" % ident)
if negated:
raise FieldsParameterParseError("'%s' cannot be negated" % ident)
if fields_str and fields_str[0] == '(':
if negated:
# Negated fields cannot contain subfields
raise FieldsParameterParseError("unexpected char '(' at position %d" % get_position(fields_str))
sub_fields, fields_str = parse_fields(fields_str[1:], expect_close_bracket=True)
if is_first:
first_ident = ident
else:
# Negated fields can't be used with '_'
if first_ident == '_' and negated:
# _,foo is allowed but _,-foo is not
raise FieldsParameterParseError("negated fields with '_' doesn't make sense")
# Additional fields without sub fields can't be used with '*'
if first_ident == '*' and not negated and not sub_fields:
# *,foo(bar) and *,-foo are allowed but *,foo is not
raise FieldsParameterParseError("additional fields with '*' doesn't make sense")
fields.append((ident, negated, sub_fields))
if fields_str and fields_str[0] == ')':
if not expect_close_bracket:
raise FieldsParameterParseError("unexpected char ')' at position %d" % get_position(fields_str))
return fields, fields_str[1:]
if fields_str and fields_str[0] == ',':
fields_str = fields_str[1:]
# A comma can not exist immediately before another comma or the end of the string
if not fields_str or fields_str[0] == ',':
raise FieldsParameterParseError("unexpected char ',' at position %d" % get_position(fields_str))
is_first = False
if expect_close_bracket:
# This parser should've exited with a close bracket but instead we
# hit the end of the input. Raise an error
raise FieldsParameterParseError("unexpected end of input (did you miss out a close bracket?)")
return fields, fields_str
fields, _ = parse_fields(fields_str)
return fields

Wyświetl plik

@ -26,7 +26,7 @@ class PagesAdminAPIEndpoint(PagesAPIEndpoint):
SearchFilter,
]
extra_meta_fields = PagesAPIEndpoint.extra_meta_fields + [
meta_fields = PagesAPIEndpoint.meta_fields + [
'latest_revision_created_at',
'status',
'children',
@ -34,12 +34,15 @@ class PagesAdminAPIEndpoint(PagesAPIEndpoint):
'parent',
]
default_fields = PagesAPIEndpoint.default_fields + [
listing_default_fields = PagesAPIEndpoint.listing_default_fields + [
'latest_revision_created_at',
'status',
'children',
]
# Allow the parent field to appear on listings
detail_only_fields = []
known_query_parameters = PagesAPIEndpoint.known_query_parameters.union([
'has_children'
])

Wyświetl plik

@ -82,9 +82,3 @@ 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',
]

Wyświetl plik

@ -68,6 +68,68 @@ class TestAdminImageListing(AdminAPITestCase, TestImageListing):
self.assertEqual(set(image.keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url', 'tags'})
def test_fields(self):
response = self.get_response(fields='width,height')
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'})
def test_remove_fields(self):
response = self.get_response(fields='-title')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'width', 'height', 'thumbnail'})
def test_remove_meta_fields(self):
response = self.get_response(fields='-tags')
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'})
def test_remove_all_meta_fields(self):
response = self.get_response(fields='-type,-detail_url,-tags')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'title', 'width', 'height', 'thumbnail'})
def test_remove_id_field(self):
response = self.get_response(fields='-id')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'meta', 'title', 'width', 'height', 'thumbnail'})
def test_all_fields(self):
response = self.get_response(fields='*')
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'})
def test_all_fields_then_remove_something(self):
response = self.get_response(fields='*,-title,-tags')
content = json.loads(response.content.decode('UTF-8'))
for image in content['items']:
self.assertEqual(set(image.keys()), {'id', 'meta', 'width', 'height', 'thumbnail'})
self.assertEqual(set(image['meta'].keys()), {'type', 'detail_url'})
def test_fields_tags(self):
response = self.get_response(fields='tags')
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'})
self.assertIsInstance(image['meta']['tags'], list)
class TestAdminImageDetail(AdminAPITestCase, TestImageDetail):
fixtures = ['demosite.json']
@ -75,7 +137,6 @@ class TestAdminImageDetail(AdminAPITestCase, TestImageDetail):
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)

Wyświetl plik

@ -107,6 +107,9 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
# FIELDS
# Not applicable to the admin API
test_parent_field_gives_error = None
def test_fields_default(self):
response = self.get_response(type='demosite.BlogEntryPage')
content = json.loads(response.content.decode('UTF-8'))
@ -115,6 +118,44 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
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_remove_meta_fields(self):
response = self.get_response(fields='-html_url')
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', 'slug', 'first_published_at', 'latest_revision_created_at', 'status', 'children'})
def test_remove_all_meta_fields(self):
response = self.get_response(fields='-type,-detail_url,-slug,-first_published_at,-html_url,-latest_revision_created_at,-status,-children')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'title'})
def test_all_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='*')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image'})
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'parent', 'html_url', 'search_description', 'children', 'descendants', 'status', 'latest_revision_created_at'})
def test_all_fields_then_remove_something(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='*,-title,-date,-seo_title,-status')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image'})
self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'parent', 'html_url', 'search_description', 'children', 'descendants', 'latest_revision_created_at'})
def test_all_nested_fields(self):
response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(*)')
content = json.loads(response.content.decode('UTF-8'))
for page in content['items']:
self.assertEqual(set(page['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
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')
@ -125,7 +166,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
if feed_image is not None:
self.assertIsInstance(feed_image, dict)
self.assertEqual(set(feed_image.keys()), {'id', 'meta'})
self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
self.assertIsInstance(feed_image['id'], int)
self.assertIsInstance(feed_image['meta'], dict)
self.assertEqual(set(feed_image['meta'].keys()), {'type', 'detail_url'})
@ -140,13 +181,14 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing):
parent = page['meta']['parent']
# All blog entry pages have the same parent
self.assertEqual(parent, {
self.assertDictEqual(parent, {
'id': 5,
'meta': {
'type': 'demosite.BlogIndexPage',
'detail_url': 'http://localhost/admin/api/v2beta/pages/5/',
'html_url': 'http://localhost/blog-index/',
}
},
'title': "Blog index"
})
def test_fields_descendants(self):
@ -289,7 +331,7 @@ class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
# 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(set(content['meta']['parent'].keys()), {'id', 'meta', 'title'})
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'})
@ -313,7 +355,7 @@ class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
# 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(set(content['feed_image'].keys()), {'id', 'meta', 'title'})
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'})
@ -443,6 +485,42 @@ class TestAdminPageDetail(AdminAPITestCase, TestPageDetail):
'listing_url': 'http://localhost/admin/api/v2beta/pages/?descendant_of=2'
})
# FIELDS
def test_remove_all_meta_fields(self):
response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-descendants,-latest_revision_created_at,-children,-show_in_menus,-seo_title,-parent,-status,-search_description')
content = json.loads(response.content.decode('UTF-8'))
self.assertNotIn('meta', set(content.keys()))
self.assertIn('id', set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(16, fields='_,id,type')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content.keys()), {'id', 'meta', '__types'})
self.assertEqual(set(content['meta'].keys()), {'type'})
def test_all_nested_fields(self):
response = self.get_response(16, fields='feed_image(*)')
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(set(content['feed_image'].keys()), {'id', 'meta', 'title', 'width', 'height', 'thumbnail'})
def test_fields_foreign_key(self):
response = self.get_response(16)
content = json.loads(response.content.decode('UTF-8'))
feed_image = content['feed_image']
self.assertIsInstance(feed_image, dict)
self.assertEqual(set(feed_image.keys()), {'id', 'meta', 'title'})
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'])
class TestAdminPageDetailWithStreamField(AdminAPITestCase):
fixtures = ['test.json']

Wyświetl plik

@ -10,8 +10,9 @@ from .serializers import DocumentSerializer
class DocumentsAPIEndpoint(BaseAPIEndpoint):
base_serializer_class = DocumentSerializer
filter_backends = [FieldsFilter, OrderingFilter, SearchFilter]
extra_body_fields = ['title']
extra_meta_fields = ['tags', ]
default_fields = ['title', 'tags']
body_fields = BaseAPIEndpoint.body_fields + ['title']
meta_fields = BaseAPIEndpoint.meta_fields + ['tags', 'download_url']
listing_default_fields = BaseAPIEndpoint.listing_default_fields + ['title', 'tags', 'download_url']
nested_default_fields = BaseAPIEndpoint.nested_default_fields + ['title', 'download_url']
name = 'documents'
model = get_document_model()

Wyświetl plik

@ -22,11 +22,3 @@ class DocumentDownloadUrlField(Field):
class DocumentSerializer(BaseSerializer):
download_url = DocumentDownloadUrlField(read_only=True)
default_fields = BaseSerializer.default_fields + [
'download_url',
]
meta_fields = BaseSerializer.meta_fields + [
'download_url',
]

Wyświetl plik

@ -7,11 +7,11 @@ from .serializers import AdminImageSerializer
class ImagesAdminAPIEndpoint(ImagesAPIEndpoint):
base_serializer_class = AdminImageSerializer
extra_body_fields = ImagesAPIEndpoint.extra_body_fields + [
body_fields = ImagesAPIEndpoint.body_fields + [
'thumbnail',
]
default_fields = ImagesAPIEndpoint.default_fields + [
listing_default_fields = ImagesAPIEndpoint.listing_default_fields + [
'width',
'height',
'thumbnail',

Wyświetl plik

@ -11,8 +11,9 @@ from .serializers import ImageSerializer
class ImagesAPIEndpoint(BaseAPIEndpoint):
base_serializer_class = ImageSerializer
filter_backends = [FieldsFilter, OrderingFilter, SearchFilter]
extra_body_fields = ['title', 'width', 'height']
extra_meta_fields = ['tags']
default_fields = ['title', 'tags']
body_fields = BaseAPIEndpoint.body_fields + ['title', 'width', 'height']
meta_fields = BaseAPIEndpoint.meta_fields + ['tags']
listing_default_fields = BaseAPIEndpoint.listing_default_fields + ['title', 'tags']
nested_default_fields = BaseAPIEndpoint.nested_default_fields + ['title']
name = 'images'
model = get_image_model()