Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
django-rest-framework
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
django-rest-framework
Commits
527e4ffd
Commit
527e4ffd
authored
May 10, 2011
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
renderer API work
parent
8f58ee48
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
222 additions
and
120 deletions
+222
-120
djangorestframework/mixins.py
+11
-9
djangorestframework/renderers.py
+81
-48
djangorestframework/tests/renderers.py
+38
-8
djangorestframework/utils/__init__.py
+12
-2
djangorestframework/utils/mediatypes.py
+25
-4
djangorestframework/views.py
+55
-49
No files found.
djangorestframework/mixins.py
View file @
527e4ffd
...
...
@@ -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
()
...
...
djangorestframework/renderers.py
View file @
527e4ffd
"""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_resolve
s
from
djangorestframework
import
statu
s
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
o
utput
return
o
bj
class
TemplateRenderer
(
BaseRenderer
):
"""A Base class provided for convenience.
"""
A Base class provided for convenience.
Render the o
utpu
t simply by using the given template.
Render the o
bjec
t 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
,
o
utput
=
None
,
verbose
=
Fals
e
):
if
o
utput
is
None
:
def
render
(
self
,
o
bj
=
None
,
media_type
=
Non
e
):
if
o
bj
is
None
:
return
''
context
=
RequestContext
(
self
.
request
,
o
utput
)
context
=
RequestContext
(
self
.
request
,
o
bj
)
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
,
o
utput
):
"""Get the content as if it had been render
t
ed by a non-documenting renderer.
def
_get_content
(
self
,
resource
,
request
,
o
bj
,
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
,
o
utput
=
None
):
content
=
self
.
_get_content
(
self
.
view
,
self
.
view
.
request
,
o
utput
)
def
render
(
self
,
o
bj
=
None
,
media_type
=
None
):
content
=
self
.
_get_content
(
self
.
view
,
self
.
view
.
request
,
o
bj
,
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
,
o
utput
=
None
,
verbose
=
Fals
e
):
if
o
utput
is
None
:
def
render
(
self
,
o
bj
=
None
,
media_type
=
Non
e
):
if
o
bj
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
,
o
utput
=
None
,
verbose
=
Fals
e
):
if
o
utput
is
None
:
def
render
(
self
,
o
bj
=
None
,
media_type
=
Non
e
):
if
o
bj
is
None
:
return
''
return
dict2xml
(
o
utput
)
return
dict2xml
(
o
bj
)
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'
...
...
djangorestframework/tests/renderers.py
View file @
527e4ffd
...
...
@@ -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
,
o
utput
,
verbose
=
Fals
e
):
return
RENDERER_A_SERIALIZER
(
o
utput
)
def
render
(
self
,
o
bj
=
None
,
content_type
=
Non
e
):
return
RENDERER_A_SERIALIZER
(
o
bj
)
class
RendererB
(
BaseRenderer
):
media_type
=
'mock/rendererb'
def
render
(
self
,
o
utput
,
verbose
=
Fals
e
):
return
RENDERER_B_SERIALIZER
(
o
utput
)
def
render
(
self
,
o
bj
=
None
,
content_type
=
Non
e
):
return
RENDERER_B_SERIALIZER
(
o
bj
)
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
)
\ No newline at end of file
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
)
djangorestframework/utils/__init__.py
View file @
527e4ffd
...
...
@@ -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
:
...
...
djangorestframework/utils/mediatypes.py
View file @
527e4ffd
...
...
@@ -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
djangorestframework/views.py
View file @
527e4ffd
...
...
@@ -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
# 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
)
except
ErrorResponse
,
exc
:
response
=
exc
.
response
# 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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment