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
b0ce3f92
Commit
b0ce3f92
authored
Jan 14, 2011
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added formats, various form improvements, more refactoring/cleanup
parent
764fbe33
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
495 additions
and
393 deletions
+495
-393
src/rest/emitters.py
+7
-1
src/rest/modelresource.py
+346
-0
src/rest/resource.py
+67
-375
src/rest/templates/emitter.html
+42
-7
src/rest/templates/emitter.txt
+1
-0
src/rest/templatetags/add_query_param.py
+17
-0
src/rest/utils.py
+4
-4
src/testapp/models.py
+9
-5
src/testapp/views.py
+2
-1
No files found.
src/rest/emitters.py
View file @
b0ce3f92
...
...
@@ -3,6 +3,8 @@ import json
from
utils
import
dict2xml
class
BaseEmitter
(
object
):
uses_forms
=
False
def
__init__
(
self
,
resource
):
self
.
resource
=
resource
...
...
@@ -24,11 +26,14 @@ class TemplatedEmitter(BaseEmitter):
'resource'
:
self
.
resource
,
})
ret
=
template
.
render
(
context
)
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
if
self
.
resource
.
resp_status
==
204
:
self
.
resource
.
resp_status
=
200
return
template
.
render
(
context
)
return
ret
class
JSONEmitter
(
BaseEmitter
):
def
emit
(
self
,
output
):
...
...
@@ -46,6 +51,7 @@ class XMLEmitter(BaseEmitter):
class
HTMLEmitter
(
TemplatedEmitter
):
template
=
'emitter.html'
uses_forms
=
True
class
TextEmitter
(
TemplatedEmitter
):
template
=
'emitter.txt'
...
...
src/rest/modelresource.py
0 → 100644
View file @
b0ce3f92
"""TODO: docs
"""
from
django.forms
import
ModelForm
from
django.db.models.query
import
QuerySet
from
django.db.models
import
Model
from
rest.resource
import
Resource
import
decimal
import
inspect
import
re
class
ModelResource
(
Resource
):
model
=
None
fields
=
None
form_fields
=
None
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
)
.
get_bound_form
(
data
,
is_response
=
is_response
)
elif
self
.
model
:
class
NewModelForm
(
ModelForm
):
class
Meta
:
model
=
self
.
model
fields
=
self
.
form_fields
if
self
.
form_fields
else
None
#self.fields
if
data
and
not
is_response
:
return
NewModelForm
(
data
)
elif
data
and
is_response
:
return
NewModelForm
(
instance
=
data
)
else
:
return
NewModelForm
()
else
:
return
None
def
cleanup_response
(
self
,
data
):
"""A munging of Piston's pre-serialization. Returns a dict"""
def
_any
(
thing
,
fields
=
()):
"""
Dispatch, all types are routed through here.
"""
ret
=
None
if
isinstance
(
thing
,
QuerySet
):
ret
=
_qs
(
thing
,
fields
=
fields
)
elif
isinstance
(
thing
,
(
tuple
,
list
)):
ret
=
_list
(
thing
)
elif
isinstance
(
thing
,
dict
):
ret
=
_dict
(
thing
)
elif
isinstance
(
thing
,
int
):
ret
=
thing
elif
isinstance
(
thing
,
bool
):
ret
=
thing
elif
isinstance
(
thing
,
type
(
None
)):
ret
=
thing
elif
isinstance
(
thing
,
decimal
.
Decimal
):
ret
=
str
(
thing
)
elif
isinstance
(
thing
,
Model
):
ret
=
_model
(
thing
,
fields
=
fields
)
#elif isinstance(thing, HttpResponse): TRC
# raise HttpStatusCode(thing)
elif
inspect
.
isfunction
(
thing
):
if
not
inspect
.
getargspec
(
thing
)[
0
]:
ret
=
_any
(
thing
())
elif
hasattr
(
thing
,
'__emittable__'
):
f
=
thing
.
__emittable__
if
inspect
.
ismethod
(
f
)
and
len
(
inspect
.
getargspec
(
f
)[
0
])
==
1
:
ret
=
_any
(
f
())
else
:
ret
=
str
(
thing
)
# TRC TODO: Change this back!
return
ret
def
_fk
(
data
,
field
):
"""
Foreign keys.
"""
return
_any
(
getattr
(
data
,
field
.
name
))
def
_related
(
data
,
fields
=
()):
"""
Foreign keys.
"""
return
[
_model
(
m
,
fields
)
for
m
in
data
.
iterator
()
]
def
_m2m
(
data
,
field
,
fields
=
()):
"""
Many to many (re-route to `_model`.)
"""
return
[
_model
(
m
,
fields
)
for
m
in
getattr
(
data
,
field
.
name
)
.
iterator
()
]
def
_method_fields
(
data
,
fields
):
if
not
data
:
return
{
}
has
=
dir
(
data
)
ret
=
dict
()
for
field
in
fields
:
if
field
in
has
:
ret
[
field
]
=
getattr
(
data
,
field
)
return
ret
def
_model
(
data
,
fields
=
()):
"""
Models. Will respect the `fields` and/or
`exclude` on the handler (see `typemapper`.)
"""
ret
=
{
}
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
handler
=
None
# TRC
get_absolute_url
=
False
if
handler
or
fields
:
v
=
lambda
f
:
getattr
(
data
,
f
.
attname
)
if
not
fields
:
"""
Fields was not specified, try to find teh correct
version in the typemapper we were sent.
"""
mapped
=
self
.
in_typemapper
(
type
(
data
),
self
.
anonymous
)
get_fields
=
set
(
mapped
.
fields
)
exclude_fields
=
set
(
mapped
.
exclude
)
.
difference
(
get_fields
)
if
not
get_fields
:
get_fields
=
set
([
f
.
attname
.
replace
(
"_id"
,
""
,
1
)
for
f
in
data
.
_meta
.
fields
])
# sets can be negated.
for
exclude
in
exclude_fields
:
if
isinstance
(
exclude
,
basestring
):
get_fields
.
discard
(
exclude
)
elif
isinstance
(
exclude
,
re
.
_pattern_type
):
for
field
in
get_fields
.
copy
():
if
exclude
.
match
(
field
):
get_fields
.
discard
(
field
)
get_absolute_url
=
True
else
:
get_fields
=
set
(
fields
)
if
'absolute_url'
in
get_fields
:
# MOVED (TRC)
get_absolute_url
=
True
met_fields
=
_method_fields
(
handler
,
get_fields
)
# TRC
for
f
in
data
.
_meta
.
local_fields
:
if
f
.
serialize
and
not
any
([
p
in
met_fields
for
p
in
[
f
.
attname
,
f
.
name
]]):
if
not
f
.
rel
:
if
f
.
attname
in
get_fields
:
ret
[
f
.
attname
]
=
_any
(
v
(
f
))
get_fields
.
remove
(
f
.
attname
)
else
:
if
f
.
attname
[:
-
3
]
in
get_fields
:
ret
[
f
.
name
]
=
_fk
(
data
,
f
)
get_fields
.
remove
(
f
.
name
)
for
mf
in
data
.
_meta
.
many_to_many
:
if
mf
.
serialize
and
mf
.
attname
not
in
met_fields
:
if
mf
.
attname
in
get_fields
:
ret
[
mf
.
name
]
=
_m2m
(
data
,
mf
)
get_fields
.
remove
(
mf
.
name
)
# try to get the remainder of fields
for
maybe_field
in
get_fields
:
if
isinstance
(
maybe_field
,
(
list
,
tuple
)):
model
,
fields
=
maybe_field
inst
=
getattr
(
data
,
model
,
None
)
if
inst
:
if
hasattr
(
inst
,
'all'
):
ret
[
model
]
=
_related
(
inst
,
fields
)
elif
callable
(
inst
):
if
len
(
inspect
.
getargspec
(
inst
)[
0
])
==
1
:
ret
[
model
]
=
_any
(
inst
(),
fields
)
else
:
ret
[
model
]
=
_model
(
inst
,
fields
)
elif
maybe_field
in
met_fields
:
# Overriding normal field which has a "resource method"
# so you can alter the contents of certain fields without
# using different names.
ret
[
maybe_field
]
=
_any
(
met_fields
[
maybe_field
](
data
))
else
:
maybe
=
getattr
(
data
,
maybe_field
,
None
)
if
maybe
:
if
callable
(
maybe
):
if
len
(
inspect
.
getargspec
(
maybe
)[
0
])
==
1
:
ret
[
maybe_field
]
=
_any
(
maybe
())
else
:
ret
[
maybe_field
]
=
_any
(
maybe
)
else
:
pass
# TRC
#handler_f = getattr(handler or self.handler, maybe_field, None)
#
#if handler_f:
# ret[maybe_field] = _any(handler_f(data))
else
:
# Add absolute_url if it exists
get_absolute_url
=
True
# Add all the fields
for
f
in
data
.
_meta
.
fields
:
if
f
.
attname
!=
'id'
:
ret
[
f
.
attname
]
=
_any
(
getattr
(
data
,
f
.
attname
))
# Add all the propertiess
klass
=
data
.
__class__
for
attr
in
dir
(
klass
):
if
not
attr
.
startswith
(
'_'
)
and
not
attr
in
(
'pk'
,
'id'
)
and
isinstance
(
getattr
(
klass
,
attr
,
None
),
property
):
#if attr.endswith('_url') or attr.endswith('_uri'):
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
#else:
ret
[
attr
]
=
_any
(
getattr
(
data
,
attr
))
#fields = dir(data.__class__) + ret.keys()
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
#print add_ons
###print dir(data.__class__)
#from django.db.models import Model
#model_fields = dir(Model)
#for attr in dir(data):
## #if attr.startswith('_'):
## # continue
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
#for k in add_ons:
# ret[k] = _any(getattr(data, k))
# TRC
# resouce uri
#if self.in_typemapper(type(data), self.anonymous):
# handler = self.in_typemapper(type(data), self.anonymous)
# if hasattr(handler, 'resource_uri'):
# url_id, fields = handler.resource_uri()
# ret['resource_uri'] = permalink( lambda: (url_id,
# (getattr(data, f) for f in fields) ) )()
# TRC
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
# try: ret['resource_uri'] = data.get_api_url()
# except: pass
# absolute uri
if
hasattr
(
data
,
'get_absolute_url'
)
and
get_absolute_url
:
try
:
ret
[
'absolute_url'
]
=
data
.
get_absolute_url
()
except
:
pass
for
key
,
val
in
ret
.
items
():
if
key
.
endswith
(
'_url'
)
or
key
.
endswith
(
'_uri'
):
ret
[
key
]
=
self
.
add_domain
(
val
)
return
ret
def
_qs
(
data
,
fields
=
()):
"""
Querysets.
"""
return
[
_any
(
v
,
fields
)
for
v
in
data
]
def
_list
(
data
):
"""
Lists.
"""
return
[
_any
(
v
)
for
v
in
data
]
def
_dict
(
data
):
"""
Dictionaries.
"""
return
dict
([
(
k
,
_any
(
v
))
for
k
,
v
in
data
.
iteritems
()
])
# Kickstart the seralizin'.
return
_any
(
data
,
self
.
fields
)
def
create
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
# TODO: test creation on a non-existing resource url
all_kw_args
=
dict
(
data
.
items
()
+
kwargs
.
items
())
instance
=
self
.
model
(
**
all_kw_args
)
instance
.
save
()
headers
=
{}
if
hasattr
(
instance
,
'get_absolute_url'
):
headers
[
'Location'
]
=
self
.
add_domain
(
instance
.
get_absolute_url
())
return
(
201
,
instance
,
headers
)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
try
:
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
except
self
.
model
.
DoesNotExist
:
return
(
404
,
None
,
{})
return
(
200
,
instance
,
{})
def
update
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
try
:
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
for
(
key
,
val
)
in
data
.
items
():
setattr
(
instance
,
key
,
val
)
except
self
.
model
.
DoesNotExist
:
instance
=
self
.
model
(
**
data
)
instance
.
save
()
instance
.
save
()
return
(
200
,
instance
,
{})
def
delete
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
try
:
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
except
self
.
model
.
DoesNotExist
:
return
(
404
,
None
,
{})
instance
.
delete
()
return
(
204
,
None
,
{})
class
QueryModelResource
(
ModelResource
):
allowed_methods
=
(
'read'
,)
queryset
=
None
def
get_bound_form
(
self
,
data
=
None
,
is_response
=
False
):
return
None
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
if
self
.
queryset
:
return
(
200
,
self
.
queryset
,
{})
queryset
=
self
.
model
.
objects
.
all
()
return
(
200
,
queryset
,
{})
src/rest/resource.py
View file @
b0ce3f92
from
django.http
import
HttpResponse
from
django.contrib.sites.models
import
Site
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
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
# TODO: Return basic object, not tuple
# TODO: Take request, not headers
# TODO: Remove self.blah munging (Add a ResponseContext object)
# TODO: Erroring on non-existent fields
# TODO: Standard exception classes and module for status codes
# TODO: Figure how out references and named urls need to work nicely
# TODO: POST on existing 404 URL, PUT on existing 404 URL
# TODO: Authentication
#
# FUTURE: Erroring on read-only fields
# Documentation, Release
#
STATUS_400_BAD_REQUEST
=
400
STATUS_405_METHOD_NOT_ALLOWED
=
405
...
...
@@ -45,15 +60,17 @@ class Resource(object):
# Map standard HTTP methods to RESTful operations
CALLMAP
=
{
'GET'
:
'read'
,
'POST'
:
'create'
,
'PUT'
:
'update'
,
'DELETE'
:
'delete'
}
REVERSE_CALLMAP
=
dict
([(
val
,
key
)
for
(
key
,
val
)
in
CALLMAP
.
items
()])
# Some reserved parameters to allow us to use standard HTML forms with our resource.
METHOD_PARAM
=
'_method'
ACCEPT_PARAM
=
'_accept'
CSRF_PARAM
=
'csrfmiddlewaretoken'
RESERVED_PARAMS
=
set
((
METHOD_PARAM
,
ACCEPT_PARAM
,
CSRF_PARAM
))
# Some reserved parameters to allow us to use standard HTML forms with our resource
METHOD_PARAM
=
'_method'
# Allow POST overloading
ACCEPT_PARAM
=
'_accept'
# Allow override of Accept header in GET requests
CONTENTTYPE_PARAM
=
'_contenttype'
# Allow override of Content-Type header (allows sending arbitrary content with standard forms)
CONTENT_PARAM
=
'_content'
# Allow override of body content (allows sending arbitrary content with standard forms)
CSRF_PARAM
=
'csrfmiddlewaretoken'
# Django's CSRF token
USE_SITEMAP_FOR_ABSOLUTE_URLS
=
False
RESERVED_PARAMS
=
set
((
METHOD_PARAM
,
ACCEPT_PARAM
,
CONTENTTYPE_PARAM
,
CONTENT_PARAM
,
CSRF_PARAM
))
def
__new__
(
cls
,
request
,
*
args
,
**
kwargs
):
...
...
@@ -69,19 +86,22 @@ class Resource(object):
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."""
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
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__
By default this is the class's docstring with leading line spaces stripped."""
return
re
.
sub
(
re
.
compile
(
'^ +'
,
re
.
MULTILINE
),
''
,
self
.
__doc__
)
def
available_content_types
(
self
):
"""Return a list of strings of all the content-types that this resource can emit."""
return
[
item
[
0
]
for
item
in
self
.
emitters
]
def
resp_status_text
(
self
):
"""Return reason text corrosponding to our HTTP response status code.
Provided for convienience."""
...
...
@@ -89,19 +109,22 @@ class Resource(object):
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.
Provided for convienience."""
return
self
.
request
.
build_absolute_uri
(
reverse
(
view
,
*
args
,
**
kwargs
))
"""Return a fully qualified URI for a given view or resource.
Use the Sites framework if possible, otherwise fallback to using the current request."""
return
self
.
add_domain
(
reverse
(
view
,
*
args
,
**
kwargs
))
def
make_absolute
(
self
,
uri
):
"""Given a relative URI, return an absolute URI using the current request as the base URI.
TODO: Add SITEMAP option.
def
add_domain
(
self
,
path
):
"""Given a path, return an fully qualified URI.
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
try
:
site
=
Site
.
objects
.
get_current
()
if
site
.
domain
and
site
.
domain
!=
'example.com'
:
return
'http://
%
s
%
s'
%
(
site
.
domain
,
path
)
except
:
pass
Provided for convienience."""
return
self
.
request
.
build_absolute_uri
(
uri
)
return
self
.
request
.
build_absolute_uri
(
path
)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
...
...
@@ -134,17 +157,18 @@ class Resource(object):
def
determine_method
(
self
,
request
):
"""Determine the HTTP method that this request should be treated as.
Allow for PUT and DELETE tunneling via the _method parameter."""
method
=
request
.
method
if
method
==
'POST'
and
request
.
POST
.
has_key
(
self
.
METHOD_PARAM
):
method
=
request
.
method
.
upper
()
if
method
==
'POST'
and
self
.
METHOD_PARAM
and
request
.
POST
.
has_key
(
self
.
METHOD_PARAM
):
method
=
request
.
POST
[
self
.
METHOD_PARAM
]
.
upper
()
return
method
def
authenticate
(
self
):
"""
...
"""
"""
TODO
"""
# user = ...
# if DEBUG and request is from localhost
# if anon_user and not anon_allowed_operations raise PermissionDenied
# return
...
...
@@ -174,17 +198,15 @@ class Resource(object):
return
None
def
cleanup_request
(
self
,
data
):
def
cleanup_request
(
self
,
data
,
form_instance
):
"""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
,
None
)
form_instance
=
self
.
get_bound_form
(
data
)
if
form_instance
is
None
:
return
data
if
not
form_instance
.
is_valid
():
if
not
form_instance
.
errors
:
...
...
@@ -196,7 +218,7 @@ class Resource(object):
raise
ResourceException
(
STATUS_400_BAD_REQUEST
,
{
'detail'
:
details
})
return
(
form_instance
.
cleaned_data
,
form_instance
)
return
form_instance
.
cleaned_data
def
cleanup_response
(
self
,
data
):
...
...
@@ -230,11 +252,17 @@ class Resource(object):
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
default
=
self
.
emitters
[
0
]
if
not
request
.
META
.
has_key
(
'HTTP_ACCEPT'
):
if
self
.
ACCEPT_PARAM
and
request
.
GET
.
get
(
self
.
ACCEPT_PARAM
,
None
):
# Use _accept parameter override
accept_list
=
[(
request
.
GET
.
get
(
self
.
ACCEPT_PARAM
),)]
elif
request
.
META
.
has_key
(
'HTTP_ACCEPT'
):
# Use standard HTTP Accept negotiation
accept_list
=
[
item
.
split
(
';'
)
for
item
in
request
.
META
[
"HTTP_ACCEPT"
]
.
split
(
','
)]
else
:
# No accept header specified
return
default
# Parse the accept header into a dict of {Priority: List of Mimetypes}
accept_list
=
[
item
.
split
(
';'
)
for
item
in
request
.
META
[
"HTTP_ACCEPT"
]
.
split
(
','
)]
accept_dict
=
{}
for
item
in
accept_list
:
mimetype
=
item
[
0
]
.
strip
()
...
...
@@ -308,19 +336,21 @@ class Resource(object):
if
method
in
(
'PUT'
,
'POST'
):
parser
=
self
.
determine_parser
(
request
)
data
=
parser
(
self
)
.
parse
(
request
.
raw_post_data
)
(
data
,
self
.
form_instance
)
=
self
.
cleanup_request
(
data
)
self
.
form_instance
=
self
.
get_bound_form
(
data
)
data
=
self
.
cleanup_request
(
data
,
self
.
form_instance
)
(
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_instance
=
self
.
get_bound_form
(
ret
,
is_response
=
True
)
if
emitter
.
uses_forms
:
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_instance
is
None
:
if
self
.
form_instance
is
None
and
emitter
.
uses_forms
:
self
.
form_instance
=
self
.
get_bound_form
()
...
...
@@ -338,341 +368,3 @@ class Resource(object):
return
resp
from
django.forms
import
ModelForm
from
django.db.models.query
import
QuerySet
from
django.db.models
import
Model
import
decimal
import
inspect
class
ModelResource
(
Resource
):
model
=
None
fields
=
None
form_fields
=
None
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
)
.
get_bound_form
(
data
,
is_response
=
is_response
)
elif
self
.
model
:
class
NewModelForm
(
ModelForm
):
class
Meta
:
model
=
self
.
model
fields
=
self
.
form_fields
if
self
.
form_fields
else
None
#self.fields
if
data
and
not
is_response
:
return
NewModelForm
(
data
)
elif
data
and
is_response
:
return
NewModelForm
(
instance
=
data
)
else
:
return
NewModelForm
()
else
:
return
None
def
cleanup_response
(
self
,
data
):
"""
Recursively serialize a lot of types, and
in cases where it doesn't recognize the type,
it will fall back to Django's `smart_unicode`.
Returns `dict`.
"""
def
_any
(
thing
,
fields
=
()):
"""
Dispatch, all types are routed through here.
"""
ret
=
None
if
isinstance
(
thing
,
QuerySet
):
ret
=
_qs
(
thing
,
fields
=
fields
)
elif
isinstance
(
thing
,
(
tuple
,
list
)):
ret
=
_list
(
thing
)
elif
isinstance
(
thing
,
dict
):
ret
=
_dict
(
thing
)
elif
isinstance
(
thing
,
int
):
ret
=
thing
elif
isinstance
(
thing
,
bool
):
ret
=
thing
elif
isinstance
(
thing
,
type
(
None
)):
ret
=
thing
elif
isinstance
(
thing
,
decimal
.
Decimal
):
ret
=
str
(
thing
)
elif
isinstance
(
thing
,
Model
):
ret
=
_model
(
thing
,
fields
=
fields
)
#elif isinstance(thing, HttpResponse): TRC
# raise HttpStatusCode(thing)
elif
inspect
.
isfunction
(
thing
):
if
not
inspect
.
getargspec
(
thing
)[
0
]:
ret
=
_any
(
thing
())
elif
hasattr
(
thing
,
'__emittable__'
):
f
=
thing
.
__emittable__
if
inspect
.
ismethod
(
f
)
and
len
(
inspect
.
getargspec
(
f
)[
0
])
==
1
:
ret
=
_any
(
f
())
else
:
ret
=
str
(
thing
)
# TRC TODO: Change this back!
return
ret
def
_fk
(
data
,
field
):
"""
Foreign keys.
"""
return
_any
(
getattr
(
data
,
field
.
name
))
def
_related
(
data
,
fields
=
()):
"""
Foreign keys.
"""
return
[
_model
(
m
,
fields
)
for
m
in
data
.
iterator
()
]
def
_m2m
(
data
,
field
,
fields
=
()):
"""
Many to many (re-route to `_model`.)
"""
return
[
_model
(
m
,
fields
)
for
m
in
getattr
(
data
,
field
.
name
)
.
iterator
()
]
def
_method_fields
(
data
,
fields
):
if
not
data
:
return
{
}
has
=
dir
(
data
)
ret
=
dict
()
for
field
in
fields
:
if
field
in
has
:
ret
[
field
]
=
getattr
(
data
,
field
)
return
ret
def
_model
(
data
,
fields
=
()):
"""
Models. Will respect the `fields` and/or
`exclude` on the handler (see `typemapper`.)
"""
ret
=
{
}
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
handler
=
None
# TRC
get_absolute_url
=
False
if
handler
or
fields
:
v
=
lambda
f
:
getattr
(
data
,
f
.
attname
)
if
not
fields
:
"""
Fields was not specified, try to find teh correct
version in the typemapper we were sent.
"""
mapped
=
self
.
in_typemapper
(
type
(
data
),
self
.
anonymous
)
get_fields
=
set
(
mapped
.
fields
)
exclude_fields
=
set
(
mapped
.
exclude
)
.
difference
(
get_fields
)
if
not
get_fields
:
get_fields
=
set
([
f
.
attname
.
replace
(
"_id"
,
""
,
1
)
for
f
in
data
.
_meta
.
fields
])
# sets can be negated.
for
exclude
in
exclude_fields
:
if
isinstance
(
exclude
,
basestring
):
get_fields
.
discard
(
exclude
)
elif
isinstance
(
exclude
,
re
.
_pattern_type
):
for
field
in
get_fields
.
copy
():
if
exclude
.
match
(
field
):
get_fields
.
discard
(
field
)
get_absolute_url
=
True
else
:
get_fields
=
set
(
fields
)
if
'absolute_url'
in
get_fields
:
# MOVED (TRC)
get_absolute_url
=
True
met_fields
=
_method_fields
(
handler
,
get_fields
)
# TRC
for
f
in
data
.
_meta
.
local_fields
:
if
f
.
serialize
and
not
any
([
p
in
met_fields
for
p
in
[
f
.
attname
,
f
.
name
]]):
if
not
f
.
rel
:
if
f
.
attname
in
get_fields
:
ret
[
f
.
attname
]
=
_any
(
v
(
f
))
get_fields
.
remove
(
f
.
attname
)
else
:
if
f
.
attname
[:
-
3
]
in
get_fields
:
ret
[
f
.
name
]
=
_fk
(
data
,
f
)
get_fields
.
remove
(
f
.
name
)
for
mf
in
data
.
_meta
.
many_to_many
:
if
mf
.
serialize
and
mf
.
attname
not
in
met_fields
:
if
mf
.
attname
in
get_fields
:
ret
[
mf
.
name
]
=
_m2m
(
data
,
mf
)
get_fields
.
remove
(
mf
.
name
)
# try to get the remainder of fields
for
maybe_field
in
get_fields
:
if
isinstance
(
maybe_field
,
(
list
,
tuple
)):
model
,
fields
=
maybe_field
inst
=
getattr
(
data
,
model
,
None
)
if
inst
:
if
hasattr
(
inst
,
'all'
):
ret
[
model
]
=
_related
(
inst
,
fields
)
elif
callable
(
inst
):
if
len
(
inspect
.
getargspec
(
inst
)[
0
])
==
1
:
ret
[
model
]
=
_any
(
inst
(),
fields
)
else
:
ret
[
model
]
=
_model
(
inst
,
fields
)
elif
maybe_field
in
met_fields
:
# Overriding normal field which has a "resource method"
# so you can alter the contents of certain fields without
# using different names.
ret
[
maybe_field
]
=
_any
(
met_fields
[
maybe_field
](
data
))
else
:
maybe
=
getattr
(
data
,
maybe_field
,
None
)
if
maybe
:
if
callable
(
maybe
):
if
len
(
inspect
.
getargspec
(
maybe
)[
0
])
==
1
:
ret
[
maybe_field
]
=
_any
(
maybe
())
else
:
ret
[
maybe_field
]
=
_any
(
maybe
)
else
:
pass
# TRC
#handler_f = getattr(handler or self.handler, maybe_field, None)
#
#if handler_f:
# ret[maybe_field] = _any(handler_f(data))
else
:
# Add absolute_url if it exists
get_absolute_url
=
True
# Add all the fields
for
f
in
data
.
_meta
.
fields
:
if
f
.
attname
!=
'id'
:
ret
[
f
.
attname
]
=
_any
(
getattr
(
data
,
f
.
attname
))
# Add all the propertiess
klass
=
data
.
__class__
for
attr
in
dir
(
klass
):
if
not
attr
.
startswith
(
'_'
)
and
not
attr
in
(
'pk'
,
'id'
)
and
isinstance
(
getattr
(
klass
,
attr
,
None
),
property
):
#if attr.endswith('_url') or attr.endswith('_uri'):
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
#else:
ret
[
attr
]
=
_any
(
getattr
(
data
,
attr
))
#fields = dir(data.__class__) + ret.keys()
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
#print add_ons
###print dir(data.__class__)
#from django.db.models import Model
#model_fields = dir(Model)
#for attr in dir(data):
## #if attr.startswith('_'):
## # continue
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
#for k in add_ons:
# ret[k] = _any(getattr(data, k))
# TRC
# resouce uri
#if self.in_typemapper(type(data), self.anonymous):
# handler = self.in_typemapper(type(data), self.anonymous)
# if hasattr(handler, 'resource_uri'):
# url_id, fields = handler.resource_uri()
# ret['resource_uri'] = permalink( lambda: (url_id,
# (getattr(data, f) for f in fields) ) )()
# TRC
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
# try: ret['resource_uri'] = data.get_api_url()
# except: pass
# absolute uri
if
hasattr
(
data
,
'get_absolute_url'
)
and
get_absolute_url
:
try
:
ret
[
'absolute_url'
]
=
self
.
make_absolute
(
data
.
get_absolute_url
())
except
:
pass
for
key
,
val
in
ret
.
items
():
if
key
.
endswith
(
'_url'
)
or
key
.
endswith
(
'_uri'
):
ret
[
key
]
=
self
.
make_absolute
(
val
)
return
ret
def
_qs
(
data
,
fields
=
()):
"""
Querysets.
"""
return
[
_any
(
v
,
fields
)
for
v
in
data
]
def
_list
(
data
):
"""
Lists.
"""
return
[
_any
(
v
)
for
v
in
data
]
def
_dict
(
data
):
"""
Dictionaries.
"""
return
dict
([
(
k
,
_any
(
v
))
for
k
,
v
in
data
.
iteritems
()
])
# Kickstart the seralizin'.
return
_any
(
data
,
self
.
fields
)
def
create
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
all_kw_args
=
dict
(
data
.
items
()
+
kwargs
.
items
())
instance
=
self
.
model
(
**
all_kw_args
)
instance
.
save
()
headers
=
{}
if
hasattr
(
instance
,
'get_absolute_url'
):
headers
[
'Location'
]
=
self
.
make_absolute
(
instance
.
get_absolute_url
())
return
(
201
,
instance
,
headers
)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
try
:
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
except
self
.
model
.
DoesNotExist
:
return
(
404
,
None
,
{})
return
(
200
,
instance
,
{})
def
update
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
try
:
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
for
(
key
,
val
)
in
data
.
items
():
setattr
(
instance
,
key
,
val
)
except
self
.
model
.
DoesNotExist
:
instance
=
self
.
model
(
**
data
)
instance
.
save
()
instance
.
save
()
return
(
200
,
instance
,
{})
def
delete
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
instance
.
delete
()
return
(
204
,
None
,
{})
class
QueryModelResource
(
ModelResource
):
allowed_methods
=
(
'read'
,)
def
get_bound_form
(
self
,
data
=
None
,
is_response
=
False
):
return
None
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
query
=
self
.
model
.
objects
.
all
()
return
(
200
,
query
,
{})
src/rest/templates/emitter.html
View file @
b0ce3f92
{% load urlize_quoted_links %}
<?xml version="1.0" encoding="UTF-8"?>
{% load urlize_quoted_links %}
{% load add_query_param %}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns=
"http://www.w3.org/1999/xhtml"
>
...
...
@@ -6,12 +6,19 @@
<style>
pre
{
border
:
1px
solid
black
;
padding
:
1em
;
background
:
#ffd
}
div
.action
{
padding
:
0.5em
1em
;
margin-bottom
:
0.5em
;
background
:
#ddf
}
ul
.accepttypes
{
float
:
right
;
list-style-type
:
none
;
margin
:
0
;
padding
:
0
}
ul
.accepttypes
li
{
display
:
inline
;}
form
div
{
margin
:
0.5em
0
}
form
div
*
{
vertical-align
:
top
}
form
ul
.errorlist
{
display
:
inline
;
margin
:
0
;
padding
:
0
}
form
ul
.errorlist
li
{
display
:
inline
;
color
:
red
;}
.clearing
{
display
:
block
;
margin
:
0
;
padding
:
0
;
clear
:
both
;}
</style>
<title>
API - {{ resource.name }}
</title>
</head>
<body>
<h1>
{{ resource.name }}
</h1>
<p>
{{ resource.description }}
</p>
<p>
{{ resource.description
|linebreaksbr
}}
</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 %}
...
...
@@ -20,14 +27,32 @@
{% if 'read' in resource.allowed_operations %}
<div
class=
'action'
>
<a
href=
'{{ resource.request.path }}'
>
Read
</a>
<ul
class=
"accepttypes"
>
{% for content_type in resource.available_content_types %}
{% with resource.ACCEPT_PARAM|add:"="|add:content_type as param %}
<li>
[
<a
href=
'{{ resource.request.path|add_query_param:param }}'
>
{{ content_type }}
</a>
]
</li>
{% endwith %}
{% endfor %}
</ul>
<div
class=
"clearing"
></div>
</div>
{% endif %}
{% if 'create' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ resource.request.path }}"
method=
"
POST
"
>
<form
action=
"{{ resource.request.path }}"
method=
"
post
"
>
{% csrf_token %}
{{ resource.form_instance.as_p }}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div
class=
"clearing"
></div>
<input
type=
"submit"
value=
"Create"
/>
</form>
</div>
...
...
@@ -35,10 +60,20 @@
{% if 'update' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ resource.request.path }}"
method=
"
POST
"
>
<form
action=
"{{ resource.request.path }}"
method=
"
post
"
>
<input
type=
"hidden"
name=
"{{ resource.METHOD_PARAM}}"
value=
"PUT"
/>
{% csrf_token %}
{{ resource.form_instance.as_p }}
{% with resource.form_instance as form %}
{% for field in form %}
<div>
{{ field.label_tag }}:
{{ field }}
{{ field.help_text }}
{{ field.errors }}
</div>
{% endfor %}
{% endwith %}
<div
class=
"clearing"
></div>
<input
type=
"submit"
value=
"Update"
/>
</form>
</div>
...
...
@@ -46,7 +81,7 @@
{% if 'delete' in resource.allowed_operations %}
<div
class=
'action'
>
<form
action=
"{{ resource.request.path }}"
method=
"
POST
"
>
<form
action=
"{{ resource.request.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 @
b0ce3f92
{{ resource.name }}
{{ resource.description }}
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
...
...
src/rest/templatetags/add_query_param.py
0 → 100644
View file @
b0ce3f92
from
django.template
import
Library
from
urlparse
import
urlparse
,
urlunparse
from
urllib
import
quote
register
=
Library
()
def
add_query_param
(
url
,
param
):
(
key
,
val
)
=
param
.
split
(
'='
)
param
=
'
%
s=
%
s'
%
(
key
,
quote
(
val
))
(
scheme
,
netloc
,
path
,
params
,
query
,
fragment
)
=
urlparse
(
url
)
if
query
:
query
+=
"&"
+
param
else
:
query
=
param
return
urlunparse
((
scheme
,
netloc
,
path
,
params
,
query
,
fragment
))
register
.
filter
(
'add_query_param'
,
add_query_param
)
src/rest/utils.py
View file @
b0ce3f92
...
...
@@ -140,9 +140,9 @@ class XMLEmitter():
def
_to_xml
(
self
,
xml
,
data
):
if
isinstance
(
data
,
(
list
,
tuple
)):
for
item
in
data
:
xml
.
startElement
(
"
resource
"
,
{})
xml
.
startElement
(
"
list-item
"
,
{})
self
.
_to_xml
(
xml
,
item
)
xml
.
endElement
(
"
resource
"
)
xml
.
endElement
(
"
list-item
"
)
elif
isinstance
(
data
,
dict
):
for
key
,
value
in
data
.
iteritems
():
...
...
@@ -158,11 +158,11 @@ class XMLEmitter():
xml
=
SimplerXMLGenerator
(
stream
,
"utf-8"
)
xml
.
startDocument
()
xml
.
startElement
(
"
conten
t"
,
{})
xml
.
startElement
(
"
roo
t"
,
{})
self
.
_to_xml
(
xml
,
data
)
xml
.
endElement
(
"
conten
t"
)
xml
.
endElement
(
"
roo
t"
)
xml
.
endDocument
()
return
stream
.
getvalue
()
...
...
src/testapp/models.py
View file @
b0ce3f92
...
...
@@ -40,8 +40,8 @@ RATING_CHOICES = ((0, 'Awful'),
class
BlogPost
(
models
.
Model
):
key
=
models
.
CharField
(
primary_key
=
True
,
max_length
=
64
,
default
=
uuid_str
,
editable
=
False
)
title
=
models
.
CharField
(
max_length
=
128
,
help_text
=
'The article title (Required)'
)
content
=
models
.
TextField
(
help_text
=
'The article body (Required)'
)
title
=
models
.
CharField
(
max_length
=
128
)
content
=
models
.
TextField
()
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
slug
=
models
.
SlugField
(
editable
=
False
,
default
=
''
)
...
...
@@ -74,11 +74,14 @@ class BlogPost(models.Model):
class
Comment
(
models
.
Model
):
blogpost
=
models
.
ForeignKey
(
BlogPost
,
editable
=
False
,
related_name
=
'comments'
)
username
=
models
.
CharField
(
max_length
=
128
,
help_text
=
'Please enter a username (Required)'
)
comment
=
models
.
TextField
(
help_text
=
'Enter your comment here (Required)'
)
rating
=
models
.
IntegerField
(
blank
=
True
,
null
=
True
,
choices
=
RATING_CHOICES
,
help_text
=
'
Please rate the blog post (Optional)
'
)
username
=
models
.
CharField
(
max_length
=
128
)
comment
=
models
.
TextField
()
rating
=
models
.
IntegerField
(
blank
=
True
,
null
=
True
,
choices
=
RATING_CHOICES
,
help_text
=
'
How did you rate this post?
'
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
Meta
:
ordering
=
(
'created'
,)
@models.permalink
def
get_absolute_url
(
self
):
return
(
'testapp.views.CommentInstance'
,
(
self
.
blogpost
.
key
,
self
.
id
))
...
...
@@ -86,5 +89,6 @@ class Comment(models.Model):
@property
@models.permalink
def
blogpost_url
(
self
):
"""Link to the blog post resource which this comment corresponds to."""
return
(
'testapp.views.BlogPostInstance'
,
(
self
.
blogpost
.
key
,))
src/testapp/views.py
View file @
b0ce3f92
from
rest.resource
import
Resource
,
ModelResource
,
QueryModelResource
from
rest.resource
import
Resource
from
rest.modelresource
import
ModelResource
,
QueryModelResource
from
testapp.models
import
BlogPost
,
Comment
##### Root Resource #####
...
...
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