mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-19 17:34:13 +03:00
renderer API work
This commit is contained in:
parent
8f58ee489d
commit
527e4ffdf7
|
@ -17,14 +17,16 @@ import re
|
|||
from StringIO import StringIO
|
||||
|
||||
|
||||
__all__ = ('RequestMixin',
|
||||
'ResponseMixin',
|
||||
'AuthMixin',
|
||||
'ReadModelMixin',
|
||||
'CreateModelMixin',
|
||||
'UpdateModelMixin',
|
||||
'DeleteModelMixin',
|
||||
'ListModelMixin')
|
||||
__all__ = (
|
||||
'RequestMixin',
|
||||
'ResponseMixin',
|
||||
'AuthMixin',
|
||||
'ReadModelMixin',
|
||||
'CreateModelMixin',
|
||||
'UpdateModelMixin',
|
||||
'DeleteModelMixin',
|
||||
'ListModelMixin'
|
||||
)
|
||||
|
||||
|
||||
########## Request Mixin ##########
|
||||
|
@ -267,7 +269,7 @@ class ResponseMixin(object):
|
|||
|
||||
# Serialize the response content
|
||||
if response.has_content_body:
|
||||
content = renderer(self).render(output=response.cleaned_content)
|
||||
content = renderer(self).render(response.cleaned_content, renderer.media_type)
|
||||
else:
|
||||
content = renderer(self).render()
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Renderers are used to serialize a View's output into specific media types.
|
||||
"""
|
||||
Renderers are used to serialize a View's output into specific media types.
|
||||
django-rest-framework also provides HTML and PlainText renderers that help self-document the API,
|
||||
by serializing the output along with documentation regarding the Resource, output status and headers,
|
||||
and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.
|
||||
|
@ -7,64 +8,78 @@ from django import forms
|
|||
from django.conf import settings
|
||||
from django.template import RequestContext, loader
|
||||
from django.utils import simplejson as json
|
||||
from django import forms
|
||||
|
||||
from djangorestframework.utils import dict2xml, url_resolves
|
||||
from djangorestframework import status
|
||||
from djangorestframework.compat import apply_markdown
|
||||
from djangorestframework.utils import dict2xml, url_resolves
|
||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.utils.description import get_name, get_description
|
||||
from djangorestframework import status
|
||||
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param
|
||||
|
||||
from urllib import quote_plus
|
||||
import string
|
||||
import re
|
||||
from decimal import Decimal
|
||||
import re
|
||||
import string
|
||||
from urllib import quote_plus
|
||||
|
||||
__all__ = (
|
||||
'BaseRenderer',
|
||||
'JSONRenderer',
|
||||
'DocumentingHTMLRenderer',
|
||||
'DocumentingXHTMLRenderer',
|
||||
'DocumentingPlainTextRenderer',
|
||||
'XMLRenderer'
|
||||
)
|
||||
|
||||
# TODO: Rename verbose to something more appropriate
|
||||
# TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
|
||||
# and only have an renderer output anything if it explicitly provides support for that.
|
||||
|
||||
class BaseRenderer(object):
|
||||
"""All renderers must extend this class, set the media_type attribute, and
|
||||
override the render() function."""
|
||||
"""
|
||||
All renderers must extend this class, set the media_type attribute, and
|
||||
override the render() function.
|
||||
"""
|
||||
media_type = None
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
|
||||
def render(self, output=None, verbose=False):
|
||||
"""By default render simply returns the ouput as-is.
|
||||
Override this method to provide for other behaviour."""
|
||||
if output is None:
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
By default render simply returns the ouput as-is.
|
||||
Override this method to provide for other behavior.
|
||||
"""
|
||||
if obj is None:
|
||||
return ''
|
||||
|
||||
return output
|
||||
return obj
|
||||
|
||||
|
||||
class TemplateRenderer(BaseRenderer):
|
||||
"""A Base class provided for convenience.
|
||||
"""
|
||||
A Base class provided for convenience.
|
||||
|
||||
Render the output simply by using the given template.
|
||||
Render the object simply by using the given template.
|
||||
To create a template renderer, subclass this, and set
|
||||
the ``media_type`` and ``template`` attributes"""
|
||||
the ``media_type`` and ``template`` attributes
|
||||
"""
|
||||
media_type = None
|
||||
template = None
|
||||
|
||||
def render(self, output=None, verbose=False):
|
||||
if output is None:
|
||||
def render(self, obj=None, media_type=None):
|
||||
if obj is None:
|
||||
return ''
|
||||
|
||||
context = RequestContext(self.request, output)
|
||||
context = RequestContext(self.request, obj)
|
||||
return self.template.render(context)
|
||||
|
||||
|
||||
class DocumentingTemplateRenderer(BaseRenderer):
|
||||
"""Base class for renderers used to self-document the API.
|
||||
Implementing classes should extend this class and set the template attribute."""
|
||||
"""
|
||||
Base class for renderers used to self-document the API.
|
||||
Implementing classes should extend this class and set the template attribute.
|
||||
"""
|
||||
template = None
|
||||
|
||||
def _get_content(self, resource, request, output):
|
||||
"""Get the content as if it had been renderted by a non-documenting renderer.
|
||||
def _get_content(self, resource, request, obj, media_type):
|
||||
"""Get the content as if it had been rendered by a non-documenting renderer.
|
||||
|
||||
(Typically this will be the content as it would have been if the Resource had been
|
||||
requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
|
||||
|
@ -73,8 +88,9 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
renderers = [renderer for renderer in resource.renderers if not isinstance(renderer, DocumentingTemplateRenderer)]
|
||||
if not renderers:
|
||||
return '[No renderers were found]'
|
||||
|
||||
content = renderers[0](resource).render(output, verbose=True)
|
||||
|
||||
media_type = add_media_type_param(media_type, 'indent', '4')
|
||||
content = renderers[0](resource).render(obj, media_type)
|
||||
if not all(char in string.printable for char in content):
|
||||
return '[%d bytes of binary content]'
|
||||
|
||||
|
@ -149,8 +165,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
return GenericContentForm(resource)
|
||||
|
||||
|
||||
def render(self, output=None):
|
||||
content = self._get_content(self.view, self.view.request, output)
|
||||
def render(self, obj=None, media_type=None):
|
||||
content = self._get_content(self.view, self.view.request, obj, media_type)
|
||||
form_instance = self._get_form_instance(self.view)
|
||||
|
||||
if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
|
||||
|
@ -194,46 +210,63 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
|||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""Renderer which serializes to JSON"""
|
||||
"""
|
||||
Renderer which serializes to JSON
|
||||
"""
|
||||
media_type = 'application/json'
|
||||
|
||||
def render(self, output=None, verbose=False):
|
||||
if output is None:
|
||||
def render(self, obj=None, media_type=None):
|
||||
if obj is None:
|
||||
return ''
|
||||
if verbose:
|
||||
return json.dumps(output, indent=4, sort_keys=True)
|
||||
return json.dumps(output)
|
||||
|
||||
indent = get_media_type_params(media_type).get('indent', None)
|
||||
if indent is not None:
|
||||
try:
|
||||
indent = int(indent)
|
||||
except ValueError:
|
||||
indent = None
|
||||
|
||||
sort_keys = indent and True or False
|
||||
return json.dumps(obj, indent=indent, sort_keys=sort_keys)
|
||||
|
||||
|
||||
class XMLRenderer(BaseRenderer):
|
||||
"""Renderer which serializes to XML."""
|
||||
"""
|
||||
Renderer which serializes to XML.
|
||||
"""
|
||||
media_type = 'application/xml'
|
||||
|
||||
def render(self, output=None, verbose=False):
|
||||
if output is None:
|
||||
def render(self, obj=None, media_type=None):
|
||||
if obj is None:
|
||||
return ''
|
||||
return dict2xml(output)
|
||||
return dict2xml(obj)
|
||||
|
||||
|
||||
class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
|
||||
"""Renderer which provides a browsable HTML interface for an API.
|
||||
See the examples listed in the django-rest-framework documentation to see this in actions."""
|
||||
"""
|
||||
Renderer which provides a browsable HTML interface for an API.
|
||||
See the examples at http://api.django-rest-framework.org to see this in action.
|
||||
"""
|
||||
media_type = 'text/html'
|
||||
template = 'renderer.html'
|
||||
|
||||
|
||||
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
|
||||
"""Identical to DocumentingHTMLRenderer, except with an xhtml media type.
|
||||
"""
|
||||
Identical to DocumentingHTMLRenderer, except with an xhtml media type.
|
||||
We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
|
||||
given their Accept headers."""
|
||||
given their Accept headers.
|
||||
"""
|
||||
media_type = 'application/xhtml+xml'
|
||||
template = 'renderer.html'
|
||||
|
||||
|
||||
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
||||
"""Renderer that serializes the output with the default renderer, but also provides plain-text
|
||||
doumentation of the returned status and headers, and of the resource's name and description.
|
||||
Useful for browsing an API with command line tools."""
|
||||
"""
|
||||
Renderer that serializes the object with the default renderer, but also provides plain-text
|
||||
documentation of the returned status and headers, and of the resource's name and description.
|
||||
Useful for browsing an API with command line tools.
|
||||
"""
|
||||
media_type = 'text/plain'
|
||||
template = 'renderer.txt'
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ from django.conf.urls.defaults import patterns, url
|
|||
from django import http
|
||||
from django.test import TestCase
|
||||
from djangorestframework.compat import View
|
||||
from djangorestframework.renderers import BaseRenderer
|
||||
from djangorestframework.renderers import BaseRenderer, JSONRenderer
|
||||
from djangorestframework.mixins import ResponseMixin
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework.utils.mediatypes import add_media_type_param
|
||||
|
||||
DUMMYSTATUS = 200
|
||||
DUMMYCONTENT = 'dummycontent'
|
||||
|
@ -20,14 +21,14 @@ class MockView(ResponseMixin, View):
|
|||
class RendererA(BaseRenderer):
|
||||
media_type = 'mock/renderera'
|
||||
|
||||
def render(self, output, verbose=False):
|
||||
return RENDERER_A_SERIALIZER(output)
|
||||
def render(self, obj=None, content_type=None):
|
||||
return RENDERER_A_SERIALIZER(obj)
|
||||
|
||||
class RendererB(BaseRenderer):
|
||||
media_type = 'mock/rendererb'
|
||||
|
||||
def render(self, output, verbose=False):
|
||||
return RENDERER_B_SERIALIZER(output)
|
||||
def render(self, obj=None, content_type=None):
|
||||
return RENDERER_B_SERIALIZER(obj)
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
@ -36,7 +37,9 @@ urlpatterns = patterns('',
|
|||
|
||||
|
||||
class RendererIntegrationTests(TestCase):
|
||||
"""End-to-end testing of renderers using an RendererMixin on a generic view."""
|
||||
"""
|
||||
End-to-end testing of renderers using an RendererMixin on a generic view.
|
||||
"""
|
||||
|
||||
urls = 'djangorestframework.tests.renderers'
|
||||
|
||||
|
@ -73,4 +76,32 @@ class RendererIntegrationTests(TestCase):
|
|||
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."""
|
||||
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
|
||||
self.assertEquals(resp.status_code, 406)
|
||||
self.assertEquals(resp.status_code, 406)
|
||||
|
||||
|
||||
|
||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||
|
||||
_indented_repr = """{
|
||||
"foo": [
|
||||
"bar",
|
||||
"baz"
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
class JSONRendererTests(TestCase):
|
||||
"""
|
||||
Tests specific to the JSON Renderer
|
||||
"""
|
||||
def test_without_content_type_args(self):
|
||||
obj = {'foo':['bar','baz']}
|
||||
renderer = JSONRenderer(None)
|
||||
content = renderer.render(obj, 'application/json')
|
||||
self.assertEquals(content, _flat_repr)
|
||||
|
||||
def test_with_content_type_args(self):
|
||||
obj = {'foo':['bar','baz']}
|
||||
renderer = JSONRenderer(None)
|
||||
content = renderer.render(obj, 'application/json; indent=2')
|
||||
self.assertEquals(content, _indented_repr)
|
||||
|
|
|
@ -16,7 +16,15 @@ import xml.etree.ElementTree as ET
|
|||
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||
|
||||
def as_tuple(obj):
|
||||
"""Given obj return a tuple"""
|
||||
"""
|
||||
Given an object which may be a list/tuple, another object, or None,
|
||||
return that object in list form.
|
||||
|
||||
IE:
|
||||
If the object is already a list/tuple just return it.
|
||||
If the object is not None, return it in a list with a single element.
|
||||
If the object is None return an empty list.
|
||||
"""
|
||||
if obj is None:
|
||||
return ()
|
||||
elif isinstance(obj, list):
|
||||
|
@ -27,7 +35,9 @@ def as_tuple(obj):
|
|||
|
||||
|
||||
def url_resolves(url):
|
||||
"""Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
|
||||
"""
|
||||
Return True if the given URL is mapped to a view in the urlconf, False otherwise.
|
||||
"""
|
||||
try:
|
||||
resolve(url)
|
||||
except:
|
||||
|
|
|
@ -15,7 +15,7 @@ def media_type_matches(lhs, rhs):
|
|||
|
||||
Valid media type strings include:
|
||||
|
||||
'application/json indent=4'
|
||||
'application/json; indent=4'
|
||||
'application/json'
|
||||
'text/*'
|
||||
'*/*'
|
||||
|
@ -33,10 +33,28 @@ def is_form_media_type(media_type):
|
|||
media_type = _MediaType(media_type)
|
||||
return media_type.full_type == 'application/x-www-form-urlencoded' or \
|
||||
media_type.full_type == 'multipart/form-data'
|
||||
|
||||
|
||||
|
||||
|
||||
def add_media_type_param(media_type, key, val):
|
||||
"""
|
||||
Add a key, value parameter to a media type string, and return the new media type string.
|
||||
"""
|
||||
media_type = _MediaType(media_type)
|
||||
media_type.params[key] = val
|
||||
return str(media_type)
|
||||
|
||||
def get_media_type_params(media_type):
|
||||
"""
|
||||
Return a dictionary of the parameters on the given media type.
|
||||
"""
|
||||
return _MediaType(media_type).params
|
||||
|
||||
|
||||
|
||||
class _MediaType(object):
|
||||
def __init__(self, media_type_str):
|
||||
if media_type_str is None:
|
||||
media_type_str = ''
|
||||
self.orig = media_type_str
|
||||
self.full_type, self.params = parse_header(media_type_str)
|
||||
self.main_type, sep, self.sub_type = self.full_type.partition('/')
|
||||
|
@ -94,5 +112,8 @@ class _MediaType(object):
|
|||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.orig
|
||||
ret = "%s/%s" % (self.main_type, self.sub_type)
|
||||
for key, val in self.params.items():
|
||||
ret += "; %s=%s" % (key, val)
|
||||
return ret
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ from djangorestframework.mixins import *
|
|||
from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status
|
||||
|
||||
|
||||
__all__ = ('BaseView',
|
||||
'ModelView',
|
||||
'InstanceModelView',
|
||||
'ListOrModelView',
|
||||
'ListOrCreateModelView')
|
||||
__all__ = (
|
||||
'BaseView',
|
||||
'ModelView',
|
||||
'InstanceModelView',
|
||||
'ListOrModelView',
|
||||
'ListOrCreateModelView'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
@ -78,55 +80,59 @@ class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
|||
# all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||
set_script_prefix(prefix)
|
||||
|
||||
try:
|
||||
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
||||
# self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
|
||||
self.perform_form_overloading()
|
||||
|
||||
# Authenticate and check request is has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
||||
# Get the appropriate handler method
|
||||
if self.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
response_obj = handler(request, *args, **kwargs)
|
||||
|
||||
# Allow return value to be either Response, or an object, or None
|
||||
if isinstance(response_obj, Response):
|
||||
response = response_obj
|
||||
elif response_obj is not None:
|
||||
response = Response(status.HTTP_200_OK, response_obj)
|
||||
else:
|
||||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.resource.object_to_serializable(response.raw_content)
|
||||
self.request = request
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||
set_script_prefix(prefix)
|
||||
|
||||
try:
|
||||
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
||||
# self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
|
||||
self.perform_form_overloading()
|
||||
|
||||
# Authenticate and check request is has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
||||
# Get the appropriate handler method
|
||||
if self.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
response_obj = handler(request, *args, **kwargs)
|
||||
|
||||
# Allow return value to be either Response, or an object, or None
|
||||
if isinstance(response_obj, Response):
|
||||
response = response_obj
|
||||
elif response_obj is not None:
|
||||
response = Response(status.HTTP_200_OK, response_obj)
|
||||
else:
|
||||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.resource.object_to_serializable(response.raw_content)
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
# TODO - this isn't actually the correct way to set the vary header,
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
response.headers['Vary'] = 'Authenticate, Accept'
|
||||
|
||||
return self.render(response)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
# TODO - this isn't actually the correct way to set the vary header,
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
response.headers['Vary'] = 'Authenticate, Accept'
|
||||
|
||||
return self.render(response)
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user