Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add support for related links using parent view and its permissions#451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
sliverc merged 17 commits intodjango-json-api:masterfromAnton-Shutik:master
Aug 17, 2018
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
17 commits
Select commitHold shift + click to select a range
ffe61c6
Pass related field name to "get_url" method
Anton-ShutikJul 31, 2018
196d8ba
Added RelatedMixin
Anton-ShutikAug 3, 2018
fc52dc4
Inherit ModelViewSet and ReadOnlyModelViewSet from RelatedMixin
Anton-ShutikAug 8, 2018
73d51a5
Use dotted path when declaring serializers
Anton-ShutikAug 8, 2018
7677042
Added doc string to HyperlinkedMixin
Anton-ShutikAug 8, 2018
bde21ec
Get field name from serializer class
Anton-ShutikAug 9, 2018
9e78e67
Use mapping for field name resolving
Anton-ShutikAug 10, 2018
d9b5f08
Improve serializer class resolving
Anton-ShutikAug 10, 2018
9decc1f
Improved related instance resolving
Anton-ShutikAug 10, 2018
77ac0b2
Added SerializerMethodResourceRelatedField + RelatedMixin example
Anton-ShutikAug 10, 2018
6e0b47c
Fix failing tox
Anton-ShutikAug 10, 2018
a210e63
Moved "related_serializers" from view to serializer
Anton-ShutikAug 13, 2018
c148507
Added tests
Anton-ShutikAug 13, 2018
c0f0dab
Added docs
Anton-ShutikAug 14, 2018
228b1e8
Updated changelog
Anton-ShutikAug 14, 2018
8191d6d
Added test + small improve
Anton-ShutikAug 15, 2018
b8a902f
Updated imports order, docs/usage.md
Anton-ShutikAug 17, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionsCHANGELOG.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
47 changes: 47 additions & 0 deletionsdocs/usage.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -443,6 +443,53 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Do you think we should combine this documentation withhttps://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#related-fields ?

Maybe we do not need to document overwriting ofget_queryset anymore as this is obsolete. Or do you still see a use case for it?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It is still working and can be used. I think we can remove the docs when we deprecate this stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I have just read through the documentation again and I am not sure whether it is clear why there are two different ways to basically do the same.

I think the one difference is that with the old way overwritingget_queryset someone can define different permissions on view which can not so easily be done with theRelatedMixin.

Somehow we should try to merge those two documentation pieces into one, recommending to useRelatedMixin way but still documenting old way in case of having different permissions for relations.

I first was planning to work on this but I do not really have time at hand. So if you have a suggestion feel free. Otherwise I might get to it at a later point.


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<pk>[^/.]+)/$',
OrderViewSet.as_view({'get': 'retrieve'}),
name='order-detail'),
url(r'^orders/(?P<pk>[^/.]+)/(?P<related_field>\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-related',
related_link_url_kwarg='pk',
self_link_view_name='order-relationships'
)

customer = ResourceRelatedField(
queryset=Customer.objects,
related_link_view_name='order-related',
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
Expand Down
28 changes: 27 additions & 1 deletionexample/serializers.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,14 +155,40 @@ 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
)
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
}
related_serializers = {
'bio': 'example.serializers.AuthorBioSerializer',
'entries': 'example.serializers.EntrySerializer',
'first_entry': 'example.serializers.EntrySerializer'
}

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):
Expand Down
97 changes: 96 additions & 1 deletionexample/tests/test_views.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 importAPIRequestFactory,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, AuthorTypeSerializer, EntrySerializer
from example.views import AuthorViewSet


class TestRelationshipView(APITestCase):
Expand DownExpand Up@@ -225,6 +230,96 @@ 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_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)
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'})
Expand Down
4 changes: 4 additions & 0 deletionsexample/urls.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,10 @@
EntryViewSet.as_view({'get': 'retrieve'}),
name='entry-featured'),

url(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
AuthorViewSet.as_view({'get': 'retrieve_related'}),
name='author-related'),

url(r'^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)',
EntryRelationshipView.as_view(),
name='entry-relationships'),
Expand Down
4 changes: 4 additions & 0 deletionsexample/urls_test.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,10 @@
EntryViewSet.as_view({'get': 'retrieve'}),
name='entry-featured'),

url(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
AuthorViewSet.as_view({'get': 'retrieve_related'}),
name='author-related'),

url(r'^entries/(?P<pk>[^/.]+)/relationships/(?P<related_field>\w+)',
EntryRelationshipView.as_view(),
name='entry-relationships'),
Expand Down
14 changes: 13 additions & 1 deletionrest_framework_json_api/relations.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,19 @@ 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]}
"""
Assuming RelatedField will be declared in two ways:
1. url(r'^authors/(?P<pk>[^/.]+)/(?P<related_field>\w+)/$',
AuthorViewSet.as_view({'get': 'retrieve_related'}))
2. url(r'^authors/(?P<author_pk>[^/.]+)/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
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:
Expand Down
78 changes: 77 additions & 1 deletionrest_framework_json_api/views.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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 (
Expand All@@ -9,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
Expand DownExpand Up@@ -98,12 +101,85 @@ def get_queryset(self, *args, **kwargs):
return qs


class ModelViewSet(AutoPrefetchMixin, PrefetchForIncludesHelperMixin, viewsets.ModelViewSet):
class RelatedMixin(object):
"""
This mixin handles all related entities, whose Serializers are declared in "related_serializers"
"""

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):
parent_serializer_class = super(RelatedMixin, self).get_serializer_class()

if 'related_field' in self.kwargs:
field_name = self.kwargs['related_field']

# Try get the class from related_serializers
if hasattr(parent_serializer_class, 'related_serializers'):
_class = parent_serializer_class.related_serializers.get(field_name, None)
if _class is None:
raise NotFound

elif hasattr(parent_serializer_class, 'included_serializers'):
_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'

if not isinstance(_class, type):
return import_class_from_dotted_path(_class)
return _class

return parent_serializer_class

def get_related_field_name(self):
return self.kwargs['related_field']

def get_related_instance(self):
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,
PrefetchForIncludesHelperMixin,
RelatedMixin,
viewsets.ModelViewSet):
pass


class ReadOnlyModelViewSet(AutoPrefetchMixin,
PrefetchForIncludesHelperMixin,
RelatedMixin,
viewsets.ReadOnlyModelViewSet):
pass

Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp