Refactor meta serialisation

This is preparation for implementing better customisation of the meta section (allowing user fields to be added here).

All meta field classes have been broken down into parts. So DocumentMetaField/PageMetaField/etc have been broken down to TypeField, DetailUrlField, etc.

I have made the following public-facing changes to the API along the way:
 - page "parent" field has moved into meta
 - page "parent" is always present now, it will be serialised to null on homepages instead of disappearing
 - Child relations now have id and meta
pull/1974/head
Karl Hobley 2016-02-24 10:27:41 +00:00
rodzic 3cc0662d42
commit 10c65d6881
3 zmienionych plików z 175 dodań i 75 usunięć

Wyświetl plik

@ -138,9 +138,6 @@ class BaseAPIEndpoint(GenericViewSet):
# Detail views show all fields all the time
fields = all_fields
# Always show id and meta first
fields = ['id', 'meta'] + fields
# If showing details, add the parent field
if isinstance(self, PagesAPIEndpoint) and self.action == 'detail_view':
fields.insert(2, 'parent')

Wyświetl plik

@ -9,7 +9,7 @@ from django.core.urlresolvers import NoReverseMatch
from taggit.managers import _TaggableManager
from rest_framework import serializers
from rest_framework.fields import Field
from rest_framework.fields import Field, SkipField
from rest_framework import relations
from wagtail.utils.compat import get_related_model
@ -25,77 +25,96 @@ def get_object_detail_url(context, model, pk):
return get_full_url(context['request'], url_path)
class MetaField(Field):
"""
Serializes the "meta" section of each object.
def get_model_base_serializer_class(context, model):
endpoint = context['router'].get_model_endpoint(model)
This section is used for storing non-field data such as model name, urls, etc.
if endpoint:
return endpoint[1].base_serializer_class
else:
return BaseSerializer
class TypeField(Field):
"""
Serializes the "type" field of each object.
Example:
"meta": {
"type": "wagtailimages.Image",
"detail_url": "http://api.example.com/v1/images/1/"
}
"type": "wagtailimages.Image"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, obj):
return OrderedDict([
('type', type(obj)._meta.app_label + '.' + type(obj).__name__),
('detail_url', get_object_detail_url(self.context, type(obj), obj.pk)),
])
return type(obj)._meta.app_label + '.' + type(obj).__name__
class PageMetaField(MetaField):
class DetailUrlField(Field):
"""
A subclass of MetaField for Page objects.
Changes the "type" field to use the name of the specific model of the page.
Serializes the "detail_url" field of each object.
Example:
"meta": {
"type": "blog.BlogPage",
"detail_url": "http://api.example.com/v1/pages/1/"
"html_url": "http://www.example.com/blog/blog-post/"
}
"detail_url": "http://api.example.com/v1/images/1/"
"""
def get_attribute(self, instance):
url = get_object_detail_url(self.context, type(instance), instance.pk)
if url:
return url
else:
# Hide the detail_url field if the object doesn't have an endpoint
raise SkipField
def to_representation(self, url):
return url
class PageHtmlUrlField(Field):
"""
Serializes the "html_url" field for pages.
Example:
"html_url": "http://www.example.com/blog/blog-post/"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
data = OrderedDict([
('type', page.specific_class._meta.app_label + '.' + page.specific_class.__name__),
('detail_url', get_object_detail_url(self.context, type(page), page.pk)),
])
try:
data['html_url'] = page.full_url
return page.full_url
except NoReverseMatch:
pass
return data
return None
class DocumentMetaField(MetaField):
class PageTypeField(Field):
"""
A subclass of MetaField for Document objects.
Serializes the "type" field for pages.
Adds a "download_url" field.
This takes into account the fact that we sometimes may not have the "specific"
page object by calling "page.specific_class" instead of looking at the object's
type.
"meta": {
"type": "wagtaildocs.Document",
"detail_url": "http://api.example.com/v1/documents/1/",
"download_url": "http://api.example.com/documents/1/my_document.pdf"
}
Example:
"type": "blog.BlogPage"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
return page.specific_class._meta.app_label + '.' + page.specific_class.__name__
class DocumentDownloadUrlField(Field):
"""
Serializes the "download_url" field for documents.
Example:
"download_url": "http://api.example.com/documents/1/my_document.pdf"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, document):
data = OrderedDict([
('type', "wagtaildocs.Document"),
('detail_url', get_object_detail_url(self.context, type(document), document.pk)),
('download_url', get_full_url(self.context['request'], document.url)),
])
return data
return get_full_url(self.context['request'], document.url)
class RelatedField(relations.RelatedField):
@ -112,15 +131,19 @@ class RelatedField(relations.RelatedField):
}
}
"""
meta_field_serializer_class = MetaField
def to_representation(self, value):
meta_serializer = self.meta_field_serializer_class()
meta_serializer.bind('meta', self)
# 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', meta_serializer.to_representation(value)['meta']),
])
@ -133,8 +156,6 @@ class PageParentField(RelatedField):
The representation is the same as the RelatedField class.
"""
meta_field_serializer_class = PageMetaField
def get_attribute(self, instance):
parent = instance.get_parent()
@ -158,6 +179,10 @@ class ChildRelationField(Field):
"carousel_items": [
{
"id": 1,
"meta": {
"type": "demo.MyCarouselItem"
},
"title": "First carousel item",
"image": {
"id": 1,
@ -167,8 +192,11 @@ class ChildRelationField(Field):
}
}
},
"carousel_items": [
{
"id": 2,
"meta": {
"type": "demo.MyCarouselItem"
},
"title": "Second carousel item (no image)",
"image": null
}
@ -251,7 +279,64 @@ class BaseSerializer(serializers.ModelSerializer):
})
serializer_related_field = RelatedField
meta = MetaField()
# Meta fields
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]
# Split meta fields from core fields
meta_fields = [field for field in fields if field.field_name in self.meta_fields]
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
# Serialise meta fields
meta = OrderedDict()
for field in meta_fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is None:
# We skip `to_representation` for `None` values so that
# fields do not have to explicitly deal with that case.
meta[field.field_name] = None
else:
meta[field.field_name] = field.to_representation(attribute)
data['meta'] = meta
# Serialise core fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is None:
# We skip `to_representation` for `None` values so that
# fields do not have to explicitly deal with that case.
data[field.field_name] = None
else:
data[field.field_name] = field.to_representation(attribute)
return data
def build_property_field(self, field_name, model_class):
# TaggableManager is not a Django field so it gets treated as a property
@ -263,9 +348,19 @@ class BaseSerializer(serializers.ModelSerializer):
class PageSerializer(BaseSerializer):
meta = PageMetaField()
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.
@ -287,13 +382,21 @@ class ImageSerializer(BaseSerializer):
class DocumentSerializer(BaseSerializer):
meta = DocumentMetaField()
download_url = DocumentDownloadUrlField(read_only=True)
default_fields = BaseSerializer.default_fields + [
'download_url',
]
meta_fields = BaseSerializer.meta_fields + [
'download_url',
]
def get_serializer_class(model_, fields_, base=BaseSerializer):
class Meta:
model = model_
fields = fields_
fields = base.default_fields + list(fields_)
return type(model_.__name__ + 'Serializer', (base, ), {
'Meta': Meta

Wyświetl plik

@ -609,15 +609,15 @@ class TestPageDetail(TestCase):
self.assertEqual(content['meta']['html_url'], 'http://localhost/blog-index/blog-post/')
# Check the parent field
self.assertIn('parent', content)
self.assertIsInstance(content['parent'], dict)
self.assertEqual(set(content['parent'].keys()), {'id', 'meta'})
self.assertEqual(content['parent']['id'], 5)
self.assertIsInstance(content['parent']['meta'], dict)
self.assertEqual(set(content['parent']['meta'].keys()), {'type', 'detail_url', 'html_url'})
self.assertEqual(content['parent']['meta']['type'], 'demosite.BlogIndexPage')
self.assertEqual(content['parent']['meta']['detail_url'], 'http://localhost/api/v2beta/pages/5/')
self.assertEqual(content['parent']['meta']['html_url'], 'http://localhost/blog-index/')
self.assertIn('parent', content['meta'])
self.assertIsInstance(content['meta']['parent'], dict)
self.assertEqual(set(content['meta']['parent'].keys()), {'id', 'meta'})
self.assertEqual(content['meta']['parent']['id'], 5)
self.assertIsInstance(content['meta']['parent']['meta'], dict)
self.assertEqual(set(content['meta']['parent']['meta'].keys()), {'type', 'detail_url', 'html_url'})
self.assertEqual(content['meta']['parent']['meta']['type'], 'demosite.BlogIndexPage')
self.assertEqual(content['meta']['parent']['meta']['detail_url'], 'http://localhost/api/v2beta/pages/5/')
self.assertEqual(content['meta']['parent']['meta']['html_url'], 'http://localhost/blog-index/')
# Check that the custom fields are included
self.assertIn('date', content)
@ -645,14 +645,15 @@ class TestPageDetail(TestCase):
# Check that the child relations were serialised properly
self.assertEqual(content['related_links'], [])
for carousel_item in content['carousel_items']:
self.assertEqual(set(carousel_item.keys()), {'embed_url', 'link', 'caption', 'image'})
self.assertEqual(set(carousel_item.keys()), {'id', 'meta', 'embed_url', 'link', 'caption', 'image'})
self.assertEqual(set(carousel_item['meta'].keys()), {'type'})
def test_meta_parent_id_doesnt_show_root_page(self):
# Root page isn't in the site so don't show it if the user is looking at the home page
response = self.get_response(2)
content = json.loads(response.content.decode('UTF-8'))
self.assertNotIn('parent', content['meta'])
self.assertIsNone(content['meta']['parent'])
def test_field_ordering(self):
response = self.get_response(16)
@ -665,7 +666,6 @@ class TestPageDetail(TestCase):
field_order = [
'id',
'meta',
'parent',
'title',
'slug',
'show_in_menus',