mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-10 00:21:01 +03:00
Accepted media type uses most specific of client/renderer media types.
This commit is contained in:
parent
ad5e6eb16f
commit
d07dc77e91
|
@ -18,24 +18,25 @@ If the generic views don't suit the needs of your API, you can drop down to usin
|
||||||
Typically when using the generic views, you'll override the view, and set several class attributes.
|
Typically when using the generic views, you'll override the view, and set several class attributes.
|
||||||
|
|
||||||
class UserList(generics.ListCreateAPIView):
|
class UserList(generics.ListCreateAPIView):
|
||||||
serializer = UserSerializer
|
|
||||||
model = User
|
model = User
|
||||||
permissions = (IsAdminUser,)
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = (IsAdminUser,)
|
||||||
paginate_by = 100
|
paginate_by = 100
|
||||||
|
|
||||||
For more complex cases you might also want to override various methods on the view class. For example.
|
For more complex cases you might also want to override various methods on the view class. For example.
|
||||||
|
|
||||||
class UserList(generics.ListCreateAPIView):
|
class UserList(generics.ListCreateAPIView):
|
||||||
serializer = UserSerializer
|
|
||||||
model = User
|
model = User
|
||||||
permissions = (IsAdminUser,)
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = (IsAdminUser,)
|
||||||
|
|
||||||
def get_paginate_by(self):
|
def get_paginate_by(self):
|
||||||
"""
|
"""
|
||||||
Use smaller pagination for HTML representations.
|
Use smaller pagination for HTML representations.
|
||||||
"""
|
"""
|
||||||
if self.request.accepted_media_type == 'text/html':
|
page_size_param = self.request.QUERY_PARAMS.get('page_size')
|
||||||
return 10
|
if page_size_param:
|
||||||
|
return int(page_size_param)
|
||||||
return 100
|
return 100
|
||||||
|
|
||||||
For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
|
For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
|
||||||
|
@ -52,24 +53,32 @@ Used for read-only endpoints to represent a collection of model instances.
|
||||||
|
|
||||||
Provides a `get` method handler.
|
Provides a `get` method handler.
|
||||||
|
|
||||||
|
Extends: [MultipleObjectBaseAPIView], [ListModelMixin]
|
||||||
|
|
||||||
## ListCreateAPIView
|
## ListCreateAPIView
|
||||||
|
|
||||||
Used for read-write endpoints to represent a collection of model instances.
|
Used for read-write endpoints to represent a collection of model instances.
|
||||||
|
|
||||||
Provides `get` and `post` method handlers.
|
Provides `get` and `post` method handlers.
|
||||||
|
|
||||||
|
Extends: [MultipleObjectBaseAPIView], [ListModelMixin], [CreateModelMixin]
|
||||||
|
|
||||||
## RetrieveAPIView
|
## RetrieveAPIView
|
||||||
|
|
||||||
Used for read-only endpoints to represent a single model instance.
|
Used for read-only endpoints to represent a single model instance.
|
||||||
|
|
||||||
Provides a `get` method handler.
|
Provides a `get` method handler.
|
||||||
|
|
||||||
|
Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin]
|
||||||
|
|
||||||
## RetrieveUpdateDestroyAPIView
|
## RetrieveUpdateDestroyAPIView
|
||||||
|
|
||||||
Used for read-write endpoints to represent a single model instance.
|
Used for read-write endpoints to represent a single model instance.
|
||||||
|
|
||||||
Provides `get`, `put` and `delete` method handlers.
|
Provides `get`, `put` and `delete` method handlers.
|
||||||
|
|
||||||
|
Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Base views
|
# Base views
|
||||||
|
@ -123,3 +132,11 @@ Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion
|
||||||
[SingleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-single-object/
|
[SingleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-single-object/
|
||||||
[multiple-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.list/MultipleObjectMixin/
|
[multiple-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.list/MultipleObjectMixin/
|
||||||
[single-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.detail/SingleObjectMixin/
|
[single-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.detail/SingleObjectMixin/
|
||||||
|
|
||||||
|
[SingleObjectBaseAPIView]: #singleobjectbaseapiview
|
||||||
|
[MultipleObjectBaseAPIView]: #multipleobjectbaseapiview
|
||||||
|
[ListModelMixin]: #listmodelmixin
|
||||||
|
[CreateModelMixin]: #createmodelmixin
|
||||||
|
[RetrieveModelMixin]: #retrievemodelmixin
|
||||||
|
[UpdateModelMixin]: #updatemodelmixin
|
||||||
|
[DestroyModelMixin]: #destroymodelmixin
|
|
@ -115,7 +115,6 @@ For example:
|
||||||
|
|
||||||
@api_view(('GET',))
|
@api_view(('GET',))
|
||||||
@renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
@renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||||
@template_name('list_users.html')
|
|
||||||
def list_users(request):
|
def list_users(request):
|
||||||
"""
|
"""
|
||||||
A view that can return JSON or HTML representations
|
A view that can return JSON or HTML representations
|
||||||
|
@ -123,15 +122,16 @@ For example:
|
||||||
"""
|
"""
|
||||||
queryset = Users.objects.filter(active=True)
|
queryset = Users.objects.filter(active=True)
|
||||||
|
|
||||||
if request.accepted_renderer.format == 'html':
|
if request.accepted_media_type == 'text/html':
|
||||||
# TemplateHTMLRenderer takes a context dict,
|
# TemplateHTMLRenderer takes a context dict,
|
||||||
# and does not require serialization.
|
# and additionally requiresa 'template_name'.
|
||||||
|
# It does not require serialization.
|
||||||
data = {'users': queryset}
|
data = {'users': queryset}
|
||||||
else:
|
return Response(data, template='list_users.html')
|
||||||
|
|
||||||
# JSONRenderer requires serialized data as normal.
|
# JSONRenderer requires serialized data as normal.
|
||||||
serializer = UserSerializer(instance=queryset)
|
serializer = UserSerializer(instance=queryset)
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
|
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
## Designing your media types
|
## Designing your media types
|
||||||
|
|
|
@ -58,7 +58,7 @@ Note that the base URL can be whatever you want, but you must include `rest_fram
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
**TODO**
|
|
||||||
|
|
||||||
## Tutorial
|
## Tutorial
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.utils.mediatypes import order_by_precedence
|
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
|
||||||
|
|
||||||
|
|
||||||
class BaseContentNegotiation(object):
|
class BaseContentNegotiation(object):
|
||||||
|
@ -46,7 +46,15 @@ class DefaultContentNegotiation(object):
|
||||||
for media_type_set in order_by_precedence(accepts):
|
for media_type_set in order_by_precedence(accepts):
|
||||||
for renderer in renderers:
|
for renderer in renderers:
|
||||||
for media_type in media_type_set:
|
for media_type in media_type_set:
|
||||||
if renderer.can_handle_media_type(media_type):
|
if media_type_matches(renderer.media_type, media_type):
|
||||||
|
# Return the most specific media type as accepted.
|
||||||
|
if len(renderer.media_type) > len(media_type):
|
||||||
|
# Eg client requests '*/*'
|
||||||
|
# Accepted media type is 'application/json'
|
||||||
|
return renderer, renderer.media_type
|
||||||
|
else:
|
||||||
|
# Eg client requests 'application/json; indent=8'
|
||||||
|
# Accepted media type is 'application/json; indent=8'
|
||||||
return renderer, media_type
|
return renderer, media_type
|
||||||
|
|
||||||
raise exceptions.NotAcceptable(available_renderers=renderers)
|
raise exceptions.NotAcceptable(available_renderers=renderers)
|
||||||
|
@ -57,7 +65,7 @@ class DefaultContentNegotiation(object):
|
||||||
so that we only negotiation against those that accept that format.
|
so that we only negotiation against those that accept that format.
|
||||||
"""
|
"""
|
||||||
renderers = [renderer for renderer in renderers
|
renderers = [renderer for renderer in renderers
|
||||||
if renderer.can_handle_format(format)]
|
if renderer.format == format]
|
||||||
if not renderers:
|
if not renderers:
|
||||||
raise exceptions.InvalidFormat(format)
|
raise exceptions.InvalidFormat(format)
|
||||||
return renderers
|
return renderers
|
||||||
|
|
|
@ -15,7 +15,7 @@ from rest_framework.request import clone_request
|
||||||
from rest_framework.utils import dict2xml
|
from rest_framework.utils import dict2xml
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param
|
||||||
from rest_framework import VERSION
|
from rest_framework import VERSION
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -32,23 +32,6 @@ class BaseRenderer(object):
|
||||||
def __init__(self, view=None):
|
def __init__(self, view=None):
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def can_handle_format(self, format):
|
|
||||||
return format == self.format
|
|
||||||
|
|
||||||
def can_handle_media_type(self, media_type):
|
|
||||||
"""
|
|
||||||
Returns `True` if this renderer is able to deal with the given
|
|
||||||
media type.
|
|
||||||
|
|
||||||
The default implementation for this function is to check the media type
|
|
||||||
argument against the media_type attribute set on the class to see if
|
|
||||||
they match.
|
|
||||||
|
|
||||||
This may be overridden to provide for other behavior, but typically
|
|
||||||
you'll instead want to just set the `media_type` attribute on the class.
|
|
||||||
"""
|
|
||||||
return media_type_matches(self.media_type, media_type)
|
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
Given an object render it into a string.
|
Given an object render it into a string.
|
||||||
|
|
|
@ -20,26 +20,21 @@ class Response(SimpleTemplateResponse):
|
||||||
super(Response, self).__init__(None, status=status)
|
super(Response, self).__init__(None, status=status)
|
||||||
self.data = data
|
self.data = data
|
||||||
self.headers = headers and headers[:] or []
|
self.headers = headers and headers[:] or []
|
||||||
self.renderer = renderer
|
|
||||||
|
|
||||||
# Accepted media type is the portion of the request Accept header
|
self.accepted_renderer = renderer
|
||||||
# that the renderer satisfied. It could be '*/*', or somthing like
|
|
||||||
# application/json; indent=4
|
|
||||||
#
|
|
||||||
# This is NOT the value that will be returned in the 'Content-Type'
|
|
||||||
# header, but we do need to know the value in case there are
|
|
||||||
# any specific parameters which affect the rendering process.
|
|
||||||
self.accepted_media_type = accepted_media_type
|
self.accepted_media_type = accepted_media_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rendered_content(self):
|
def rendered_content(self):
|
||||||
assert self.renderer, "No renderer set on Response"
|
renderer = self.accepted_renderer
|
||||||
|
|
||||||
self['Content-Type'] = self.renderer.media_type
|
assert renderer, "No renderer set on Response"
|
||||||
|
|
||||||
|
self['content-type'] = self.accepted_media_type
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
return self.renderer.render()
|
return renderer.render()
|
||||||
render_media_type = self.accepted_media_type or self.renderer.media_type
|
|
||||||
return self.renderer.render(self.data, render_media_type)
|
return renderer.render(self.data, self.accepted_media_type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
|
|
|
@ -58,7 +58,7 @@ class DecoratorTestCase(TestCase):
|
||||||
|
|
||||||
request = self.factory.get('/')
|
request = self.factory.get('/')
|
||||||
response = view(request)
|
response = view(request)
|
||||||
self.assertTrue(isinstance(response.renderer, JSONRenderer))
|
self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer))
|
||||||
|
|
||||||
def test_parser_classes(self):
|
def test_parser_classes(self):
|
||||||
|
|
||||||
|
|
58
rest_framework/tests/negotiation.py
Normal file
58
rest_framework/tests/negotiation.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
|
from rest_framework.negotiation import DefaultContentNegotiation
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
class MockJSONRenderer(object):
|
||||||
|
media_type = 'application/json'
|
||||||
|
|
||||||
|
def __init__(self, view):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MockHTMLRenderer(object):
|
||||||
|
media_type = 'text/html'
|
||||||
|
|
||||||
|
def __init__(self, view):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((MockJSONRenderer, MockHTMLRenderer))
|
||||||
|
def example(request):
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAcceptedMediaType(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.renderers = [MockJSONRenderer(None), MockHTMLRenderer(None)]
|
||||||
|
self.negotiator = DefaultContentNegotiation()
|
||||||
|
|
||||||
|
def negotiate(self, request):
|
||||||
|
return self.negotiator.negotiate(request, self.renderers)
|
||||||
|
|
||||||
|
def test_client_without_accept_use_renderer(self):
|
||||||
|
request = factory.get('/')
|
||||||
|
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||||
|
self.assertEquals(accepted_media_type, 'application/json')
|
||||||
|
|
||||||
|
def test_client_underspecifies_accept_use_renderer(self):
|
||||||
|
request = factory.get('/', HTTP_ACCEPT='*/*')
|
||||||
|
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||||
|
self.assertEquals(accepted_media_type, 'application/json')
|
||||||
|
|
||||||
|
def test_client_overspecifies_accept_use_client(self):
|
||||||
|
request = factory.get('/', HTTP_ACCEPT='application/json; indent=8')
|
||||||
|
accepted_renderer, accepted_media_type = self.negotiate(request)
|
||||||
|
self.assertEquals(accepted_media_type, 'application/json; indent=8')
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTests(TestCase):
|
||||||
|
def test_accepted_negotiation_set_on_request(self):
|
||||||
|
request = factory.get('/', HTTP_ACCEPT='*/*')
|
||||||
|
response = example(request)
|
||||||
|
self.assertEquals(response.accepted_media_type, 'application/json')
|
|
@ -211,7 +211,7 @@ class APIView(View):
|
||||||
if isinstance(response, Response):
|
if isinstance(response, Response):
|
||||||
if not getattr(self, 'renderer', None):
|
if not getattr(self, 'renderer', None):
|
||||||
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request, force=True)
|
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request, force=True)
|
||||||
response.renderer = self.renderer
|
response.accepted_renderer = self.renderer
|
||||||
response.accepted_media_type = self.accepted_media_type
|
response.accepted_media_type = self.accepted_media_type
|
||||||
|
|
||||||
for key, value in self.headers.items():
|
for key, value in self.headers.items():
|
||||||
|
|
Loading…
Reference in New Issue
Block a user