mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-09 16:10:50 +03:00
Allow .json .html .xml style urls and also allow these formats to be specified in a "?format=..." query string.
This commit is contained in:
parent
d3024ff181
commit
d8bec115ad
|
@ -11,6 +11,7 @@ from django.http.multipartparser import LimitBytes
|
||||||
|
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.parsers import FormParser, MultiPartParser
|
from djangorestframework.parsers import FormParser, MultiPartParser
|
||||||
|
from djangorestframework.renderers import BaseRenderer
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||||
|
@ -290,7 +291,7 @@ class ResponseMixin(object):
|
||||||
accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
|
accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
|
||||||
else:
|
else:
|
||||||
# No accept header specified
|
# No accept header specified
|
||||||
return (self._default_renderer(self), self._default_renderer.media_type)
|
accept_list = ['*/*']
|
||||||
|
|
||||||
# Check the acceptable media types against each renderer,
|
# Check the acceptable media types against each renderer,
|
||||||
# attempting more specific media types first
|
# attempting more specific media types first
|
||||||
|
@ -298,11 +299,11 @@ class ResponseMixin(object):
|
||||||
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
||||||
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
|
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
|
||||||
|
|
||||||
for media_type_lst in order_by_precedence(accept_list):
|
for accepted_media_type_lst in order_by_precedence(accept_list):
|
||||||
for renderer in renderers:
|
for renderer in renderers:
|
||||||
for media_type in media_type_lst:
|
for accepted_media_type in accepted_media_type_lst:
|
||||||
if renderer.can_handle_response(media_type):
|
if renderer.can_handle_response(accepted_media_type):
|
||||||
return renderer, media_type
|
return renderer, accepted_media_type
|
||||||
|
|
||||||
# No acceptable renderers were found
|
# No acceptable renderers were found
|
||||||
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
|
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
|
||||||
|
@ -317,6 +318,13 @@ class ResponseMixin(object):
|
||||||
"""
|
"""
|
||||||
return [renderer.media_type for renderer in self.renderers]
|
return [renderer.media_type for renderer in self.renderers]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _rendered_formats(self):
|
||||||
|
"""
|
||||||
|
Return a list of all the formats that this view can render.
|
||||||
|
"""
|
||||||
|
return [renderer.format for renderer in self.renderers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _default_renderer(self):
|
def _default_renderer(self):
|
||||||
"""
|
"""
|
||||||
|
@ -486,7 +494,10 @@ class ReadModelMixin(object):
|
||||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||||
else:
|
else:
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
# Otherwise assume the kwargs uniquely identify the model
|
||||||
instance = model.objects.get(**kwargs)
|
filtered_keywords = kwargs.copy()
|
||||||
|
if BaseRenderer._FORMAT_QUERY_PARAM in filtered_keywords:
|
||||||
|
del filtered_keywords[BaseRenderer._FORMAT_QUERY_PARAM]
|
||||||
|
instance = model.objects.get(**filtered_keywords)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,10 @@ class BaseRenderer(object):
|
||||||
and override the :meth:`render` method.
|
and override the :meth:`render` method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_FORMAT_QUERY_PARAM = 'format'
|
||||||
|
|
||||||
media_type = None
|
media_type = None
|
||||||
|
format = None
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
self.view = view
|
self.view = view
|
||||||
|
@ -58,6 +61,11 @@ class BaseRenderer(object):
|
||||||
This may be overridden to provide for other behavior, but typically you'll
|
This may be overridden to provide for other behavior, but typically you'll
|
||||||
instead want to just set the :attr:`media_type` attribute on the class.
|
instead want to just set the :attr:`media_type` attribute on the class.
|
||||||
"""
|
"""
|
||||||
|
format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None)
|
||||||
|
if format is None:
|
||||||
|
format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None)
|
||||||
|
if format is not None:
|
||||||
|
return format == self.format
|
||||||
return media_type_matches(self.media_type, accept)
|
return media_type_matches(self.media_type, accept)
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
|
@ -84,6 +92,7 @@ class JSONRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/json'
|
media_type = 'application/json'
|
||||||
|
format = 'json'
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
|
@ -111,6 +120,7 @@ class XMLRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/xml'
|
media_type = 'application/xml'
|
||||||
|
format = 'xml'
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
|
@ -289,12 +299,12 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
'version': VERSION,
|
'version': VERSION,
|
||||||
'markeddown': markeddown,
|
'markeddown': markeddown,
|
||||||
'breadcrumblist': breadcrumb_list,
|
'breadcrumblist': breadcrumb_list,
|
||||||
'available_media_types': self.view._rendered_media_types,
|
'available_formats': self.view._rendered_formats,
|
||||||
'put_form': put_form_instance,
|
'put_form': put_form_instance,
|
||||||
'post_form': post_form_instance,
|
'post_form': post_form_instance,
|
||||||
'login_url': login_url,
|
'login_url': login_url,
|
||||||
'logout_url': logout_url,
|
'logout_url': logout_url,
|
||||||
'ACCEPT_PARAM': getattr(self.view, '_ACCEPT_QUERY_PARAM', None),
|
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
|
||||||
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
|
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
|
||||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
|
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
|
||||||
})
|
})
|
||||||
|
@ -317,6 +327,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
|
format = 'html'
|
||||||
template = 'renderer.html'
|
template = 'renderer.html'
|
||||||
|
|
||||||
|
|
||||||
|
@ -328,6 +339,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/xhtml+xml'
|
media_type = 'application/xhtml+xml'
|
||||||
|
format = 'xhtml'
|
||||||
template = 'renderer.html'
|
template = 'renderer.html'
|
||||||
|
|
||||||
|
|
||||||
|
@ -339,6 +351,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
|
format = 'txt'
|
||||||
template = 'renderer.txt'
|
template = 'renderer.txt'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
DEBUG_PROPAGATE_EXCEPTIONS = True
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Your Name', 'your_email@domain.com'),
|
# ('Your Name', 'your_email@domain.com'),
|
||||||
|
|
|
@ -48,9 +48,9 @@
|
||||||
<h2>GET {{ name }}</h2>
|
<h2>GET {{ name }}</h2>
|
||||||
<div class='submit-row' style='margin: 0; border: 0'>
|
<div class='submit-row' style='margin: 0; border: 0'>
|
||||||
<a href='{{ request.get_full_path }}' rel="nofollow" style='float: left'>GET</a>
|
<a href='{{ request.get_full_path }}' rel="nofollow" style='float: left'>GET</a>
|
||||||
{% for media_type in available_media_types %}
|
{% for format in available_formats %}
|
||||||
{% with ACCEPT_PARAM|add:"="|add:media_type as param %}
|
{% with FORMAT_PARAM|add:"="|add:format as param %}
|
||||||
[<a href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]
|
[<a href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow">{{ format }}</a>]
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.conf.urls.defaults import patterns, url
|
||||||
from django import http
|
from django import http
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from djangorestframework import status
|
||||||
from djangorestframework.compat import View as DjangoView
|
from djangorestframework.compat import View as DjangoView
|
||||||
from djangorestframework.renderers import BaseRenderer, JSONRenderer
|
from djangorestframework.renderers import BaseRenderer, JSONRenderer
|
||||||
from djangorestframework.parsers import JSONParser
|
from djangorestframework.parsers import JSONParser
|
||||||
|
@ -11,7 +12,7 @@ from djangorestframework.utils.mediatypes import add_media_type_param
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
DUMMYSTATUS = 200
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
DUMMYCONTENT = 'dummycontent'
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
|
||||||
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
||||||
|
@ -19,12 +20,14 @@ RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
|
||||||
|
|
||||||
class RendererA(BaseRenderer):
|
class RendererA(BaseRenderer):
|
||||||
media_type = 'mock/renderera'
|
media_type = 'mock/renderera'
|
||||||
|
format="formata"
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
return RENDERER_A_SERIALIZER(obj)
|
return RENDERER_A_SERIALIZER(obj)
|
||||||
|
|
||||||
class RendererB(BaseRenderer):
|
class RendererB(BaseRenderer):
|
||||||
media_type = 'mock/rendererb'
|
media_type = 'mock/rendererb'
|
||||||
|
format="formatb"
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
return RENDERER_B_SERIALIZER(obj)
|
return RENDERER_B_SERIALIZER(obj)
|
||||||
|
@ -32,11 +35,13 @@ class RendererB(BaseRenderer):
|
||||||
class MockView(ResponseMixin, DjangoView):
|
class MockView(ResponseMixin, DjangoView):
|
||||||
renderers = (RendererA, RendererB)
|
renderers = (RendererA, RendererB)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request, **kwargs):
|
||||||
response = Response(DUMMYSTATUS, DUMMYCONTENT)
|
response = Response(DUMMYSTATUS, DUMMYCONTENT)
|
||||||
return self.render(response)
|
return self.render(response)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
|
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,10 +90,58 @@ class RendererIntegrationTests(TestCase):
|
||||||
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_accept_query(self):
|
||||||
|
"""The '_accept' query string should behave in the same way as the Accept header."""
|
||||||
|
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
|
||||||
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
|
||||||
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||||
self.assertEquals(resp.status_code, 406)
|
self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_format_query(self):
|
||||||
|
"""If a 'format' query is specified, the renderer with the matching
|
||||||
|
format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_serializes_content_on_format_kwargs(self):
|
||||||
|
"""If a 'format' keyword arg is specified, the renderer with the matching
|
||||||
|
format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/something.formatb')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_specified_renderer_is_used_on_format_query_with_matching_accept(self):
|
||||||
|
"""If both a 'format' query and a matching Accept header specified,
|
||||||
|
the renderer with the matching format attribute should serialize the response."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format,
|
||||||
|
HTTP_ACCEPT=RendererB.media_type)
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_conflicting_format_query_and_accept_ignores_accept(self):
|
||||||
|
"""If a 'format' query is specified that does not match the Accept
|
||||||
|
header, we should only honor the 'format' query string."""
|
||||||
|
resp = self.client.get('/?format=%s' % RendererB.format,
|
||||||
|
HTTP_ACCEPT='dummy')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
|
def test_bla(self):
|
||||||
|
resp = self.client.get('/?format=formatb',
|
||||||
|
HTTP_ACCEPT='text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')
|
||||||
|
self.assertEquals(resp['Content-Type'], RendererB.media_type)
|
||||||
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
# all other authentication is CSRF exempt.
|
# all other authentication is CSRF exempt.
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
self.request = request
|
self.request = request
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
@ -163,7 +164,10 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
response.headers.update(self.headers)
|
response.headers.update(self.headers)
|
||||||
|
|
||||||
return self.render(response)
|
return self.render(response)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
class ModelView(View):
|
class ModelView(View):
|
||||||
"""A RESTful view that maps to a model in the database."""
|
"""A RESTful view that maps to a model in the database."""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user