Commit d07dc77e by Tom Christie

Accepted media type uses most specific of client/renderer media types.

parent ad5e6eb1
...@@ -18,24 +18,25 @@ If the generic views don't suit the needs of your API, you can drop down to usin ...@@ -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. ...@@ -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 ...@@ -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
\ No newline at end of file
...@@ -115,7 +115,6 @@ For example: ...@@ -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: ...@@ -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.
serializer = UserSerializer(instance=queryset)
data = serializer.data
# JSONRenderer requires serialized data as normal.
serializer = UserSerializer(instance=queryset)
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 ...@@ -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
......
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,8 +46,16 @@ class DefaultContentNegotiation(object): ...@@ -46,8 +46,16 @@ 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 renderer, 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
raise exceptions.NotAcceptable(available_renderers=renderers) raise exceptions.NotAcceptable(available_renderers=renderers)
...@@ -57,7 +65,7 @@ class DefaultContentNegotiation(object): ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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
self.accepted_renderer = renderer
# Accepted media type is the portion of the request Accept header
# 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
assert renderer, "No renderer set on Response"
self['Content-Type'] = self.renderer.media_type 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): ...@@ -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):
......
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): ...@@ -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():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment