From ffe61c6cd1a4604761cf85607f7e3f0899d10beb Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Tue, 31 Jul 2018 17:52:08 +0300 Subject: [PATCH 01/17] Pass related field name to "get_url" method --- rest_framework_json_api/relations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 1c51d2da..2719357d 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -116,7 +116,11 @@ def get_links(self, obj=None, lookup_field='pk'): }) self_link = self.get_url('self', self.self_link_view_name, self_kwargs, request) - related_kwargs = {self.related_link_url_kwarg: kwargs[self.related_link_lookup_field]} + if self.related_link_url_kwarg == 'pk': + related_kwargs = self_kwargs + else: + related_kwargs = {self.related_link_url_kwarg: kwargs[self.related_link_lookup_field]} + related_link = self.get_url('related', self.related_link_view_name, related_kwargs, request) if self_link: From 196d8ba7ee55369998976779548de3eefe4a4a78 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 3 Aug 2018 13:52:52 +0300 Subject: [PATCH 02/17] Added RelatedMixin --- example/serializers.py | 11 +++++++ example/urls.py | 4 +++ example/urls_test.py | 4 +++ example/views.py | 9 ++++-- rest_framework_json_api/views.py | 50 ++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index f43accac..72e5cd78 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -155,6 +155,17 @@ class Meta: class AuthorSerializer(serializers.ModelSerializer): + bio = relations.ResourceRelatedField( + related_link_view_name='author-related', + self_link_view_name='author-relationships', + queryset=AuthorBio.objects, + ) + entries = relations.ResourceRelatedField( + related_link_view_name='author-related', + self_link_view_name='author-relationships', + queryset=Entry.objects, + many=True + ) included_serializers = { 'bio': AuthorBioSerializer, 'type': AuthorTypeSerializer diff --git a/example/urls.py b/example/urls.py index 469ce53a..fa06499f 100644 --- a/example/urls.py +++ b/example/urls.py @@ -45,6 +45,10 @@ EntryViewSet.as_view({'get': 'retrieve'}), name='entry-featured'), + url(r'^authors/(?P[^/.]+)/(?P\w+)/$', + AuthorViewSet.as_view({'get': 'retrieve_related'}), + name='author-related'), + url(r'^entries/(?P[^/.]+)/relationships/(?P\w+)', EntryRelationshipView.as_view(), name='entry-relationships'), diff --git a/example/urls_test.py b/example/urls_test.py index 3ec07380..e7a27ce4 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -56,6 +56,10 @@ EntryViewSet.as_view({'get': 'retrieve'}), name='entry-featured'), + url(r'^authors/(?P[^/.]+)/(?P\w+)/$', + AuthorViewSet.as_view({'get': 'retrieve_related'}), + name='author-related'), + url(r'^entries/(?P[^/.]+)/relationships/(?P\w+)', EntryRelationshipView.as_view(), name='entry-relationships'), diff --git a/example/views.py b/example/views.py index 5dfc3341..16b724b5 100644 --- a/example/views.py +++ b/example/views.py @@ -7,10 +7,11 @@ import rest_framework_json_api.renderers from rest_framework_json_api.pagination import PageNumberPagination from rest_framework_json_api.utils import format_drf_errors -from rest_framework_json_api.views import ModelViewSet, RelationshipView +from rest_framework_json_api.views import ModelViewSet, RelatedMixin, RelationshipView from example.models import Author, Blog, Comment, Company, Entry, Project from example.serializers import ( + AuthorBioSerializer, AuthorSerializer, BlogSerializer, CommentSerializer, @@ -92,9 +93,13 @@ class NonPaginatedEntryViewSet(EntryViewSet): pagination_class = NoPagination -class AuthorViewSet(ModelViewSet): +class AuthorViewSet(RelatedMixin, ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer + related_serializers = { + 'bio': AuthorBioSerializer, + 'entries': EntrySerializer + } class CommentViewSet(ModelViewSet): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 64e5d12e..492d4931 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,3 +1,5 @@ +from collections import Iterable + from django.core.exceptions import ImproperlyConfigured from django.db.models import Model from django.db.models.fields.related_descriptors import ( @@ -98,6 +100,54 @@ def get_queryset(self, *args, **kwargs): return qs +class RelatedMixin(object): + """ + This mixin handles all related entities, whose Serializers are declared in "related_serializers" + """ + related_serializers = {} + field_name_mapping = {} + + def retrieve_related(self, request, *args, **kwargs): + serializer_kwargs = {} + instance = self.get_related_instance() + + if hasattr(instance, 'all'): + instance = instance.all() + + if callable(instance): + instance = instance() + + if instance is None: + return Response(data=None) + + if isinstance(instance, Iterable): + serializer_kwargs['many'] = True + + serializer = self.get_serializer(instance, **serializer_kwargs) + return Response(serializer.data) + + def get_serializer_class(self): + if 'related_field' in self.kwargs: + field_name = self.get_related_field_name() + _class = self.related_serializers.get(field_name, None) + if _class is None: + raise NotFound + return _class + return super(RelatedMixin, self).get_serializer_class() + + def get_related_field_name(self): + field_name = self.kwargs['related_field'] + if field_name in self.field_name_mapping: + return self.field_name_mapping[field_name] + return field_name + + def get_related_instance(self): + try: + return getattr(self.get_object(), self.get_related_field_name()) + except AttributeError: + raise NotFound + + class ModelViewSet(AutoPrefetchMixin, PrefetchForIncludesHelperMixin, viewsets.ModelViewSet): pass From fc52dc4d344e42c703c454ce4e21094de4cda348 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Wed, 8 Aug 2018 11:44:45 +0300 Subject: [PATCH 03/17] Inherit ModelViewSet and ReadOnlyModelViewSet from RelatedMixin --- rest_framework_json_api/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 492d4931..88781e5a 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -148,12 +148,16 @@ def get_related_instance(self): raise NotFound -class ModelViewSet(AutoPrefetchMixin, PrefetchForIncludesHelperMixin, viewsets.ModelViewSet): +class ModelViewSet(AutoPrefetchMixin, + PrefetchForIncludesHelperMixin, + RelatedMixin, + viewsets.ModelViewSet): pass class ReadOnlyModelViewSet(AutoPrefetchMixin, PrefetchForIncludesHelperMixin, + RelatedMixin, viewsets.ReadOnlyModelViewSet): pass From 73d51a52a1b10133ed770cca8ea8e4e0edca5c17 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Wed, 8 Aug 2018 12:44:42 +0300 Subject: [PATCH 04/17] Use dotted path when declaring serializers --- example/views.py | 9 ++++----- rest_framework_json_api/views.py | 7 ++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/views.py b/example/views.py index 16b724b5..6f765e70 100644 --- a/example/views.py +++ b/example/views.py @@ -7,11 +7,10 @@ import rest_framework_json_api.renderers from rest_framework_json_api.pagination import PageNumberPagination from rest_framework_json_api.utils import format_drf_errors -from rest_framework_json_api.views import ModelViewSet, RelatedMixin, RelationshipView +from rest_framework_json_api.views import ModelViewSet, RelationshipView from example.models import Author, Blog, Comment, Company, Entry, Project from example.serializers import ( - AuthorBioSerializer, AuthorSerializer, BlogSerializer, CommentSerializer, @@ -93,12 +92,12 @@ class NonPaginatedEntryViewSet(EntryViewSet): pagination_class = NoPagination -class AuthorViewSet(RelatedMixin, ModelViewSet): +class AuthorViewSet(ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer related_serializers = { - 'bio': AuthorBioSerializer, - 'entries': EntrySerializer + 'bio': 'example.serializers.AuthorBioSerializer', + 'entries': 'example.serializers.EntrySerializer' } diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 88781e5a..61bdbe1b 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -11,6 +11,7 @@ from django.db.models.manager import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch +from django.utils.module_loading import import_string as import_class_from_dotted_path from rest_framework import generics, viewsets from rest_framework.exceptions import MethodNotAllowed, NotFound from rest_framework.response import Response @@ -129,10 +130,10 @@ def retrieve_related(self, request, *args, **kwargs): def get_serializer_class(self): if 'related_field' in self.kwargs: field_name = self.get_related_field_name() - _class = self.related_serializers.get(field_name, None) - if _class is None: + class_str = self.related_serializers.get(field_name, None) + if class_str is None: raise NotFound - return _class + return import_class_from_dotted_path(class_str) return super(RelatedMixin, self).get_serializer_class() def get_related_field_name(self): From 76770426592bbe2db40b1b61cb98e8c59f683ecd Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Wed, 8 Aug 2018 14:21:53 +0300 Subject: [PATCH 05/17] Added doc string to HyperlinkedMixin --- rest_framework_json_api/relations.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 2719357d..5c9c37b1 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -116,6 +116,12 @@ def get_links(self, obj=None, lookup_field='pk'): }) self_link = self.get_url('self', self.self_link_view_name, self_kwargs, request) + """ + Assuming RelatedField will be declared in two ways: + 1. url(r'^authors/(?P[^/.]+)/(?P\w+)/$', AuthorViewSet.as_view({'get': 'retrieve_related'})) + 2. url(r'^authors/(?P[^/.]+)/bio/$', AuthorBioViewSet.as_view({'get': 'retrieve'})) + In other words if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse() + """ if self.related_link_url_kwarg == 'pk': related_kwargs = self_kwargs else: From bde21ec6d09738533bb605dc4b3e8c56cf1e699e Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Thu, 9 Aug 2018 13:31:23 +0300 Subject: [PATCH 06/17] Get field name from serializer class --- rest_framework_json_api/views.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 61bdbe1b..4fc40566 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -106,7 +106,6 @@ class RelatedMixin(object): This mixin handles all related entities, whose Serializers are declared in "related_serializers" """ related_serializers = {} - field_name_mapping = {} def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -129,7 +128,7 @@ def retrieve_related(self, request, *args, **kwargs): def get_serializer_class(self): if 'related_field' in self.kwargs: - field_name = self.get_related_field_name() + field_name = self.kwargs['related_field'] class_str = self.related_serializers.get(field_name, None) if class_str is None: raise NotFound @@ -138,8 +137,12 @@ def get_serializer_class(self): def get_related_field_name(self): field_name = self.kwargs['related_field'] - if field_name in self.field_name_mapping: - return self.field_name_mapping[field_name] + # Making sure we're getting correct model field/property/method name + try: + return super(RelatedMixin, self).get_serializer_class()().fields[field_name].source + except KeyError: + # Looks like the field was not declared on the serializer + pass return field_name def get_related_instance(self): From 9e78e6793248507c43fc7d13c95027fb0ef51531 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 10 Aug 2018 16:35:25 +0300 Subject: [PATCH 07/17] Use mapping for field name resolving --- rest_framework_json_api/views.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 4fc40566..3933e7a0 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -106,6 +106,7 @@ class RelatedMixin(object): This mixin handles all related entities, whose Serializers are declared in "related_serializers" """ related_serializers = {} + related_field_mapping = {} def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -137,12 +138,8 @@ def get_serializer_class(self): def get_related_field_name(self): field_name = self.kwargs['related_field'] - # Making sure we're getting correct model field/property/method name - try: - return super(RelatedMixin, self).get_serializer_class()().fields[field_name].source - except KeyError: - # Looks like the field was not declared on the serializer - pass + if field_name in self.related_field_mapping: + return self.related_field_mapping[field_name] return field_name def get_related_instance(self): From d9b5f084e56c2b22ae438fc8b42a3fee465e3ff6 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 10 Aug 2018 16:55:11 +0300 Subject: [PATCH 08/17] Improve serializer class resolving --- rest_framework_json_api/views.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 3933e7a0..79859dec 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -128,13 +128,26 @@ def retrieve_related(self, request, *args, **kwargs): return Response(serializer.data) def get_serializer_class(self): + parent_serializer_class = super(RelatedMixin, self).get_serializer_class() + if 'related_field' in self.kwargs: field_name = self.kwargs['related_field'] + + assert hasattr(parent_serializer_class, 'included_serializers') or self.related_serializers,\ + 'Either "included_serializers" or "related_serializers" should be configured' + + # Try get the class from related_serializers class_str = self.related_serializers.get(field_name, None) + if class_str is None: - raise NotFound + # Class was not found in related_serializers, look for it in included_serializers + class_str = getattr(self, 'included_serializers', {}).get(field_name, None) + + if class_str is None: + raise NotFound return import_class_from_dotted_path(class_str) - return super(RelatedMixin, self).get_serializer_class() + + return parent_serializer_class def get_related_field_name(self): field_name = self.kwargs['related_field'] From 9decc1f2be7ae59ee8655a7bc4de14c33b3de762 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 10 Aug 2018 16:57:15 +0300 Subject: [PATCH 09/17] Improved related instance resolving --- rest_framework_json_api/views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 79859dec..0d5f2dc8 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -156,10 +156,18 @@ def get_related_field_name(self): return field_name def get_related_instance(self): - try: - return getattr(self.get_object(), self.get_related_field_name()) - except AttributeError: - raise NotFound + parent_obj = self.get_object() + parent_serializer = self.serializer_class(parent_obj) + field_name = self.get_related_field_name() + field = parent_serializer.fields.get(field_name, None) + + if field is not None: + return field.get_attribute(parent_obj) + else: + try: + return getattr(parent_obj, field_name) + except AttributeError: + raise NotFound class ModelViewSet(AutoPrefetchMixin, From 77ac0b247a2a62bd34731e271f9ab46f0707917d Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 10 Aug 2018 16:58:26 +0300 Subject: [PATCH 10/17] Added SerializerMethodResourceRelatedField + RelatedMixin example --- example/serializers.py | 12 +++++++++++- example/views.py | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 72e5cd78..72859acb 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -166,6 +166,13 @@ class AuthorSerializer(serializers.ModelSerializer): queryset=Entry.objects, many=True ) + first_entry = relations.SerializerMethodResourceRelatedField( + related_link_view_name='author-related', + self_link_view_name='author-relationships', + model=Entry, + read_only=True, + source='get_first_entry' + ) included_serializers = { 'bio': AuthorBioSerializer, 'type': AuthorTypeSerializer @@ -173,7 +180,10 @@ class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author - fields = ('name', 'email', 'bio', 'entries', 'type') + fields = ('name', 'email', 'bio', 'entries', 'first_entry', 'type') + + def get_first_entry(self, obj): + return obj.entries.first() class WriterSerializer(serializers.ModelSerializer): diff --git a/example/views.py b/example/views.py index 6f765e70..eb5df390 100644 --- a/example/views.py +++ b/example/views.py @@ -97,7 +97,8 @@ class AuthorViewSet(ModelViewSet): serializer_class = AuthorSerializer related_serializers = { 'bio': 'example.serializers.AuthorBioSerializer', - 'entries': 'example.serializers.EntrySerializer' + 'entries': 'example.serializers.EntrySerializer', + 'first_entry': 'example.serializers.EntrySerializer' } From 6e0b47c879fd43f35b552f75470d92d224d0fd44 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 10 Aug 2018 17:17:27 +0300 Subject: [PATCH 11/17] Fix failing tox --- rest_framework_json_api/relations.py | 8 +++++--- rest_framework_json_api/views.py | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 5c9c37b1..040a84f1 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -118,9 +118,11 @@ def get_links(self, obj=None, lookup_field='pk'): """ Assuming RelatedField will be declared in two ways: - 1. url(r'^authors/(?P[^/.]+)/(?P\w+)/$', AuthorViewSet.as_view({'get': 'retrieve_related'})) - 2. url(r'^authors/(?P[^/.]+)/bio/$', AuthorBioViewSet.as_view({'get': 'retrieve'})) - In other words if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse() + 1. url(r'^authors/(?P[^/.]+)/(?P\w+)/$', + AuthorViewSet.as_view({'get': 'retrieve_related'})) + 2. url(r'^authors/(?P[^/.]+)/bio/$', + AuthorBioViewSet.as_view({'get': 'retrieve'})) + So, if related_link_url_kwarg == 'pk' it will add 'related_field' parameter to reverse() """ if self.related_link_url_kwarg == 'pk': related_kwargs = self_kwargs diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 0d5f2dc8..8af30367 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -133,8 +133,10 @@ def get_serializer_class(self): if 'related_field' in self.kwargs: field_name = self.kwargs['related_field'] - assert hasattr(parent_serializer_class, 'included_serializers') or self.related_serializers,\ - 'Either "included_serializers" or "related_serializers" should be configured' + assert hasattr(parent_serializer_class, 'included_serializers')\ + or self.related_serializers,\ + 'Either "included_serializers" or ' \ + '"related_serializers" should be configured' # Try get the class from related_serializers class_str = self.related_serializers.get(field_name, None) From a210e636e52bf5ecc8e29c7d56dae374fc923f56 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Mon, 13 Aug 2018 12:02:28 +0300 Subject: [PATCH 12/17] Moved "related_serializers" from view to serializer --- example/serializers.py | 5 +++++ example/views.py | 5 ----- rest_framework_json_api/views.py | 28 ++++++++++++---------------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 72859acb..d96a917d 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -177,6 +177,11 @@ class AuthorSerializer(serializers.ModelSerializer): 'bio': AuthorBioSerializer, 'type': AuthorTypeSerializer } + related_serializers = { + 'bio': 'example.serializers.AuthorBioSerializer', + 'entries': 'example.serializers.EntrySerializer', + 'first_entry': 'example.serializers.EntrySerializer' + } class Meta: model = Author diff --git a/example/views.py b/example/views.py index eb5df390..5dfc3341 100644 --- a/example/views.py +++ b/example/views.py @@ -95,11 +95,6 @@ class NonPaginatedEntryViewSet(EntryViewSet): class AuthorViewSet(ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer - related_serializers = { - 'bio': 'example.serializers.AuthorBioSerializer', - 'entries': 'example.serializers.EntrySerializer', - 'first_entry': 'example.serializers.EntrySerializer' - } class CommentViewSet(ModelViewSet): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 8af30367..528a5541 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -105,8 +105,6 @@ class RelatedMixin(object): """ This mixin handles all related entities, whose Serializers are declared in "related_serializers" """ - related_serializers = {} - related_field_mapping = {} def retrieve_related(self, request, *args, **kwargs): serializer_kwargs = {} @@ -133,29 +131,27 @@ def get_serializer_class(self): if 'related_field' in self.kwargs: field_name = self.kwargs['related_field'] - assert hasattr(parent_serializer_class, 'included_serializers')\ - or self.related_serializers,\ - 'Either "included_serializers" or ' \ - '"related_serializers" should be configured' - # Try get the class from related_serializers - class_str = self.related_serializers.get(field_name, None) - - if class_str is None: - # Class was not found in related_serializers, look for it in included_serializers - class_str = getattr(self, 'included_serializers', {}).get(field_name, None) + if hasattr(parent_serializer_class, 'related_serializers'): + class_str = parent_serializer_class.related_serializers.get(field_name, None) + if class_str is None: + raise NotFound + elif hasattr(parent_serializer_class, 'included_serializers'): + class_str = parent_serializer_class.included_serializers.get(field_name, None) if class_str is None: raise NotFound + + else: + assert False, \ + 'Either "included_serializers" or "related_serializers" should be configured' + return import_class_from_dotted_path(class_str) return parent_serializer_class def get_related_field_name(self): - field_name = self.kwargs['related_field'] - if field_name in self.related_field_mapping: - return self.related_field_mapping[field_name] - return field_name + return self.kwargs['related_field'] def get_related_instance(self): parent_obj = self.get_object() From c14850786e8663759c54476ed6b1754ee7d4d1ee Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Mon, 13 Aug 2018 15:50:48 +0300 Subject: [PATCH 13/17] Added tests --- example/tests/test_views.py | 87 ++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 9cccf2f9..d97c4f60 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -2,14 +2,19 @@ from django.test import RequestFactory from django.utils import timezone +from rest_framework.exceptions import NotFound +from rest_framework.request import Request from rest_framework.reverse import reverse -from rest_framework.test import APITestCase, force_authenticate +from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate from rest_framework_json_api.utils import format_resource_type from . import TestBase from .. import views +from example.factories import AuthorFactory, EntryFactory from example.models import Author, Blog, Comment, Entry +from example.serializers import AuthorBioSerializer, EntrySerializer +from example.views import AuthorViewSet class TestRelationshipView(APITestCase): @@ -225,6 +230,86 @@ def test_delete_to_many_relationship_with_change(self): assert response.status_code == 200, response.content.decode() +class TestRelatedMixin(APITestCase): + + def setUp(self): + self.author = AuthorFactory() + + def _get_view(self, kwargs): + factory = APIRequestFactory() + request = Request(factory.get('', content_type='application/vnd.api+json')) + return AuthorViewSet(request=request, kwargs=kwargs) + + def test_get_related_field_name(self): + kwargs = {'pk': self.author.id, 'related_field': 'bio'} + view = self._get_view(kwargs) + got = view.get_related_field_name() + self.assertEqual(got, kwargs['related_field']) + + def test_get_related_instance_serializer_field(self): + kwargs = {'pk': self.author.id, 'related_field': 'bio'} + view = self._get_view(kwargs) + got = view.get_related_instance() + self.assertEqual(got, self.author.bio) + + def test_get_related_instance_model_field(self): + kwargs = {'pk': self.author.id, 'related_field': 'id'} + view = self._get_view(kwargs) + got = view.get_related_instance() + self.assertEqual(got, self.author.id) + + def test_get_serializer_class(self): + kwargs = {'pk': self.author.id, 'related_field': 'bio'} + view = self._get_view(kwargs) + got = view.get_serializer_class() + self.assertEqual(got, AuthorBioSerializer) + + def test_get_serializer_class_many(self): + kwargs = {'pk': self.author.id, 'related_field': 'entries'} + view = self._get_view(kwargs) + got = view.get_serializer_class() + self.assertEqual(got, EntrySerializer) + + def test_get_serializer_class_raises_error(self): + kwargs = {'pk': self.author.id, 'related_field': 'type'} + view = self._get_view(kwargs) + self.assertRaises(NotFound, view.get_serializer_class) + + def test_retrieve_related_single(self): + url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'bio'}) + resp = self.client.get(url) + expected = { + 'data': { + 'type': 'authorBios', 'id': str(self.author.bio.id), + 'relationships': { + 'author': {'data': {'type': 'authors', 'id': str(self.author.id)}}}, + 'attributes': { + 'body': str(self.author.bio.body) + }, + } + } + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), expected) + + def test_retrieve_related_many(self): + entry = EntryFactory(authors=self.author) + url = reverse('author-related', kwargs={'pk': self.author.pk, 'related_field': 'entries'}) + resp = self.client.get(url) + + self.assertEqual(resp.status_code, 200) + self.assertTrue(isinstance(resp.json()['data'], list)) + self.assertEqual(len(resp.json()['data']), 1) + self.assertEqual(resp.json()['data'][0]['id'], str(entry.id)) + + def test_retrieve_related_None(self): + kwargs = {'pk': self.author.pk, 'related_field': 'first_entry'} + url = reverse('author-related', kwargs=kwargs) + resp = self.client.get(url) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), {'data': None}) + + class TestValidationErrorResponses(TestBase): def test_if_returns_error_on_empty_post(self): view = views.BlogViewSet.as_view({'post': 'create'}) From c0f0dab17583e7eda8896c7870d517b516294e6e Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Tue, 14 Aug 2018 12:08:59 +0300 Subject: [PATCH 14/17] Added docs --- docs/usage.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index d0fee78e..6759792f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -443,6 +443,50 @@ class LineItemViewSet(viewsets.ModelViewSet): not render `data`. Use this in case you only need links of relationships and want to lower payload and increase performance. +#### Related urls + +There is a nice way to handle "related" urls like `/orders/3/lineitems/` or `/orders/3/customer/`. +All you need is just add to `urls.py`: +```python +url(r'^orders/(?P[^/.]+)/(?P\w+)/$', + OrderViewSet.as_view({'get': 'retrieve_related'}), + name='order-related'), +``` +Make sure that RelatedField declaration has `related_link_url_kwarg='pk'` or simply skipped (will be set by default): +```python + line_items = ResourceRelatedField( + queryset=LineItem.objects, + many=True, + related_link_view_name='order-lineitems-list', + related_link_url_kwarg='pk', + self_link_view_name='order_relationships' + ) + + customer = ResourceRelatedField( + queryset=Customer.objects, + related_link_view_name='order-customer-detail', + self_link_view_name='order-relationships' + ) +``` +And, the most important part - declare serializer for each related entity: +```python +class OrderSerializer(serializers.HyperlinkedModelSerializer): + ... + related_serializers = { + 'customer': 'example.serializers.CustomerSerializer', + 'line_items': 'example.serializers.LineItemSerializer' + } +``` +Or, if you already have `included_serializers` declared and your `related_serializers` look the same, just skip it: +```python +class OrderSerializer(serializers.HyperlinkedModelSerializer): + ... + included_serializers = { + 'customer': 'example.serializers.CustomerSerializer', + 'line_items': 'example.serializers.LineItemSerializer' + } +``` + ### RelationshipView `rest_framework_json_api.views.RelationshipView` is used to build relationship views (see the From 228b1e8bd15276ee51f30ba4643fcd300b3ccb02 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Tue, 14 Aug 2018 12:11:15 +0300 Subject: [PATCH 15/17] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e47c5da0..fc160067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Add testing configuration to `REST_FRAMEWORK` configuration as described in [DRF](https://www.django-rest-framework.org/api-guide/testing/#configuration) * Add sorting configuration to `REST_FRAMEWORK` as defined in [json api spec](http://jsonapi.org/format/#fetching-sorting) * Add `HyperlinkedRelatedField` and `SerializerMethodHyperlinkedRelatedField`. See [usage docs](docs/usage.md#related-fields) +* Add related urls support. See [usage docs](docs/usage.md#related-urls) v2.5.0 - Released July 11, 2018 From 8191d6de6a0e7287e533acdda2845b790781a7eb Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Wed, 15 Aug 2018 11:32:46 +0300 Subject: [PATCH 16/17] Added test + small improve --- example/tests/test_views.py | 12 +++++++++++- rest_framework_json_api/views.py | 12 +++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index d97c4f60..4c5ef8f8 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -13,7 +13,7 @@ from .. import views from example.factories import AuthorFactory, EntryFactory from example.models import Author, Blog, Comment, Entry -from example.serializers import AuthorBioSerializer, EntrySerializer +from example.serializers import AuthorBioSerializer, EntrySerializer, AuthorTypeSerializer from example.views import AuthorViewSet @@ -270,6 +270,16 @@ def test_get_serializer_class_many(self): got = view.get_serializer_class() self.assertEqual(got, EntrySerializer) + def test_get_serializer_comes_from_included_serializers(self): + kwargs = {'pk': self.author.id, 'related_field': 'type'} + view = self._get_view(kwargs) + related_serializers = view.serializer_class.related_serializers + delattr(view.serializer_class, 'related_serializers') + got = view.get_serializer_class() + self.assertEqual(got, AuthorTypeSerializer) + + view.serializer_class.related_serializers = related_serializers + def test_get_serializer_class_raises_error(self): kwargs = {'pk': self.author.id, 'related_field': 'type'} view = self._get_view(kwargs) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 528a5541..b77e6a99 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -133,20 +133,22 @@ def get_serializer_class(self): # Try get the class from related_serializers if hasattr(parent_serializer_class, 'related_serializers'): - class_str = parent_serializer_class.related_serializers.get(field_name, None) - if class_str is None: + _class = parent_serializer_class.related_serializers.get(field_name, None) + if _class is None: raise NotFound elif hasattr(parent_serializer_class, 'included_serializers'): - class_str = parent_serializer_class.included_serializers.get(field_name, None) - if class_str is None: + _class = parent_serializer_class.included_serializers.get(field_name, None) + if _class is None: raise NotFound else: assert False, \ 'Either "included_serializers" or "related_serializers" should be configured' - return import_class_from_dotted_path(class_str) + if not isinstance(_class, type): + return import_class_from_dotted_path(_class) + return _class return parent_serializer_class From b8a902f19beb27373c98ea5303e2baf8bc13f702 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 17 Aug 2018 14:50:16 +0300 Subject: [PATCH 17/17] Updated imports order, docs/usage.md --- docs/usage.md | 9 ++++++--- example/tests/test_views.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 6759792f..25bb7310 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -448,6 +448,9 @@ and increase performance. There is a nice way to handle "related" urls like `/orders/3/lineitems/` or `/orders/3/customer/`. All you need is just add to `urls.py`: ```python +url(r'^orders/(?P[^/.]+)/$', + OrderViewSet.as_view({'get': 'retrieve'}), + name='order-detail'), url(r'^orders/(?P[^/.]+)/(?P\w+)/$', OrderViewSet.as_view({'get': 'retrieve_related'}), name='order-related'), @@ -457,14 +460,14 @@ Make sure that RelatedField declaration has `related_link_url_kwarg='pk'` or sim line_items = ResourceRelatedField( queryset=LineItem.objects, many=True, - related_link_view_name='order-lineitems-list', + related_link_view_name='order-related', related_link_url_kwarg='pk', - self_link_view_name='order_relationships' + self_link_view_name='order-relationships' ) customer = ResourceRelatedField( queryset=Customer.objects, - related_link_view_name='order-customer-detail', + related_link_view_name='order-related', self_link_view_name='order-relationships' ) ``` diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 4c5ef8f8..48e1bfa6 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -13,7 +13,7 @@ from .. import views from example.factories import AuthorFactory, EntryFactory from example.models import Author, Blog, Comment, Entry -from example.serializers import AuthorBioSerializer, EntrySerializer, AuthorTypeSerializer +from example.serializers import AuthorBioSerializer, AuthorTypeSerializer, EntrySerializer from example.views import AuthorViewSet