From 301463a99dceceb21ecec933f3a83e55ca37c3b8 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Feb 2017 13:08:40 +0000 Subject: [PATCH 01/10] Use source keyword argument (instead of overriding get_attribute) This allows the ImageRenditionField to be used on models that contain an image field. --- wagtail/wagtailimages/api/admin/serializers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wagtail/wagtailimages/api/admin/serializers.py b/wagtail/wagtailimages/api/admin/serializers.py index ac4536d1b..d4f61ee55 100644 --- a/wagtail/wagtailimages/api/admin/serializers.py +++ b/wagtail/wagtailimages/api/admin/serializers.py @@ -31,9 +31,6 @@ class ImageRenditionField(Field): self.filter_spec = filter_spec super(ImageRenditionField, self).__init__(*args, **kwargs) - def get_attribute(self, instance): - return instance - def to_representation(self, image): try: thumbnail = image.get_rendition(self.filter_spec) @@ -50,4 +47,4 @@ class ImageRenditionField(Field): class AdminImageSerializer(ImageSerializer): - thumbnail = ImageRenditionField('max-165x165', read_only=True) + thumbnail = ImageRenditionField('max-165x165', source='*', read_only=True) From 9d7a33a87acf16e42c17fd8b01284625c4bb68f6 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Feb 2017 13:12:25 +0000 Subject: [PATCH 02/10] Moved ImageRenditionField to a more importable place --- .../wagtailimages/api/admin/serializers.py | 44 +----------------- wagtail/wagtailimages/api/fields.py | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 43 deletions(-) create mode 100644 wagtail/wagtailimages/api/fields.py diff --git a/wagtail/wagtailimages/api/admin/serializers.py b/wagtail/wagtailimages/api/admin/serializers.py index d4f61ee55..db2f1dfc6 100644 --- a/wagtail/wagtailimages/api/admin/serializers.py +++ b/wagtail/wagtailimages/api/admin/serializers.py @@ -1,50 +1,8 @@ from __future__ import absolute_import, unicode_literals -from collections import OrderedDict - -from rest_framework.fields import Field - -from ...models import SourceImageIOError +from ..fields import ImageRenditionField from ..v2.serializers import ImageSerializer -class ImageRenditionField(Field): - """ - A field that generates a rendition with the specified filter spec, and serialises - details of that rendition. - - Example: - "thumbnail": { - "url": "/media/images/myimage.max-165x165.jpg", - "width": 165, - "height": 100 - } - - If there is an error with the source image. The dict will only contain a single - key, "error", indicating this error: - - "thumbnail": { - "error": "SourceImageIOError" - } - """ - def __init__(self, filter_spec, *args, **kwargs): - self.filter_spec = filter_spec - super(ImageRenditionField, self).__init__(*args, **kwargs) - - def to_representation(self, image): - try: - thumbnail = image.get_rendition(self.filter_spec) - - return OrderedDict([ - ('url', thumbnail.url), - ('width', thumbnail.width), - ('height', thumbnail.height), - ]) - except SourceImageIOError: - return OrderedDict([ - ('error', 'SourceImageIOError'), - ]) - - class AdminImageSerializer(ImageSerializer): thumbnail = ImageRenditionField('max-165x165', source='*', read_only=True) diff --git a/wagtail/wagtailimages/api/fields.py b/wagtail/wagtailimages/api/fields.py new file mode 100644 index 000000000..6eb3d7925 --- /dev/null +++ b/wagtail/wagtailimages/api/fields.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, unicode_literals + +from collections import OrderedDict + +from rest_framework.fields import Field + +from ..models import SourceImageIOError + + +class ImageRenditionField(Field): + """ + A field that generates a rendition with the specified filter spec, and serialises + details of that rendition. + + Example: + "thumbnail": { + "url": "/media/images/myimage.max-165x165.jpg", + "width": 165, + "height": 100 + } + + If there is an error with the source image. The dict will only contain a single + key, "error", indicating this error: + + "thumbnail": { + "error": "SourceImageIOError" + } + """ + def __init__(self, filter_spec, *args, **kwargs): + self.filter_spec = filter_spec + super(ImageRenditionField, self).__init__(*args, **kwargs) + + def to_representation(self, image): + try: + thumbnail = image.get_rendition(self.filter_spec) + + return OrderedDict([ + ('url', thumbnail.url), + ('width', thumbnail.width), + ('height', thumbnail.height), + ]) + except SourceImageIOError: + return OrderedDict([ + ('error', 'SourceImageIOError'), + ]) From 268899b471b2c3d12459a4396a4dd8e793e0031b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 17 Mar 2017 16:57:50 +0000 Subject: [PATCH 03/10] Allow custom field classes to be used in api_fields --- wagtail/api/v2/endpoints.py | 33 +++++++++++++++++++++++++++--- wagtail/api/v2/serializers.py | 15 ++++++++++---- wagtail/api/v2/tests/test_pages.py | 11 ++++++++-- wagtail/tests/demosite/models.py | 3 +++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/wagtail/api/v2/endpoints.py b/wagtail/api/v2/endpoints.py index c3a0b08ed..578703ead 100644 --- a/wagtail/api/v2/endpoints.py +++ b/wagtail/api/v2/endpoints.py @@ -102,7 +102,10 @@ class BaseAPIEndpoint(GenericViewSet): fields = cls.body_fields[:] if hasattr(model, 'api_fields'): - fields.extend(model.api_fields) + fields.extend([ + field[0] if isinstance(field, tuple) else field + for field in model.api_fields + ]) return fields @@ -115,10 +118,33 @@ class BaseAPIEndpoint(GenericViewSet): meta_fields = cls.meta_fields[:] if hasattr(model, 'api_meta_fields'): - meta_fields.extend(model.api_meta_fields) + meta_fields.extend([ + field[0] if isinstance(field, tuple) else field + for field in model.api_meta_fields + ]) return meta_fields + @classmethod + def get_field_configs(cls, model): + configs = {} + + if hasattr(model, 'api_fields'): + configs.update({ + field[0]: field[1] + for field in model.api_fields + if isinstance(field, tuple) + }) + + if hasattr(model, 'api_meta_fields'): + configs.update({ + field[0]: field[1] + for field in model.api_meta_fields + if isinstance(field, tuple) + }) + + return configs + @classmethod def get_available_fields(cls, model, db_fields_only=False): """ @@ -257,7 +283,8 @@ class BaseAPIEndpoint(GenericViewSet): # 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) + field_configs = {field[0]: field[1] for field in cls.get_field_configs(model).items() if field[0] in fields} + return get_serializer_class(model, fields, meta_fields=meta_fields, field_configs=field_configs, child_serializer_classes=child_serializer_classes, base=cls.base_serializer_class) def get_serializer_class(self): request = self.request diff --git a/wagtail/api/v2/serializers.py b/wagtail/api/v2/serializers.py index e44f56957..06d824059 100644 --- a/wagtail/api/v2/serializers.py +++ b/wagtail/api/v2/serializers.py @@ -337,13 +337,20 @@ class PageSerializer(BaseSerializer): return super(PageSerializer, self).build_relational_field(field_name, relation_info) -def get_serializer_class(model_, fields_, meta_fields, child_serializer_classes=None, base=BaseSerializer): +def get_serializer_class(model, field_names, meta_fields, field_configs=None, child_serializer_classes=None, base=BaseSerializer): + model_ = model + class Meta: model = model_ - fields = list(fields_) + fields = list(field_names) - return type(str(model_.__name__ + 'Serializer'), (base, ), { + attrs = { 'Meta': Meta, 'meta_fields': list(meta_fields), 'child_serializer_classes': child_serializer_classes or {}, - }) + } + + if field_configs: + attrs.update(field_configs) + + return type(str(model_.__name__ + 'Serializer'), (base, ), attrs) diff --git a/wagtail/api/v2/tests/test_pages.py b/wagtail/api/v2/tests/test_pages.py index ceb8c24c0..8885dcb96 100644 --- a/wagtail/api/v2/tests/test_pages.py +++ b/wagtail/api/v2/tests/test_pages.py @@ -188,7 +188,7 @@ class TestPageListing(TestCase): 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.keys()), {'id', 'meta', 'title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'}) 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): @@ -196,7 +196,7 @@ class TestPageListing(TestCase): 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.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'}) 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): @@ -809,6 +809,12 @@ class TestPageDetail(TestCase): self.assertEqual(content['feed_image']['meta']['type'], 'wagtailimages.Image') self.assertEqual(content['feed_image']['meta']['detail_url'], 'http://localhost/api/v2beta/images/7/') + # Check that the feed images' thumbnail was serialised properly + self.assertEqual(content['feed_image_thumbnail'], { + # This is OK because it tells us it used ImageRenditionField to generate the output + 'error': 'SourceImageIOError' + }) + # Check that the child relations were serialised properly self.assertEqual(content['related_links'], []) for carousel_item in content['carousel_items']: @@ -838,6 +844,7 @@ class TestPageDetail(TestCase): 'tags', 'date', 'feed_image', + 'feed_image_thumbnail', 'carousel_items', 'related_links', ] diff --git a/wagtail/tests/demosite/models.py b/wagtail/tests/demosite/models.py index 42073d648..c3bf9bdbb 100644 --- a/wagtail/tests/demosite/models.py +++ b/wagtail/tests/demosite/models.py @@ -16,6 +16,8 @@ from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtailsearch import index +from wagtail.wagtailimages.api.fields import ImageRenditionField + # ABSTRACT MODELS # ============================= @@ -280,6 +282,7 @@ class BlogEntryPage(Page): 'tags', 'date', 'feed_image', + ('feed_image_thumbnail', ImageRenditionField('fill-300x300', source='feed_image')), 'carousel_items', 'related_links', ) From 161d55565b702ba4cff3e79a5ee4d70d2be1fb7e Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 30 Mar 2017 11:08:33 +0100 Subject: [PATCH 04/10] Updated to use APIField class in api_fields Thanks to @BertrandBordage for the suggestion! --- wagtail/api/__init__.py | 3 +++ wagtail/api/conf.py | 13 +++++++++++++ wagtail/api/v2/endpoints.py | 13 +++++++------ wagtail/tests/demosite/models.py | 18 +++++++++--------- 4 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 wagtail/api/conf.py diff --git a/wagtail/api/__init__.py b/wagtail/api/__init__.py index 2e796b138..bc8dad9ae 100644 --- a/wagtail/api/__init__.py +++ b/wagtail/api/__init__.py @@ -1 +1,4 @@ +from .conf import APIField # noqa + + default_app_config = 'wagtail.api.apps.WagtailAPIAppConfig' diff --git a/wagtail/api/conf.py b/wagtail/api/conf.py new file mode 100644 index 000000000..65a9169c5 --- /dev/null +++ b/wagtail/api/conf.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import, unicode_literals + + +class APIField(object): + def __init__(self, name, serializer=None): + self.name = name + self.serializer = serializer + + def __hash__(self): + return hash(self.name) + + def __repr__(self): + return ''.format(self.name) diff --git a/wagtail/api/v2/endpoints.py b/wagtail/api/v2/endpoints.py index 578703ead..98f37c192 100644 --- a/wagtail/api/v2/endpoints.py +++ b/wagtail/api/v2/endpoints.py @@ -13,6 +13,7 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from wagtail.api import APIField from wagtail.wagtailcore.models import Page from .filters import ( @@ -103,7 +104,7 @@ class BaseAPIEndpoint(GenericViewSet): if hasattr(model, 'api_fields'): fields.extend([ - field[0] if isinstance(field, tuple) else field + field.name if isinstance(field, APIField) else field for field in model.api_fields ]) @@ -119,7 +120,7 @@ class BaseAPIEndpoint(GenericViewSet): if hasattr(model, 'api_meta_fields'): meta_fields.extend([ - field[0] if isinstance(field, tuple) else field + field.name if isinstance(field, APIField) else field for field in model.api_meta_fields ]) @@ -131,16 +132,16 @@ class BaseAPIEndpoint(GenericViewSet): if hasattr(model, 'api_fields'): configs.update({ - field[0]: field[1] + field.name: field.serializer for field in model.api_fields - if isinstance(field, tuple) + if isinstance(field, APIField) and field.serializer is not None }) if hasattr(model, 'api_meta_fields'): configs.update({ - field[0]: field[1] + field.name: field.serializer for field in model.api_meta_fields - if isinstance(field, tuple) + if isinstance(field, APIField) and field.serializer is not None }) return configs diff --git a/wagtail/tests/demosite/models.py b/wagtail/tests/demosite/models.py index c3bf9bdbb..d6d07cea0 100644 --- a/wagtail/tests/demosite/models.py +++ b/wagtail/tests/demosite/models.py @@ -7,17 +7,17 @@ from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey from taggit.models import TaggedItemBase +from wagtail.api import APIField from wagtail.utils.pagination import paginate from wagtail.wagtailadmin.edit_handlers import ( FieldPanel, InlinePanel, MultiFieldPanel, PageChooserPanel) from wagtail.wagtailcore.fields import RichTextField from wagtail.wagtailcore.models import Orderable, Page from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel +from wagtail.wagtailimages.api.fields import ImageRenditionField from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtailsearch import index -from wagtail.wagtailimages.api.fields import ImageRenditionField - # ABSTRACT MODELS # ============================= @@ -278,13 +278,13 @@ class BlogEntryPage(Page): ) api_fields = ( - 'body', - 'tags', - 'date', - 'feed_image', - ('feed_image_thumbnail', ImageRenditionField('fill-300x300', source='feed_image')), - 'carousel_items', - 'related_links', + APIField('body'), + APIField('tags'), + APIField('date'), + APIField('feed_image'), + APIField('feed_image_thumbnail', serializer=ImageRenditionField('fill-300x300', source='feed_image')), + APIField('carousel_items'), + APIField('related_links'), ) search_fields = Page.search_fields + [ From 84d6262ca565ded69c0f324d2aae468e8a12b808 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 30 Mar 2017 11:12:08 +0100 Subject: [PATCH 05/10] Renamed field_configs to field_serializer_overrides --- wagtail/api/v2/endpoints.py | 21 ++++++++++++++------- wagtail/api/v2/serializers.py | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/wagtail/api/v2/endpoints.py b/wagtail/api/v2/endpoints.py index 98f37c192..73057e1f1 100644 --- a/wagtail/api/v2/endpoints.py +++ b/wagtail/api/v2/endpoints.py @@ -127,24 +127,24 @@ class BaseAPIEndpoint(GenericViewSet): return meta_fields @classmethod - def get_field_configs(cls, model): - configs = {} + def get_field_serializer_overrides(cls, model): + serializers = {} if hasattr(model, 'api_fields'): - configs.update({ + serializers.update({ field.name: field.serializer for field in model.api_fields if isinstance(field, APIField) and field.serializer is not None }) if hasattr(model, 'api_meta_fields'): - configs.update({ + serializers.update({ field.name: field.serializer for field in model.api_meta_fields if isinstance(field, APIField) and field.serializer is not None }) - return configs + return serializers @classmethod def get_available_fields(cls, model, db_fields_only=False): @@ -284,8 +284,15 @@ class BaseAPIEndpoint(GenericViewSet): # Reorder fields so it matches the order of all_fields fields = [field for field in all_fields if field in fields] - field_configs = {field[0]: field[1] for field in cls.get_field_configs(model).items() if field[0] in fields} - return get_serializer_class(model, fields, meta_fields=meta_fields, field_configs=field_configs, child_serializer_classes=child_serializer_classes, base=cls.base_serializer_class) + field_serializer_overrides = {field[0]: field[1] for field in cls.get_field_serializer_overrides(model).items() if field[0] in fields} + return get_serializer_class( + model, + fields, + meta_fields=meta_fields, + field_serializer_overrides=field_serializer_overrides, + child_serializer_classes=child_serializer_classes, + base=cls.base_serializer_class + ) def get_serializer_class(self): request = self.request diff --git a/wagtail/api/v2/serializers.py b/wagtail/api/v2/serializers.py index 06d824059..7371d419f 100644 --- a/wagtail/api/v2/serializers.py +++ b/wagtail/api/v2/serializers.py @@ -337,7 +337,7 @@ class PageSerializer(BaseSerializer): return super(PageSerializer, self).build_relational_field(field_name, relation_info) -def get_serializer_class(model, field_names, meta_fields, field_configs=None, child_serializer_classes=None, base=BaseSerializer): +def get_serializer_class(model, field_names, meta_fields, field_serializer_overrides=None, child_serializer_classes=None, base=BaseSerializer): model_ = model class Meta: @@ -350,7 +350,7 @@ def get_serializer_class(model, field_names, meta_fields, field_configs=None, ch 'child_serializer_classes': child_serializer_classes or {}, } - if field_configs: - attrs.update(field_configs) + if field_serializer_overrides: + attrs.update(field_serializer_overrides) return type(str(model_.__name__ + 'Serializer'), (base, ), attrs) From 726e85f4a6c61aa403a829c0a531aac81153b80a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 6 Apr 2017 09:41:19 +0100 Subject: [PATCH 06/10] Simplified API fields getter methods --- wagtail/api/v2/endpoints.py | 63 +++++++++++-------------------------- 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/wagtail/api/v2/endpoints.py b/wagtail/api/v2/endpoints.py index 73057e1f1..ba81ba2f8 100644 --- a/wagtail/api/v2/endpoints.py +++ b/wagtail/api/v2/endpoints.py @@ -94,57 +94,32 @@ class BaseAPIEndpoint(GenericViewSet): return Response(data, status=status.HTTP_400_BAD_REQUEST) return super(BaseAPIEndpoint, self).handle_exception(exc) + @classmethod + def _convert_api_fields(cls, fields): + return [field if isinstance(field, APIField) else APIField(field) + for field in fields] + @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 = cls.body_fields[:] + return cls._convert_api_fields(cls.body_fields + list(getattr(model, 'api_fields', ()))) - if hasattr(model, 'api_fields'): - fields.extend([ - field.name if isinstance(field, APIField) else field - for field in model.api_fields - ]) - - return fields + @classmethod + def get_body_fields_names(cls, model): + return [field.name for field in cls.get_body_fields(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 = cls.meta_fields[:] + return cls._convert_api_fields(cls.meta_fields + list(getattr(model, 'api_meta_fields', ()))) - if hasattr(model, 'api_meta_fields'): - meta_fields.extend([ - field.name if isinstance(field, APIField) else field - for field in model.api_meta_fields - ]) - - return meta_fields + @classmethod + def get_meta_fields_names(cls, model): + return [field.name for field in cls.get_meta_fields(model)] @classmethod def get_field_serializer_overrides(cls, model): - serializers = {} - - if hasattr(model, 'api_fields'): - serializers.update({ - field.name: field.serializer - for field in model.api_fields - if isinstance(field, APIField) and field.serializer is not None - }) - - if hasattr(model, 'api_meta_fields'): - serializers.update({ - field.name: field.serializer - for field in model.api_meta_fields - if isinstance(field, APIField) and field.serializer is not None - }) - - return serializers + return {field.name: field.serializer + for field in cls.get_body_fields(model) + cls.get_meta_fields(model) + if field.serializer is not None} @classmethod def get_available_fields(cls, model, db_fields_only=False): @@ -156,7 +131,7 @@ class BaseAPIEndpoint(GenericViewSet): 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) + fields = cls.get_body_fields_names(model) + cls.get_meta_fields_names(model) if db_fields_only: # Get list of available database fields then remove any fields in our @@ -199,8 +174,8 @@ class BaseAPIEndpoint(GenericViewSet): @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) + body_fields = cls.get_body_fields_names(model) + meta_fields = cls.get_meta_fields_names(model) all_fields = body_fields + meta_fields # Remove any duplicates From 12fa1ebee8b561e547abd5fe41849d0f87ac5281 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 6 Apr 2017 10:02:46 +0100 Subject: [PATCH 07/10] Fix API v1 tests We're updated the BlogEntryPage model to use class-based api_fields but API v1 doesn't support them. This commit adds enough compatibility to make the v1 API tests work but issues a warning if the v1 API module encounters any new style configs that it doesn't support. --- wagtail/contrib/wagtailapi/endpoints.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index b39049497..d9041502a 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import warnings from collections import OrderedDict from django.conf.urls import url @@ -10,6 +11,7 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from wagtail.api import APIField from wagtail.wagtailcore.models import Page from wagtail.wagtailcore.utils import resolve_model_string from wagtail.wagtaildocs.models import get_document_model @@ -80,7 +82,23 @@ class BaseAPIEndpoint(GenericViewSet): if hasattr(model, 'api_fields'): api_fields.extend(model.api_fields) - return api_fields + # Remove any new-style API field configs (only supported in v2) + def convert_api_fields(fields): + for field in fields: + if isinstance(field, APIField): + warnings.warn( + "class-based api_fields are not supported by the v1 API module. " + "Please update the .api_fields attribute of {}.{} or update to the " + "v2 API.".format(model._meta.app_label, model.__name__) + ) + + # Ignore fields with custom serializers + if field.serializer is None: + yield field.name + else: + yield field + + return list(convert_api_fields(api_fields)) def check_query_parameters(self, queryset): """ From 7e160edad67d58285edc21a1a7d4c3a61cb1c7bf Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 6 Apr 2017 15:17:57 +0100 Subject: [PATCH 08/10] Docs --- docs/advanced_topics/api/v2/configuration.rst | 119 +++++++++++++++++- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/docs/advanced_topics/api/v2/configuration.rst b/docs/advanced_topics/api/v2/configuration.rst index 64a3cea9d..0e669aef3 100644 --- a/docs/advanced_topics/api/v2/configuration.rst +++ b/docs/advanced_topics/api/v2/configuration.rst @@ -111,11 +111,15 @@ For example: # blog/models.py + from wagtail.api import APIField + class BlogPageAuthor(Orderable): page = models.ForeignKey('blog.BlogPage', related_name='authors') name = models.CharField(max_length=255) - api_fields = ['name'] + api_fields = [ + APIField('name'), + ] class BlogPage(Page): @@ -126,10 +130,10 @@ For example: # Export fields over the API api_fields = [ - 'published_date', - 'body', - 'feed_image', - 'authors', # This will nest the relevant BlogPageAuthor objects in the API response + APIField('published_date'), + APIField('body'), + APIField('feed_image'), + APIField('authors'), # This will nest the relevant BlogPageAuthor objects in the API response ] This will make ``published_date``, ``body``, ``feed_image`` and a list of @@ -137,6 +141,111 @@ This will make ``published_date``, ``body``, ``feed_image`` and a list of fields, you must select the ``blog.BlogPage`` type using the ``?type`` :ref:`parameter in the API itself `. +Custom serialisers +------------------ + +.. versionadded: 1.10 + +Serialisers_ are used to convert the database representation of a model into +something that can be sent over the wire in JSON format. You can override the +serialiser for any field using the ``serializer`` keyword argument: + +.. code-block:: python + + from rest_framework.fields import DateField + + class BlogPage(Page): + ... + + api_fields = [ + # Change the format of the published_date field to "Thursday 06 April 2017" + APIField('published_date', serializer=DateField(format='%A $d %B %Y')), + ... + ] + +Django REST framework's serializers can all take a source_ argument allowing you +to add API fields that have a different field name or no underlying field at all: + +.. code-block:: python + + from rest_framework.fields import DateField + + class BlogPage(Page): + ... + + api_fields = [ + # Date in ISO8601 format (the default) + APIField('published_date'), + + # A separate published_date_display field with a different format + APIField('published_date_display', serializer=DateField(format='%A $d %B %Y', source='published_date')), + ... + ] + +This adds two fields to the API for the same underlying database field +(other fields omitted for brevity): + +.. code-block:: json + + { + "published_date": "2017-04-06", + "published_date_display": "Thursday 06 April 2017" + } + +.. _Serialisers: http://www.django-rest-framework.org/api-guide/fields/ +.. _source: http://www.django-rest-framework.org/api-guide/fields/#source + +Images in the API +----------------- + +.. versionadded: 1.10 + +The :class:`~wagtail.wagtailimages.api.fields.ImageRenditionField` serialiser +allows you to add renditions of images into your API. It requires an image +filter string specifying the resize operations to perform on the image. It can +also take the ``source`` keyword argument described above. + +For example: + +.. code-block:: python + + from wagtail.wagtailimages.api.fields.ImageRenditionField + + class BlogPage(Page): + ... + + api_fields = [ + # Adds information about the source image (eg, title) into the API + APIField('feed_image'), + + # Adds a URL to a rendered thumbnail of the image to the API + APIField('feed_image_thumbnail', serializer=ImageRenditionField('fill-100x100', source='feed_image')), + ... + ] + +This would add the following to the reponse: + +.. code-block:: json + + { + "feed_image": { + "id": 45529, + "meta": { + "type": "wagtailimages.Image", + "detail_url": "http://www.example.com/api/v2/images/12/", + "tags": [] + }, + "title": "A test image", + "width": 2000, + "height": 1125 + }, + "feed_image_thumbnail": { + "url": "http://www.example.com/media/images/a_test_image.fill-100x100.jpg", + "width": 100, + "height": 100 + } + } + Additional settings =================== From 141c1d65683790ee948c0c42608df6bc51fd8eaa Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 6 Apr 2017 15:21:43 +0100 Subject: [PATCH 09/10] Fixed admin API tests --- wagtail/wagtailadmin/tests/api/test_pages.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/tests/api/test_pages.py b/wagtail/wagtailadmin/tests/api/test_pages.py index 9673d0365..8c5d03b69 100644 --- a/wagtail/wagtailadmin/tests/api/test_pages.py +++ b/wagtail/wagtailadmin/tests/api/test_pages.py @@ -138,7 +138,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing): 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.keys()), {'id', 'meta', 'title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'}) 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): @@ -146,7 +146,7 @@ class TestAdminPageListing(AdminAPITestCase, TestPageListing): 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.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'}) 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): @@ -413,6 +413,7 @@ class TestAdminPageDetail(AdminAPITestCase, TestPageDetail): 'tags', 'date', 'feed_image', + 'feed_image_thumbnail', 'carousel_items', 'related_links', '__types', From fd9db9335ab19f563766918460c03593fbd29259 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 6 Apr 2017 15:27:34 +0100 Subject: [PATCH 10/10] Docs edits --- docs/advanced_topics/api/v2/configuration.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/advanced_topics/api/v2/configuration.rst b/docs/advanced_topics/api/v2/configuration.rst index 0e669aef3..6e755c828 100644 --- a/docs/advanced_topics/api/v2/configuration.rst +++ b/docs/advanced_topics/api/v2/configuration.rst @@ -147,8 +147,8 @@ Custom serialisers .. versionadded: 1.10 Serialisers_ are used to convert the database representation of a model into -something that can be sent over the wire in JSON format. You can override the -serialiser for any field using the ``serializer`` keyword argument: +JSON format. You can override the serialiser for any field using the +``serializer`` keyword argument: .. code-block:: python @@ -182,8 +182,7 @@ to add API fields that have a different field name or no underlying field at all ... ] -This adds two fields to the API for the same underlying database field -(other fields omitted for brevity): +This adds two fields to the API (other fields omitted for brevity): .. code-block:: json @@ -223,7 +222,7 @@ For example: ... ] -This would add the following to the reponse: +This would add the following to the JSON: .. code-block:: json