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
764fbe33
Commit
764fbe33
authored
Jan 13, 2011
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Various cleanup
parent
5557dfb5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
101 additions
and
71 deletions
+101
-71
src/rest/emitters.py
+0
-9
src/rest/resource.py
+66
-36
src/rest/templates/emitter.html
+11
-10
src/rest/templates/emitter.txt
+6
-4
src/testapp/views.py
+18
-12
No files found.
src/rest/emitters.py
View file @
764fbe33
from
django.template
import
RequestContext
,
loader
from
django.core.handlers.wsgi
import
STATUS_CODE_TEXT
import
json
from
utils
import
dict2xml
...
...
@@ -22,14 +21,6 @@ class TemplatedEmitter(BaseEmitter):
template
=
loader
.
get_template
(
self
.
template
)
context
=
RequestContext
(
self
.
resource
.
request
,
{
'content'
:
content
,
'status'
:
self
.
resource
.
resp_status
,
'reason'
:
STATUS_CODE_TEXT
.
get
(
self
.
resource
.
resp_status
,
''
),
'headers'
:
self
.
resource
.
resp_headers
,
'resource_name'
:
self
.
resource
.
__class__
.
__name__
,
'resource_doc'
:
self
.
resource
.
__doc__
,
'create_form'
:
self
.
resource
.
form
,
'update_form'
:
self
.
resource
.
form
,
'request'
:
self
.
resource
.
request
,
'resource'
:
self
.
resource
,
})
...
...
src/rest/resource.py
View file @
764fbe33
from
django.http
import
HttpResponse
from
django.core.urlresolvers
import
reverse
from
django.core.handlers.wsgi
import
STATUS_CODE_TEXT
from
rest
import
emitters
,
parsers
from
decimal
import
Decimal
import
re
#
STATUS_400_BAD_REQUEST
=
400
...
...
@@ -22,6 +24,10 @@ class ResourceException(Exception):
class
Resource
(
object
):
# List of RESTful operations which may be performed on this resource.
allowed_operations
=
(
'read'
,)
anon_allowed_operations
=
()
# Optional form for input validation and presentation of HTML formatted responses.
form
=
None
# List of content-types the resource can respond with, ordered by preference
emitters
=
(
(
'application/json'
,
emitters
.
JSONEmitter
),
...
...
@@ -36,9 +42,6 @@ class Resource(object):
'application/x-www-form-urlencoded'
:
parsers
.
FormParser
,
'multipart/form-data'
:
parsers
.
FormParser
}
# Optional form for input validation and presentation of HTML formatted responses.
form
=
None
# Map standard HTTP methods to RESTful operations
CALLMAP
=
{
'GET'
:
'read'
,
'POST'
:
'create'
,
'PUT'
:
'update'
,
'DELETE'
:
'delete'
}
...
...
@@ -57,20 +60,34 @@ class Resource(object):
"""Make the class callable so it can be used as a Django view."""
self
=
object
.
__new__
(
cls
)
self
.
__init__
()
self
.
request
=
request
try
:
return
self
.
_handle_request
(
request
,
*
args
,
**
kwargs
)
except
:
import
traceback
traceback
.
print_exc
()
raise
return
self
.
_handle_request
(
request
,
*
args
,
**
kwargs
)
def
__init__
(
self
):
pass
def
name
(
self
):
"""Provide a name for the resource.
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names',
although this behaviour may be overridden."""
class_name
=
self
.
__class__
.
__name__
return
re
.
sub
(
'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
,
'
\\
1'
,
class_name
)
.
strip
()
def
description
(
self
):
"""Provide a description for the resource.
By default this is the class's docstring,
although this behaviour may be overridden."""
return
"
%
s"
%
self
.
__doc__
def
resp_status_text
(
self
):
"""Return reason text corrosponding to our HTTP response status code.
Provided for convienience."""
return
STATUS_CODE_TEXT
.
get
(
self
.
resp_status
,
''
)
def
reverse
(
self
,
view
,
*
args
,
**
kwargs
):
"""Return a fully qualified URI for a given view or resource, using the current request as the base URI.
TODO: Add SITEMAP option.
...
...
@@ -125,8 +142,14 @@ class Resource(object):
return
method
def
authenticate
(
self
):
"""..."""
# user = ...
# if anon_user and not anon_allowed_operations raise PermissionDenied
# return
def
check_method_allowed
(
self
,
method
):
"""Ensure the request method is acceptable fo
t
this resource."""
"""Ensure the request method is acceptable fo
r
this resource."""
if
not
method
in
self
.
CALLMAP
.
keys
():
raise
ResourceException
(
STATUS_501_NOT_IMPLEMENTED
,
{
'detail'
:
'Unknown or unsupported method
\'
%
s
\'
'
%
method
})
...
...
@@ -137,13 +160,12 @@ class Resource(object):
def
determine
_form
(
self
,
data
=
None
,
is_response
=
False
):
def
get_bound
_form
(
self
,
data
=
None
,
is_response
=
False
):
"""Optionally return a Django Form instance, which may be used for validation
and/or rendered by an HTML/XHTML emitter.
If data is not None the form will be bound to data. is_response indicates if data should be
treated as the input data (bind to client input) or the response data (bind to an existing object).
"""
treated as the input data (bind to client input) or the response data (bind to an existing object)."""
if
self
.
form
:
if
data
:
return
self
.
form
(
data
)
...
...
@@ -156,15 +178,25 @@ class Resource(object):
"""Perform any resource-specific data deserialization and/or validation
after the initial HTTP content-type deserialization has taken place.
Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
By default this uses form validation to filter the basic input into the required types."""
if
self
.
form
is
None
:
return
data
return
(
data
,
None
)
form_instance
=
self
.
get_bound_form
(
data
)
if
not
form_instance
.
is_valid
():
if
not
form_instance
.
errors
:
details
=
'No content was supplied'
else
:
details
=
dict
((
key
,
map
(
unicode
,
val
))
for
(
key
,
val
)
in
form_instance
.
errors
.
iteritems
())
if
form_instance
.
non_field_errors
():
details
[
'_extra'
]
=
self
.
form
.
non_field_errors
()
if
not
self
.
form
.
is_valid
():
details
=
dict
((
key
,
map
(
unicode
,
val
))
for
(
key
,
val
)
in
self
.
form
.
errors
.
iteritems
())
raise
ResourceException
(
STATUS_400_BAD_REQUEST
,
{
'detail'
:
details
})
return
self
.
form
.
cleaned_data
return
(
form_instance
.
cleaned_data
,
form_instance
)
def
cleanup_response
(
self
,
data
):
...
...
@@ -188,7 +220,7 @@ class Resource(object):
return
self
.
parsers
[
content_type
]
except
KeyError
:
raise
ResourceException
(
STATUS_415_UNSUPPORTED_MEDIA_TYPE
,
{
'detail'
:
'Unsupported
content
type
\'
%
s
\'
'
%
content_type
})
{
'detail'
:
'Unsupported
media
type
\'
%
s
\'
'
%
content_type
})
def
determine_emitter
(
self
,
request
):
...
...
@@ -253,13 +285,13 @@ class Resource(object):
5. serialize response data into response content, using standard HTTP content negotiation
"""
emitter
=
None
method
=
self
.
determine_method
(
request
)
# We make these attributes to allow for a certain amount of munging,
# eg The HTML emitter needs to render this information
self
.
method
=
self
.
determine_method
(
request
)
self
.
form
=
None
self
.
request
=
request
self
.
form
_instance
=
None
self
.
resp_status
=
None
self
.
resp_content
=
None
self
.
resp_headers
=
{}
try
:
...
...
@@ -267,32 +299,31 @@ class Resource(object):
mimetype
,
emitter
=
self
.
determine_emitter
(
request
)
# Ensure the requested operation is permitted on this resource
self
.
check_method_allowed
(
self
.
method
)
self
.
check_method_allowed
(
method
)
# Get the appropriate create/read/update/delete function
func
=
getattr
(
self
,
self
.
CALLMAP
.
get
(
self
.
method
,
''
))
func
=
getattr
(
self
,
self
.
CALLMAP
.
get
(
method
,
''
))
# Either generate the response data, deserializing and validating any request data
if
self
.
method
in
(
'PUT'
,
'POST'
):
if
method
in
(
'PUT'
,
'POST'
):
parser
=
self
.
determine_parser
(
request
)
data
=
parser
(
self
)
.
parse
(
request
.
raw_post_data
)
self
.
form
=
self
.
determine_form
(
data
)
data
=
self
.
cleanup_request
(
data
)
(
data
,
self
.
form_instance
)
=
self
.
cleanup_request
(
data
)
(
self
.
resp_status
,
ret
,
self
.
resp_headers
)
=
func
(
data
,
request
.
META
,
*
args
,
**
kwargs
)
else
:
(
self
.
resp_status
,
ret
,
self
.
resp_headers
)
=
func
(
request
.
META
,
*
args
,
**
kwargs
)
self
.
form
=
self
.
determine
_form
(
ret
,
is_response
=
True
)
self
.
form
_instance
=
self
.
get_bound
_form
(
ret
,
is_response
=
True
)
except
ResourceException
,
exc
:
(
self
.
resp_status
,
ret
,
self
.
resp_headers
)
=
(
exc
.
status
,
exc
.
content
,
exc
.
headers
)
if
emitter
is
None
:
mimetype
,
emitter
=
self
.
emitters
[
0
]
if
self
.
form
is
None
:
self
.
form
=
self
.
determine_form
()
if
self
.
form_instance
is
None
:
self
.
form_instance
=
self
.
get_bound_form
()
# Always add the allow header
self
.
resp_headers
[
'Allow'
]
=
', '
.
join
([
self
.
REVERSE_CALLMAP
[
operation
]
for
operation
in
self
.
allowed_operations
])
...
...
@@ -315,17 +346,16 @@ from django.db.models.query import QuerySet
from
django.db.models
import
Model
import
decimal
import
inspect
import
re
class
ModelResource
(
Resource
):
model
=
None
fields
=
None
form_fields
=
None
def
determine
_form
(
self
,
data
=
None
,
is_response
=
False
):
def
get_bound
_form
(
self
,
data
=
None
,
is_response
=
False
):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if
self
.
form
:
return
super
(
self
.
__class__
,
self
)
.
determine
_form
(
data
,
is_response
=
is_response
)
return
super
(
self
.
__class__
,
self
)
.
get_bound
_form
(
data
,
is_response
=
is_response
)
elif
self
.
model
:
class
NewModelForm
(
ModelForm
):
...
...
@@ -640,7 +670,7 @@ class ModelResource(Resource):
class
QueryModelResource
(
ModelResource
):
allowed_methods
=
(
'read'
,)
def
determine
_form
(
self
,
data
=
None
,
is_response
=
False
):
def
get_bound
_form
(
self
,
data
=
None
,
is_response
=
False
):
return
None
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
...
...
src/rest/templates/emitter.html
View file @
764fbe33
...
...
@@ -7,26 +7,27 @@
pre
{
border
:
1px
solid
black
;
padding
:
1em
;
background
:
#ffd
}
div
.action
{
padding
:
0.5em
1em
;
margin-bottom
:
0.5em
;
background
:
#ddf
}
</style>
<title>
API - {{ resource.name }}
</title>
</head>
<body>
<h1>
{{ resource
_
name }}
</h1>
<p>
{{ resource
_doc
}}
</p>
<pre><b>
{{
status }} {{ reason
}}
</b>
{% autoescape off %}
{% for key, val in headers.items %}
<b>
{{ key }}:
</b>
{{ val|urlize_quoted_links }}
<h1>
{{ resource
.
name }}
</h1>
<p>
{{ resource
.description
}}
</p>
<pre><b>
{{
resource.resp_status }} {{ resource.resp_status_text
}}
</b>
{% autoescape off %}
{% for key, val in
resource.resp_
headers.items %}
<b>
{{ key }}:
</b>
{{ val|urlize_quoted_links }}
{% endfor %}
{{ content|urlize_quoted_links }}
</pre>
{% endautoescape %}
{% if 'read' in resource.allowed_operations %}
<div
class=
'action'
>
<a
href=
'{{ request.path }}'
>
Read
</a>
<a
href=
'{{ re
source.re
quest.path }}'
>
Read
</a>
</div>
{% endif %}
{% if 'create' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ re
source.re
quest.path }}"
method=
"POST"
>
{% csrf_token %}
{{
create_form
.as_p }}
{{
resource.form_instance
.as_p }}
<input
type=
"submit"
value=
"Create"
/>
</form>
</div>
...
...
@@ -34,10 +35,10 @@
{% if 'update' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ re
source.re
quest.path }}"
method=
"POST"
>
<input
type=
"hidden"
name=
"{{ resource.METHOD_PARAM}}"
value=
"PUT"
/>
{% csrf_token %}
{{
create_form
.as_p }}
{{
resource.form_instance
.as_p }}
<input
type=
"submit"
value=
"Update"
/>
</form>
</div>
...
...
@@ -45,7 +46,7 @@
{% if 'delete' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ re
source.re
quest.path }}"
method=
"POST"
>
{% csrf_token %}
<input
type=
"hidden"
name=
"{{ resource.METHOD_PARAM}}"
value=
"DELETE"
/>
<input
type=
"submit"
value=
"Delete"
/>
...
...
src/rest/templates/emitter.txt
View file @
764fbe33
{% autoescape off %}HTTP Status {{ status }}
{% for key, val in headers.items %}{{ key }}: {{ val }}
{{ resource.name }}
{{ resource.description }}
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
{% for key, val in resource.resp_headers.items %}{{ key }}: {{ val }}
{% endfor %}
{{ content }}{% endautoescape %}
\ No newline at end of file
{{ content }}{% endautoescape %}
src/testapp/views.py
View file @
764fbe33
from
rest.resource
import
Resource
,
ModelResource
,
QueryModelResource
from
testapp.models
import
BlogPost
,
Comment
##### Root Resource #####
class
RootResource
(
Resource
):
"""This is the top level resource for the API.
All the sub-resources are discoverable from here."""
...
...
@@ -11,49 +13,53 @@ class RootResource(Resource):
'blog-post'
:
self
.
reverse
(
BlogPostCreator
)},
{})
# Blog Post Resources
##### Blog Post Resources #####
BLOG_POST_FIELDS
=
(
'created'
,
'title'
,
'slug'
,
'content'
,
'absolute_url'
,
'comment_url'
,
'comments_url'
)
class
BlogPostList
(
QueryModelResource
):
"""A resource which lists all existing blog posts."""
allowed_operations
=
(
'read'
,
)
model
=
BlogPost
fields
=
BLOG_POST_FIELDS
class
BlogPostCreator
(
ModelResource
):
"""A resource with which blog posts may be created."""
allowed_operations
=
(
'create'
,)
model
=
BlogPost
fields
=
(
'created'
,
'title'
,
'slug'
,
'content'
,
'absolute_url'
,
'comment_url'
,
'comments_url'
)
fields
=
BLOG_POST_FIELDS
class
BlogPostInstance
(
ModelResource
):
"""A resource which represents a single blog post."""
allowed_operations
=
(
'read'
,
'update'
,
'delete'
)
model
=
BlogPost
fields
=
(
'created'
,
'title'
,
'slug'
,
'content'
,
'absolute_url'
,
'comment_url'
,
'comments_url'
)
fields
=
BLOG_POST_FIELDS
# Comment Resources
##### Comment Resources #####
COMMENT_FIELDS
=
(
'username'
,
'comment'
,
'created'
,
'rating'
,
'absolute_url'
,
'blogpost_url'
)
class
CommentList
(
QueryModelResource
):
"""A resource which lists all existing comments for a given blog post."""
allowed_operations
=
(
'read'
,
)
model
=
Comment
fields
=
COMMENT_FIELDS
class
CommentCreator
(
ModelResource
):
"""A resource with which blog comments may be created for a given blog post."""
allowed_operations
=
(
'create'
,)
model
=
Comment
fields
=
(
'username'
,
'comment'
,
'created'
,
'rating'
,
'absolute_url'
,
'blogpost_url'
)
fields
=
COMMENT_FIELDS
class
CommentInstance
(
ModelResource
):
"""A resource which represents a single comment."""
allowed_operations
=
(
'read'
,
'update'
,
'delete'
)
model
=
Comment
fields
=
(
'username'
,
'comment'
,
'created'
,
'rating'
,
'absolute_url'
,
'blogpost_url'
)
fields
=
COMMENT_FIELDS
#
#'read-only-api': self.reverse(ReadOnlyResource),
# 'write-only-api': self.reverse(WriteOnlyResource),
...
...
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