From 0dc92bd8f992c9f24304a499fad9d31f17c4824e Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Wed, 2 Aug 2017 20:50:17 -0500 Subject: [PATCH 01/10] - basic HalListSerializer implementation - adapt HalModelSerializer to use HalListSerializer --- drf_hal_json/serializers.py | 44 ++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index 19f5696..97d0be1 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -1,14 +1,50 @@ from collections import defaultdict +import rest_framework +from rest_framework.reverse import reverse +from rest_framework.utils.serializer_helpers import ReturnDict + from drf_hal_json import EMBEDDED_FIELD_NAME, LINKS_FIELD_NAME, URL_FIELD_NAME from drf_hal_json.fields import HalHyperlinkedPropertyField, HalContributeToLinkField, \ HalHyperlinkedSerializerMethodField from rest_framework.fields import empty, FileField, ImageField from rest_framework.relations import HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, RelatedField -from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer +from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer, ListSerializer from rest_framework.utils.field_mapping import get_nested_relation_kwargs +class HalListSerializer(ListSerializer): + + @property + def data(self): + # The parent class returns ReturnList + ret = super(ListSerializer, self).data + return ReturnDict(ret, serializer=self) + + def to_representation(self, collection): + # Wrap the standard ListSerializer in _embedded and populate _links with the URL of the list + return { + LINKS_FIELD_NAME: { + URL_FIELD_NAME: {'href': self.build_view_url()}, + }, + EMBEDDED_FIELD_NAME: super(HalListSerializer, self).to_representation(collection) + } + + def build_view_url(self): + # Deduce the URL of the view from the Model + model = getattr(self.child.Meta, 'model') + # Support a Meta attribute for adjusting the base_name + base_name = getattr(self.child.Meta, 'base_name', None) + if base_name is None: + # basic approach from rest_framework.utils.field_mapping.get_detail_view_name + base_name = '%(model_name)s' % { + 'app_label': model._meta.app_label, + 'model_name': model._meta.object_name.lower() + } + view_name = '{}-list'.format(base_name) + return reverse(view_name, request=self._context['request']) + + class HalModelSerializer(HyperlinkedModelSerializer): """ Serializer for HAL representation of django models @@ -21,6 +57,12 @@ def __init__(self, instance=None, data=empty, **kwargs): if data != empty and not LINKS_FIELD_NAME in data: data[LINKS_FIELD_NAME] = dict() # put links in data, so that field validation does not fail + @classmethod + def many_init(cls, *args, **kwargs): + # implementation recommended by ListSerializer.many_init + kwargs['child'] = cls() + return HalListSerializer(*args, **kwargs) + def build_link_object(self, val): if (type([]) == type(val)): return [self.build_link_object(v) for v in val] From 70bc1b56ffc6582a9afaa4f19531d1c3525f030f Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 10:01:03 -0500 Subject: [PATCH 02/10] - fully replicate `ListSerializer.many_init` logic (with HalListSerializer default) --- drf_hal_json/serializers.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index 97d0be1..f00b656 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -9,7 +9,8 @@ HalHyperlinkedSerializerMethodField from rest_framework.fields import empty, FileField, ImageField from rest_framework.relations import HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, RelatedField -from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer, ListSerializer +from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer, ListSerializer, \ + LIST_SERIALIZER_KWARGS from rest_framework.utils.field_mapping import get_nested_relation_kwargs @@ -59,9 +60,21 @@ def __init__(self, instance=None, data=empty, **kwargs): @classmethod def many_init(cls, *args, **kwargs): - # implementation recommended by ListSerializer.many_init - kwargs['child'] = cls() - return HalListSerializer(*args, **kwargs) + # duplicate ListSerializer.many_init due to Meta dependency (injecting a value was not possible) + allow_empty = kwargs.pop('allow_empty', None) + child_serializer = cls(*args, **kwargs) + list_kwargs = { + 'child': child_serializer, + } + if allow_empty is not None: + list_kwargs['allow_empty'] = allow_empty + list_kwargs.update({ + key: value for key, value in kwargs.items() + if key in LIST_SERIALIZER_KWARGS + }) + meta = getattr(cls, 'Meta', None) + list_serializer_class = getattr(meta, 'list_serializer_class', HalListSerializer) + return list_serializer_class(*args, **kwargs) def build_link_object(self, val): if (type([]) == type(val)): From 6b51dd2a26f3dcb0bf786e80a9d00e3022a4de19 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 10:31:10 -0500 Subject: [PATCH 03/10] - remove vestigial import --- drf_hal_json/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index f00b656..b0ec104 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -1,6 +1,5 @@ from collections import defaultdict -import rest_framework from rest_framework.reverse import reverse from rest_framework.utils.serializer_helpers import ReturnDict From e6b0cc227b210d1487227bfcb0510fa784df4442 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 10:43:03 -0500 Subject: [PATCH 04/10] - fix typo in documentation and implementation --- drf_hal_json/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index b0ec104..b084eb4 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -59,7 +59,7 @@ def __init__(self, instance=None, data=empty, **kwargs): @classmethod def many_init(cls, *args, **kwargs): - # duplicate ListSerializer.many_init due to Meta dependency (injecting a value was not possible) + # duplicate BaseSerializer.many_init due to Meta dependency (injecting a value was not possible) allow_empty = kwargs.pop('allow_empty', None) child_serializer = cls(*args, **kwargs) list_kwargs = { @@ -73,7 +73,7 @@ def many_init(cls, *args, **kwargs): }) meta = getattr(cls, 'Meta', None) list_serializer_class = getattr(meta, 'list_serializer_class', HalListSerializer) - return list_serializer_class(*args, **kwargs) + return list_serializer_class(*args, **list_kwargs) def build_link_object(self, val): if (type([]) == type(val)): From f8c402cafdd22feca2bf820060114b9dd215bbe8 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 12:25:34 -0500 Subject: [PATCH 05/10] - add explicit context check (from `HyperlinkedRelatedField.to_representation`) - use correct context object (`context` not `_context`) --- drf_hal_json/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index b084eb4..58e4e2c 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -22,6 +22,12 @@ def data(self): return ReturnDict(ret, serializer=self) def to_representation(self, collection): + # cribbed from HyperlinkedRelatedField.to_representation + assert 'request' in self.context, ( + "`%s` requires the request in the serializer" + " context. Add `context={'request': request}` when instantiating " + "the serializer." % self.__class__.__name__ + ) # Wrap the standard ListSerializer in _embedded and populate _links with the URL of the list return { LINKS_FIELD_NAME: { @@ -42,7 +48,7 @@ def build_view_url(self): 'model_name': model._meta.object_name.lower() } view_name = '{}-list'.format(base_name) - return reverse(view_name, request=self._context['request']) + return reverse(view_name, request=self.context['request']) class HalModelSerializer(HyperlinkedModelSerializer): From 62cc0c83d9b48b707dc75f464958d230989502e8 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 13:55:50 -0500 Subject: [PATCH 06/10] - more DRY approach to `many_init` - `HalRelatedModelSerializer` to correctly render related fields (i.e. without `embedded`) - fix test harness to use `HalRelatedModelSerializer` --- drf_hal_json/serializers.py | 32 ++++++++++++++++---------------- tests/testproject/serializers.py | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index 58e4e2c..c561fc0 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -8,8 +8,7 @@ HalHyperlinkedSerializerMethodField from rest_framework.fields import empty, FileField, ImageField from rest_framework.relations import HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, RelatedField -from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer, ListSerializer, \ - LIST_SERIALIZER_KWARGS +from rest_framework.serializers import BaseSerializer, HyperlinkedModelSerializer, ListSerializer from rest_framework.utils.field_mapping import get_nested_relation_kwargs @@ -56,6 +55,7 @@ class HalModelSerializer(HyperlinkedModelSerializer): Serializer for HAL representation of django models """ serializer_related_field = HyperlinkedRelatedField + default_list_serializer = HalListSerializer def __init__(self, instance=None, data=empty, **kwargs): super(HalModelSerializer, self).__init__(instance, data, **kwargs) @@ -65,21 +65,17 @@ def __init__(self, instance=None, data=empty, **kwargs): @classmethod def many_init(cls, *args, **kwargs): - # duplicate BaseSerializer.many_init due to Meta dependency (injecting a value was not possible) - allow_empty = kwargs.pop('allow_empty', None) - child_serializer = cls(*args, **kwargs) - list_kwargs = { - 'child': child_serializer, - } - if allow_empty is not None: - list_kwargs['allow_empty'] = allow_empty - list_kwargs.update({ - key: value for key, value in kwargs.items() - if key in LIST_SERIALIZER_KWARGS - }) + # inject the default into list_serializer_class (if not present) meta = getattr(cls, 'Meta', None) - list_serializer_class = getattr(meta, 'list_serializer_class', HalListSerializer) - return list_serializer_class(*args, **list_kwargs) + if meta is None: + class Meta: + pass + meta = Meta + setattr(cls, 'Meta', meta) + list_serializer_class = getattr(meta, 'list_serializer_class', None) + if list_serializer_class is None: + setattr(meta, 'list_serializer_class', cls.default_list_serializer) + return super(HalModelSerializer, cls).many_init(*args, **kwargs) def build_link_object(self, val): if (type([]) == type(val)): @@ -169,3 +165,7 @@ class Meta: field_kwargs = get_nested_relation_kwargs(relation_info) return field_class, field_kwargs + + +class HalRelatedModelSerializer(HalModelSerializer): + default_list_serializer = ListSerializer diff --git a/tests/testproject/serializers.py b/tests/testproject/serializers.py index a14a233..34a8ddb 100644 --- a/tests/testproject/serializers.py +++ b/tests/testproject/serializers.py @@ -1,13 +1,13 @@ from drf_hal_json.fields import (HalContributeToLinkField, HalHyperlinkedIdentityField, HalHyperlinkedPropertyField, HalHyperlinkedRelatedField, HalHyperlinkedSerializerMethodField) -from drf_hal_json.serializers import HalModelSerializer +from drf_hal_json.serializers import HalModelSerializer, HalRelatedModelSerializer from rest_framework import serializers from .models import (AbundantResource, CustomResource, FileResource, RelatedResource1, RelatedResource2, RelatedResource3, TestResource, URLResource) -class RelatedResource1Serializer(HalModelSerializer): +class RelatedResource1Serializer(HalRelatedModelSerializer): name = HalContributeToLinkField(place_on='self', property_name='title') class Meta: From c77c3d0a686121d6b71ef672d40e7288d0b64445 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Thu, 3 Aug 2017 14:05:57 -0500 Subject: [PATCH 07/10] - use the distinction between `data` and `to_representation` to provide different structures to root (`data`) and nested (`to_representation`) uses --- drf_hal_json/serializers.py | 30 ++++++++++-------------------- tests/testproject/serializers.py | 4 ++-- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index c561fc0..71701dc 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -17,23 +17,17 @@ class HalListSerializer(ListSerializer): @property def data(self): # The parent class returns ReturnList - ret = super(ListSerializer, self).data - return ReturnDict(ret, serializer=self) - - def to_representation(self, collection): - # cribbed from HyperlinkedRelatedField.to_representation - assert 'request' in self.context, ( - "`%s` requires the request in the serializer" - " context. Add `context={'request': request}` when instantiating " - "the serializer." % self.__class__.__name__ - ) - # Wrap the standard ListSerializer in _embedded and populate _links with the URL of the list - return { - LINKS_FIELD_NAME: { - URL_FIELD_NAME: {'href': self.build_view_url()}, + return ReturnDict( + { + LINKS_FIELD_NAME: { + URL_FIELD_NAME: { + 'href': self.build_view_url() + } + }, + EMBEDDED_FIELD_NAME: super(ListSerializer, self).data }, - EMBEDDED_FIELD_NAME: super(HalListSerializer, self).to_representation(collection) - } + serializer=self + ) def build_view_url(self): # Deduce the URL of the view from the Model @@ -165,7 +159,3 @@ class Meta: field_kwargs = get_nested_relation_kwargs(relation_info) return field_class, field_kwargs - - -class HalRelatedModelSerializer(HalModelSerializer): - default_list_serializer = ListSerializer diff --git a/tests/testproject/serializers.py b/tests/testproject/serializers.py index 34a8ddb..a14a233 100644 --- a/tests/testproject/serializers.py +++ b/tests/testproject/serializers.py @@ -1,13 +1,13 @@ from drf_hal_json.fields import (HalContributeToLinkField, HalHyperlinkedIdentityField, HalHyperlinkedPropertyField, HalHyperlinkedRelatedField, HalHyperlinkedSerializerMethodField) -from drf_hal_json.serializers import HalModelSerializer, HalRelatedModelSerializer +from drf_hal_json.serializers import HalModelSerializer from rest_framework import serializers from .models import (AbundantResource, CustomResource, FileResource, RelatedResource1, RelatedResource2, RelatedResource3, TestResource, URLResource) -class RelatedResource1Serializer(HalRelatedModelSerializer): +class RelatedResource1Serializer(HalModelSerializer): name = HalContributeToLinkField(place_on='self', property_name='title') class Meta: From 8a01341da4a483b2637ee491ebf8151e979dd6b9 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Mon, 14 Aug 2017 15:56:19 -0500 Subject: [PATCH 08/10] - `embedded` was missing a key matching the `order` key at https://tools.ietf.org/html/draft-kelly-json-hal-08#section-6 (but uses `items` to match pagination package) --- drf_hal_json/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index 71701dc..d7f22f0 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -24,7 +24,10 @@ def data(self): 'href': self.build_view_url() } }, - EMBEDDED_FIELD_NAME: super(ListSerializer, self).data + EMBEDDED_FIELD_NAME: { + # `items` mirrors hardcoded value in pagination classes + 'items': super(ListSerializer, self).data + } }, serializer=self ) From fd45e709d2177b23478ef8a245fe5a967b374642 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Tue, 15 Aug 2017 16:55:15 -0500 Subject: [PATCH 09/10] - test that would fail for unpaged resources (without fix) --- tests/testproject/tests.py | 9 +++++++++ tests/testproject/urls.py | 3 ++- tests/testproject/views.py | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/testproject/tests.py b/tests/testproject/tests.py index 48043cb..d724e06 100644 --- a/tests/testproject/tests.py +++ b/tests/testproject/tests.py @@ -121,6 +121,15 @@ def test_pagination(self): self.assertIn("next", pages["_links"]) next_link = pages["_links"]["next"]["href"] + unpaged = self.client.get("/abundant-unpaged/").data + # Renders with _links and _embedded + self.assertIn("_links", unpaged) + self.assertIn("_embedded", unpaged) + self.assertIn("self", unpaged["_links"]) + # Includes no pages + self.assertNotIn("previous", unpaged["_links"]) + self.assertNotIn("next", unpaged["_links"]) + pages = self.client.get(next_link).data self.assertIn("self", pages["_links"]) self.assertIn("previous", pages["_links"]) diff --git a/tests/testproject/urls.py b/tests/testproject/urls.py index 8db125b..1b442b7 100644 --- a/tests/testproject/urls.py +++ b/tests/testproject/urls.py @@ -4,7 +4,7 @@ from .views import (AbundantResourceViewSet, CustomResourceViewSet, RelatedResource1ViewSet, RelatedResource2ViewSet, RelatedResource3ViewSet, TestResourceViewSet, - URLResourceViewSet, FileResourceViewSet) + URLResourceViewSet, FileResourceViewSet, AbundantUnpagedViewSet) router = DefaultRouter() router.register(r'test-resources', TestResourceViewSet) @@ -13,6 +13,7 @@ router.register(r'related-resources-3', RelatedResource3ViewSet) router.register(r'custom-resources', CustomResourceViewSet) router.register(r'abundant-resources', AbundantResourceViewSet) +router.register(r'abundant-unpaged', AbundantUnpagedViewSet) router.register(r'url-resources', URLResourceViewSet) router.register(r'file-resources', FileResourceViewSet) urlpatterns = router.urls diff --git a/tests/testproject/views.py b/tests/testproject/views.py index e9439bf..327a26d 100644 --- a/tests/testproject/views.py +++ b/tests/testproject/views.py @@ -43,6 +43,10 @@ class AbundantResourceViewSet(HalCreateModelMixin, ModelViewSet): queryset = AbundantResource.objects.all() +class AbundantUnpagedViewSet(AbundantResourceViewSet): + pagination_class = None + + class URLResourceViewSet(HalCreateModelMixin, ModelViewSet): serializer_class = HyperlinkedPropertySerializer queryset = URLResource.objects.all() From 4f2089526f1b3e0fe1bc152039de58a3ab6a0047 Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Tue, 26 Sep 2017 10:03:42 -0500 Subject: [PATCH 10/10] - use `build_absolute_uri` instead of custom logic --- drf_hal_json/serializers.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/drf_hal_json/serializers.py b/drf_hal_json/serializers.py index d7f22f0..63e4b5e 100644 --- a/drf_hal_json/serializers.py +++ b/drf_hal_json/serializers.py @@ -1,6 +1,5 @@ from collections import defaultdict -from rest_framework.reverse import reverse from rest_framework.utils.serializer_helpers import ReturnDict from drf_hal_json import EMBEDDED_FIELD_NAME, LINKS_FIELD_NAME, URL_FIELD_NAME @@ -21,7 +20,7 @@ def data(self): { LINKS_FIELD_NAME: { URL_FIELD_NAME: { - 'href': self.build_view_url() + 'href': self.context['request'].build_absolute_uri() } }, EMBEDDED_FIELD_NAME: { @@ -32,20 +31,6 @@ def data(self): serializer=self ) - def build_view_url(self): - # Deduce the URL of the view from the Model - model = getattr(self.child.Meta, 'model') - # Support a Meta attribute for adjusting the base_name - base_name = getattr(self.child.Meta, 'base_name', None) - if base_name is None: - # basic approach from rest_framework.utils.field_mapping.get_detail_view_name - base_name = '%(model_name)s' % { - 'app_label': model._meta.app_label, - 'model_name': model._meta.object_name.lower() - } - view_name = '{}-list'.format(base_name) - return reverse(view_name, request=self.context['request']) - class HalModelSerializer(HyperlinkedModelSerializer): """