kopia lustrzana https://github.com/wagtail/wagtail
Implement API v2 fields changes (RFC 5) (#2484)
rodzic
5dfcdfb2a6
commit
6115f84e38
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {},
|
||||
})
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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")
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
Ładowanie…
Reference in New Issue