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
f144b769
Commit
f144b769
authored
Jan 04, 2011
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Lots of good form validation and default actions
parent
48c7171a
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
616 additions
and
122 deletions
+616
-122
src/rest/emitters.py
+6
-6
src/rest/parsers.py
+39
-1
src/rest/resource.py
+437
-79
src/rest/templates/emitter.html
+5
-5
src/rest/templatetags/__init__.pyc
+0
-0
src/rest/templatetags/urlize_quoted_links.pyc
+0
-0
src/testapp/models.py
+30
-1
src/testapp/tests.py
+65
-24
src/testapp/urls.py
+3
-0
src/testapp/views.py
+31
-6
No files found.
src/rest/emitters.py
View file @
f144b769
...
@@ -4,12 +4,13 @@ import json
...
@@ -4,12 +4,13 @@ import json
from
utils
import
dict2xml
from
utils
import
dict2xml
class
BaseEmitter
(
object
):
class
BaseEmitter
(
object
):
def
__init__
(
self
,
resource
,
request
,
status
,
headers
):
def
__init__
(
self
,
resource
,
request
,
status
,
headers
,
form
):
self
.
request
=
request
self
.
request
=
request
self
.
resource
=
resource
self
.
resource
=
resource
self
.
status
=
status
self
.
status
=
status
self
.
headers
=
headers
self
.
headers
=
headers
self
.
form
=
form
def
emit
(
self
,
output
):
def
emit
(
self
,
output
):
return
output
return
output
...
@@ -26,14 +27,13 @@ class TemplatedEmitter(BaseEmitter):
...
@@ -26,14 +27,13 @@ class TemplatedEmitter(BaseEmitter):
'headers'
:
self
.
headers
,
'headers'
:
self
.
headers
,
'resource_name'
:
self
.
resource
.
__class__
.
__name__
,
'resource_name'
:
self
.
resource
.
__class__
.
__name__
,
'resource_doc'
:
self
.
resource
.
__doc__
,
'resource_doc'
:
self
.
resource
.
__doc__
,
'create_form'
:
self
.
resource
.
create_form
and
self
.
resource
.
create_form
()
or
None
,
'create_form'
:
self
.
form
,
'update_form'
:
self
.
resource
.
update_form
and
self
.
resource
.
update_form
()
or
None
,
'update_form'
:
self
.
form
,
'allowed_methods'
:
self
.
resource
.
allowed_methods
,
'request'
:
self
.
request
,
'request'
:
self
.
request
,
'resource'
:
self
.
resource
,
'resource'
:
self
.
resource
,
})
})
return
template
.
render
(
context
)
return
template
.
render
(
context
)
class
JSONEmitter
(
BaseEmitter
):
class
JSONEmitter
(
BaseEmitter
):
def
emit
(
self
,
output
):
def
emit
(
self
,
output
):
return
json
.
dumps
(
output
)
return
json
.
dumps
(
output
)
...
...
src/rest/parsers.py
View file @
f144b769
...
@@ -17,6 +17,44 @@ class XMLParser(BaseParser):
...
@@ -17,6 +17,44 @@ class XMLParser(BaseParser):
pass
pass
class
FormParser
(
BaseParser
):
class
FormParser
(
BaseParser
):
"""The default parser for form data.
Return a dict containing a single value for each non-reserved parameter
"""
def
__init__
(
self
,
resource
,
request
):
if
request
.
method
==
'PUT'
:
# Fix from piston to force Django to give PUT requests the same
# form processing that POST requests get...
#
# Bug fix: if _load_post_and_files has already been called, for
# example by middleware accessing request.POST, the below code to
# pretend the request is a POST instead of a PUT will be too late
# to make a difference. Also calling _load_post_and_files will result
# in the following exception:
# AttributeError: You cannot set the upload handlers after the upload has been processed.
# The fix is to check for the presence of the _post field which is set
# the first time _load_post_and_files is called (both by wsgi.py and
# modpython.py). If it's set, the request has to be 'reset' to redo
# the query value parsing in POST mode.
if
hasattr
(
request
,
'_post'
):
del
request
.
_post
del
request
.
_files
try
:
request
.
method
=
"POST"
request
.
_load_post_and_files
()
request
.
method
=
"PUT"
except
AttributeError
:
request
.
META
[
'REQUEST_METHOD'
]
=
'POST'
request
.
_load_post_and_files
()
request
.
META
[
'REQUEST_METHOD'
]
=
'PUT'
#
self
.
data
=
{}
for
(
key
,
val
)
in
request
.
POST
.
items
():
if
key
not
in
resource
.
RESERVED_PARAMS
:
self
.
data
[
key
]
=
val
def
parse
(
self
,
input
):
def
parse
(
self
,
input
):
return
self
.
request
.
POST
return
self
.
data
src/rest/resource.py
View file @
f144b769
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
rest
import
emitters
,
parsers
,
utils
from
rest
import
emitters
,
parsers
from
decimal
import
Decimal
from
decimal
import
Decimal
#
#
...
@@ -20,44 +20,103 @@ class ResourceException(Exception):
...
@@ -20,44 +20,103 @@ class ResourceException(Exception):
class
Resource
(
object
):
class
Resource
(
object
):
# List of RESTful operations which may be performed on this resource.
allowed_operations
=
(
'read'
,)
allowed_methods
=
(
'GET'
,)
# List of content-types the resource can respond with, ordered by preference
emitters
=
(
(
'application/json'
,
emitters
.
JSONEmitter
),
callmap
=
{
'GET'
:
'read'
,
'POST'
:
'create'
,
'PUT'
:
'update'
,
'DELETE'
:
'delete'
}
emitters
=
[
(
'application/json'
,
emitters
.
JSONEmitter
),
(
'text/html'
,
emitters
.
HTMLEmitter
),
(
'text/html'
,
emitters
.
HTMLEmitter
),
(
'application/xhtml+xml'
,
emitters
.
HTMLEmitter
),
(
'application/xhtml+xml'
,
emitters
.
HTMLEmitter
),
(
'text/plain'
,
emitters
.
TextEmitter
),
(
'text/plain'
,
emitters
.
TextEmitter
),
(
'application/xml'
,
emitters
.
XMLEmitter
),
]
(
'application/xml'
,
emitters
.
XMLEmitter
),
)
# List of content-types the resource can read from
parsers
=
{
'application/json'
:
parsers
.
JSONParser
,
parsers
=
{
'application/json'
:
parsers
.
JSONParser
,
'application/xml'
:
parsers
.
XMLParser
,
'application/xml'
:
parsers
.
XMLParser
,
'application/x-www-form-urlencoded'
:
parsers
.
FormParser
,
'application/x-www-form-urlencoded'
:
parsers
.
FormParser
,
'multipart/form-data'
:
parsers
.
FormParser
}
'multipart/form-data'
:
parsers
.
FormParser
}
create_form
=
None
# Optional form for input validation and presentation of HTML formatted responses.
update_form
=
None
form
=
None
# 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'
METHOD_PARAM
=
'_method'
ACCEPT_PARAM
=
'_accept'
ACCEPT_PARAM
=
'_accept'
CSRF_PARAM
=
'csrfmiddlewaretoken'
RESERVED_PARAMS
=
set
((
METHOD_PARAM
,
ACCEPT_PARAM
,
CSRF_PARAM
))
USE_SITEMAP_FOR_ABSOLUTE_URLS
=
False
def
__new__
(
cls
,
request
,
*
args
,
**
kwargs
):
def
__new__
(
cls
,
request
,
*
args
,
**
kwargs
):
"""Make the class callable so it can be used as a Django view."""
self
=
object
.
__new__
(
cls
)
self
=
object
.
__new__
(
cls
)
self
.
__init__
()
self
.
__init__
()
self
.
_request
=
request
self
.
_request
=
request
return
self
.
_handle_request
(
request
,
*
args
,
**
kwargs
)
try
:
return
self
.
_handle_request
(
request
,
*
args
,
**
kwargs
)
except
:
import
traceback
traceback
.
print_exc
()
raise
def
__init__
(
self
):
def
__init__
(
self
):
pass
pass
def
_determine_method
(
self
,
request
):
def
reverse
(
self
,
view
,
*
args
,
**
kwargs
):
"""Determine the HTTP method that this request should be treated as,
"""Return a fully qualified URI for a given view or resource, using the current request as the base URI.
allowing for PUT and DELETE tunneling via the _method parameter."""
TODO: Add SITEMAP option.
Provided for convienience."""
return
self
.
_request
.
build_absolute_uri
(
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.
Provided for convienience."""
return
self
.
_request
.
build_absolute_uri
(
uri
)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
"""RESTful read on the resource, which must be subclassed to be implemented. Should be a safe operation."""
self
.
not_implemented
(
'read'
)
def
create
(
self
,
data
=
None
,
headers
=
{},
*
args
,
**
kwargs
):
"""RESTful create on the resource, which must be subclassed to be implemented."""
self
.
not_implemented
(
'create'
)
def
update
(
self
,
data
=
None
,
headers
=
{},
*
args
,
**
kwargs
):
"""RESTful update on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
self
.
not_implemented
(
'update'
)
def
delete
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
"""RESTful delete on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
self
.
not_implemented
(
'delete'
)
def
not_implemented
(
self
,
operation
):
"""Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_operations, but which has not been implemented."""
raise
ResourceException
(
STATUS_500_INTERNAL_SERVER_ERROR
,
{
'detail'
:
'
%
s operation on this resource has not been implemented'
%
(
operation
,
)})
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
method
=
request
.
method
if
method
==
'POST'
and
request
.
POST
.
has_key
(
self
.
METHOD_PARAM
):
if
method
==
'POST'
and
request
.
POST
.
has_key
(
self
.
METHOD_PARAM
):
...
@@ -66,17 +125,47 @@ class Resource(object):
...
@@ -66,17 +125,47 @@ class Resource(object):
return
method
return
method
def
_check_method_allowed
(
self
,
method
):
def
check_method_allowed
(
self
,
method
):
if
not
method
in
self
.
allowed_methods
:
"""Ensure the request method is acceptable fot this resource."""
if
not
method
in
self
.
CALLMAP
.
keys
():
raise
ResourceException
(
STATUS_501_NOT_IMPLEMENTED
,
{
'detail'
:
'Unknown or unsupported method
\'
%
s
\'
'
%
method
})
if
not
self
.
CALLMAP
[
method
]
in
self
.
allowed_operations
:
raise
ResourceException
(
STATUS_405_METHOD_NOT_ALLOWED
,
raise
ResourceException
(
STATUS_405_METHOD_NOT_ALLOWED
,
{
'detail'
:
'Method
\'
%
s
\'
not allowed on this resource.'
%
method
})
{
'detail'
:
'Method
\'
%
s
\'
not allowed on this resource.'
%
method
})
def
determine_form
(
self
,
data
=
None
):
"""Optionally return a Django Form instance, which may be used for validation
and/or rendered by an HTML/XHTML emitter.
if
not
method
in
self
.
callmap
.
keys
():
The data argument will be non Null if the form is required to be bound to some deserialized
raise
ResourceException
(
STATUS_501_NOT_IMPLEMENTED
,
input data, or Null if the form is required to be unbound.
{
'detail'
:
'Unknown or unsupported method
\'
%
s
\'
'
%
method
})
"""
if
self
.
form
:
return
self
.
form
(
data
)
return
None
def
cleanup_request
(
self
,
data
,
form
=
None
):
"""Perform any resource-specific data deserialization and/or validation
after the initial HTTP content-type deserialization has taken place.
Optionally this may use a Django Form which will have been bound to the data,
rather than using the data directly.
"""
return
data
def
cleanup_response
(
self
,
data
):
"""Perform any resource-specific data filtering prior to the standard HTTP
content-type serialization."""
return
data
def
_
determine_parser
(
self
,
request
):
def
determine_parser
(
self
,
request
):
"""Return the appropriate parser for the input, given the client's 'Content-Type' header,
"""Return the appropriate parser for the input, given the client's 'Content-Type' header,
and the content types that this Resource knows how to parse."""
and the content types that this Resource knows how to parse."""
content_type
=
request
.
META
.
get
(
'CONTENT_TYPE'
,
'application/x-www-form-urlencoded'
)
content_type
=
request
.
META
.
get
(
'CONTENT_TYPE'
,
'application/x-www-form-urlencoded'
)
...
@@ -90,14 +179,13 @@ class Resource(object):
...
@@ -90,14 +179,13 @@ class Resource(object):
except
KeyError
:
except
KeyError
:
raise
ResourceException
(
STATUS_415_UNSUPPORTED_MEDIA_TYPE
,
raise
ResourceException
(
STATUS_415_UNSUPPORTED_MEDIA_TYPE
,
{
'detail'
:
'Unsupported content type
\'
%
s
\'
'
%
content_type
})
{
'detail'
:
'Unsupported content type
\'
%
s
\'
'
%
content_type
})
def
_determine_emitter
(
self
,
request
):
def
determine_emitter
(
self
,
request
):
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
and the content types that this Resource knows how to serve.
and the content types that this Resource knows how to serve.
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
"""
default
=
self
.
emitters
[
0
]
default
=
self
.
emitters
[
0
]
if
not
request
.
META
.
has_key
(
'HTTP_ACCEPT'
):
if
not
request
.
META
.
has_key
(
'HTTP_ACCEPT'
):
...
@@ -141,61 +229,61 @@ class Resource(object):
...
@@ -141,61 +229,61 @@ class Resource(object):
{
'detail'
:
'Could not statisfy the client
\'
s accepted content type'
,
{
'detail'
:
'Could not statisfy the client
\'
s accepted content type'
,
'accepted_types'
:
[
item
[
0
]
for
item
in
self
.
emitters
]})
'accepted_types'
:
[
item
[
0
]
for
item
in
self
.
emitters
]})
def
_validate_data
(
self
,
method
,
data
):
"""If there is an appropriate form to deal with this operation,
then validate the data and return the resulting dictionary.
"""
if
method
==
'PUT'
and
self
.
update_form
:
form
=
self
.
update_form
(
data
)
elif
method
==
'POST'
and
self
.
create_form
:
form
=
self
.
create_form
(
data
)
else
:
return
data
if
not
form
.
is_valid
():
raise
ResourceException
(
STATUS_400_BAD_REQUEST
,
{
'detail'
:
dict
((
k
,
map
(
unicode
,
v
))
for
(
k
,
v
)
in
form
.
errors
.
iteritems
())})
return
form
.
cleaned_data
def
_handle_request
(
self
,
request
,
*
args
,
**
kwargs
):
def
_handle_request
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Broadly this consists of the following procedure:
0. ensure the operation is permitted
1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
2. cleanup and validate request data (PUT/POST only)
3. call the core method to get the response data
4. cleanup the response data
5. serialize response data into response content, using standard HTTP content negotiation
"""
method
=
self
.
determine_method
(
request
)
emitter
=
None
form
=
None
try
:
# Before we attempt anything else determine what format to emit our response data with.
mimetype
,
emitter
=
self
.
determine_emitter
(
request
)
# Hack to ensure PUT requests get the same form treatment as POST requests
# Ensure the requested operation is permitted on this resource
utils
.
coerce_put_post
(
request
)
self
.
check_method_allowed
(
method
)
# Get the request method, allowing for PUT and DELETE tunneling
method
=
self
.
_determine_method
(
request
)
try
:
# Get the appropriate create/read/update/delete function
self
.
_check_method_allowed
(
method
)
func
=
getattr
(
self
,
self
.
CALLMAP
.
get
(
method
,
''
))
# Parse the HTTP Request content
func
=
getattr
(
self
,
self
.
callmap
.
get
(
method
,
''
))
# Either generate the response data, deserializing and validating any request data
if
method
in
(
'PUT'
,
'POST'
):
if
method
in
(
'PUT'
,
'POST'
):
parser
=
self
.
_
determine_parser
(
request
)
parser
=
self
.
determine_parser
(
request
)
data
=
parser
(
self
,
request
)
.
parse
(
request
.
raw_post_data
)
data
=
parser
(
self
,
request
)
.
parse
(
request
.
raw_post_data
)
data
=
self
.
_validate_data
(
method
,
data
)
form
=
self
.
determine_form
(
data
)
data
=
self
.
cleanup_request
(
data
,
form
)
(
status
,
ret
,
headers
)
=
func
(
data
,
request
.
META
,
*
args
,
**
kwargs
)
(
status
,
ret
,
headers
)
=
func
(
data
,
request
.
META
,
*
args
,
**
kwargs
)
else
:
else
:
(
status
,
ret
,
headers
)
=
func
(
request
.
META
,
*
args
,
**
kwargs
)
(
status
,
ret
,
headers
)
=
func
(
request
.
META
,
*
args
,
**
kwargs
)
except
ResourceException
,
exc
:
(
status
,
ret
,
headers
)
=
(
exc
.
status
,
exc
.
content
,
exc
.
headers
)
headers
[
'Allow'
]
=
', '
.
join
(
self
.
allowed_methods
)
# Serialize the HTTP Response content
try
:
mimetype
,
emitter
=
self
.
_determine_emitter
(
request
)
except
ResourceException
,
exc
:
except
ResourceException
,
exc
:
(
status
,
ret
,
headers
)
=
(
exc
.
status
,
exc
.
content
,
exc
.
headers
)
(
status
,
ret
,
headers
)
=
(
exc
.
status
,
exc
.
content
,
exc
.
headers
)
# Use a default emitter if request failed without being able to determine an acceptable emitter
if
emitter
is
None
:
mimetype
,
emitter
=
self
.
emitters
[
0
]
mimetype
,
emitter
=
self
.
emitters
[
0
]
# Use a form unbound to any data if one has not yet been created
if
form
is
None
:
form
=
self
.
determine_form
()
# Always add the allow header
headers
[
'Allow'
]
=
', '
.
join
([
self
.
REVERSE_CALLMAP
[
operation
]
for
operation
in
self
.
allowed_operations
])
content
=
emitter
(
self
,
request
,
status
,
headers
)
.
emit
(
ret
)
# Serialize the response content
ret
=
self
.
cleanup_response
(
ret
)
content
=
emitter
(
self
,
request
,
status
,
headers
,
form
)
.
emit
(
ret
)
# Build the HTTP Response
# Build the HTTP Response
resp
=
HttpResponse
(
content
,
mimetype
=
mimetype
,
status
=
status
)
resp
=
HttpResponse
(
content
,
mimetype
=
mimetype
,
status
=
status
)
...
@@ -204,24 +292,293 @@ class Resource(object):
...
@@ -204,24 +292,293 @@ class Resource(object):
return
resp
return
resp
def
_not_implemented
(
self
,
operation
):
resource_name
=
self
.
__class__
.
__name__
raise
ResourceException
(
STATUS_500_INTERNAL_SERVER_ERROR
,
{
'detail'
:
'
%
s operation on this resource has not been implemented'
%
(
operation
,
)})
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
self
.
_not_implemented
(
'read'
)
def
create
(
self
,
data
=
None
,
headers
=
{},
*
args
,
**
kwargs
):
self
.
_not_implemented
(
'create'
)
from
django.forms
import
ModelForm
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
):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if
self
.
form
:
return
self
.
form
elif
self
.
model
:
class
NewModelForm
(
ModelForm
):
class
Meta
:
model
=
self
.
model
fields
=
self
.
form_fields
if
self
.
form_fields
else
self
.
fields
if
data
is
None
:
return
NewModelForm
()
else
:
return
NewModelForm
(
data
)
else
:
return
None
def
update
(
self
,
data
=
None
,
headers
=
{},
*
args
,
**
kwargs
):
def
cleanup_request
(
self
,
data
,
form
=
None
):
self
.
_not_implemented
(
'update'
)
"""Filter data into form-cleaned data, performing validation and type coercion."""
if
form
is
None
:
return
data
def
delete
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
if
not
form
.
is_valid
():
self
.
_not_implemented
(
'delete'
)
details
=
dict
((
key
,
map
(
unicode
,
val
))
for
(
key
,
val
)
in
form
.
errors
.
iteritems
())
raise
ResourceException
(
STATUS_400_BAD_REQUEST
,
{
'detail'
:
details
})
def
reverse
(
self
,
view
,
*
args
,
**
kwargs
):
return
form
.
cleaned_data
"""Return a fully qualified URI for a view, using the current request as the base URI.
def
cleanup_response
(
self
,
data
):
"""
"""
return
self
.
_request
.
build_absolute_uri
(
reverse
(
view
,
*
args
,
**
kwargs
))
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
,
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_uri
=
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
)
else
:
get_fields
=
set
(
fields
)
if
'absolute_uri'
in
get_fields
:
# MOVED (TRC)
get_absolute_uri
=
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
:
for
f
in
data
.
_meta
.
fields
:
ret
[
f
.
attname
]
=
_any
(
getattr
(
data
,
f
.
attname
))
fields
=
dir
(
data
.
__class__
)
+
ret
.
keys
()
add_ons
=
[
k
for
k
in
dir
(
data
)
if
k
not
in
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_uri
:
try
:
ret
[
'absolute_uri'
]
=
self
.
make_absolute
(
data
.
get_absolute_url
())
except
:
pass
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
=
{}):
instance
=
self
.
model
(
**
data
)
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
):
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
return
(
200
,
instance
,
{})
def
update
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
for
(
key
,
val
)
in
data
.
items
():
setattr
(
instance
,
key
,
val
)
instance
.
save
()
return
(
200
,
instance
,
{})
def
delete
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
instance
=
self
.
model
.
objects
.
get
(
**
kwargs
)
instance
.
delete
()
return
(
204
,
''
,
{})
\ No newline at end of file
src/rest/templates/emitter.html
View file @
f144b769
...
@@ -12,17 +12,17 @@
...
@@ -12,17 +12,17 @@
<h1>
{{ resource_name }}
</h1>
<h1>
{{ resource_name }}
</h1>
<p>
{{ resource_doc }}
</p>
<p>
{{ resource_doc }}
</p>
<pre>
{% autoescape off %}
<b>
{{ status }} {{ reason }}
</b>
<pre>
{% autoescape off %}
<b>
{{ status }} {{ reason }}
</b>
{% for key, val in headers.items %}
<b>
{{ key }}:
</b>
{{ val }}
{% for key, val in headers.items %}
<b>
{{ key }}:
</b>
{{ val
|urlize_quoted_links
}}
{% endfor %}
{% endfor %}
{{ content|urlize_quoted_links }}{% endautoescape %}
</pre>
{{ content|urlize_quoted_links }}{% endautoescape %}
</pre>
{% if '
GET' in allowed_method
s %}
{% if '
read' in resource.allowed_operation
s %}
<div
class=
'action'
>
<div
class=
'action'
>
<a
href=
'{{ request.path }}'
>
Read
</a>
<a
href=
'{{ request.path }}'
>
Read
</a>
</div>
</div>
{% endif %}
{% endif %}
{% if '
POST' in resource.allowed_method
s %}
{% if '
create' in resource.allowed_operation
s %}
<div
class=
'action'
>
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
{% csrf_token %}
{% csrf_token %}
...
@@ -32,7 +32,7 @@
...
@@ -32,7 +32,7 @@
</div>
</div>
{% endif %}
{% endif %}
{% if '
PUT' in resource.allowed_method
s %}
{% if '
update' in resource.allowed_operation
s %}
<div
class=
'action'
>
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<input
type=
"hidden"
name=
"{{ resource.METHOD_PARAM}}"
value=
"PUT"
/>
<input
type=
"hidden"
name=
"{{ resource.METHOD_PARAM}}"
value=
"PUT"
/>
...
@@ -43,7 +43,7 @@
...
@@ -43,7 +43,7 @@
</div>
</div>
{% endif %}
{% endif %}
{% if '
DELETE' in resource.allowed_method
s %}
{% if '
delete' in resource.allowed_operation
s %}
<div
class=
'action'
>
<div
class=
'action'
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
<form
action=
"{{ request.path }}"
method=
"POST"
>
{% csrf_token %}
{% csrf_token %}
...
...
src/rest/templatetags/__init__.pyc
View file @
f144b769
No preview for this file type
src/rest/templatetags/urlize_quoted_links.pyc
View file @
f144b769
No preview for this file type
src/testapp/models.py
View file @
f144b769
from
django.db
import
models
from
django.db
import
models
import
uuid
# Create your models here.
def
uuid_str
():
return
str
(
uuid
.
uuid1
())
class
ExampleModel
(
models
.
Model
):
num
=
models
.
IntegerField
(
default
=
2
,
choices
=
((
1
,
'one'
),
(
2
,
'two'
)))
hidden_num
=
models
.
IntegerField
(
verbose_name
=
'Something'
,
help_text
=
'HELP'
)
text
=
models
.
TextField
(
blank
=
False
)
another
=
models
.
CharField
(
max_length
=
10
)
class
ExampleContainer
(
models
.
Model
):
"""Container. Has a key, a name, and some internal data, and contains a set of items."""
key
=
models
.
CharField
(
primary_key
=
True
,
default
=
uuid_str
,
max_length
=
36
,
editable
=
False
)
name
=
models
.
CharField
(
max_length
=
256
)
internal
=
models
.
IntegerField
(
default
=
0
)
@models.permalink
def
get_absolute_url
(
self
):
return
(
'testapp.views.ContainerInstance'
,
[
self
.
key
])
class
ExampleItem
(
models
.
Model
):
"""Item. Belongs to a container and has an index number and a note.
Items are uniquely identified by their container and index number."""
container
=
models
.
ForeignKey
(
ExampleContainer
,
related_name
=
'items'
)
index
=
models
.
IntegerField
()
note
=
models
.
CharField
(
max_length
=
1024
)
unique_together
=
(
container
,
index
)
\ No newline at end of file
src/testapp/tests.py
View file @
f144b769
...
@@ -9,7 +9,8 @@ from django.test import TestCase
...
@@ -9,7 +9,8 @@ from django.test import TestCase
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
testapp
import
views
from
testapp
import
views
import
json
import
json
from
rest.utils
import
xml2dict
,
dict2xml
#from rest.utils import xml2dict, dict2xml
class
AcceptHeaderTests
(
TestCase
):
class
AcceptHeaderTests
(
TestCase
):
def
assert_accept_mimetype
(
self
,
mimetype
,
expect
=
None
,
expect_match
=
True
):
def
assert_accept_mimetype
(
self
,
mimetype
,
expect
=
None
,
expect_match
=
True
):
...
@@ -45,6 +46,10 @@ class AcceptHeaderTests(TestCase):
...
@@ -45,6 +46,10 @@ class AcceptHeaderTests(TestCase):
def
test_invalid_accept_header_returns_406
(
self
):
def
test_invalid_accept_header_returns_406
(
self
):
resp
=
self
.
client
.
get
(
reverse
(
views
.
ReadOnlyResource
),
HTTP_ACCEPT
=
'invalid/invalid'
)
resp
=
self
.
client
.
get
(
reverse
(
views
.
ReadOnlyResource
),
HTTP_ACCEPT
=
'invalid/invalid'
)
self
.
assertEquals
(
resp
.
status_code
,
406
)
self
.
assertEquals
(
resp
.
status_code
,
406
)
def
test_prefer_specific
(
self
):
self
.
fail
(
"Test not implemented"
)
class
AllowedMethodsTests
(
TestCase
):
class
AllowedMethodsTests
(
TestCase
):
def
test_reading_read_only_allowed
(
self
):
def
test_reading_read_only_allowed
(
self
):
...
@@ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase):
...
@@ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase):
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
{})
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
{})
self
.
assertEquals
(
resp
.
status_code
,
200
)
self
.
assertEquals
(
resp
.
status_code
,
200
)
class
EncodeDecodeTests
(
TestCase
):
class
EncodeDecodeTests
(
TestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
super
(
self
.
__class__
,
self
)
.
setUp
()
super
(
self
.
__class__
,
self
)
.
setUp
()
...
@@ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase):
...
@@ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase):
def
test_encode_form_decode_json
(
self
):
def
test_encode_form_decode_json
(
self
):
content
=
self
.
input
content
=
self
.
input
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
HTTP_ACCEPT
=
'application/json'
)
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
)
output
=
json
.
loads
(
resp
.
content
)
output
=
json
.
loads
(
resp
.
content
)
self
.
assertEquals
(
self
.
input
,
output
)
self
.
assertEquals
(
self
.
input
,
output
)
def
test_encode_json_decode_json
(
self
):
def
test_encode_json_decode_json
(
self
):
content
=
json
.
dumps
(
self
.
input
)
content
=
json
.
dumps
(
self
.
input
)
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
'application/json'
,
HTTP_ACCEPT
=
'application/json'
)
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
'application/json'
)
output
=
json
.
loads
(
resp
.
content
)
output
=
json
.
loads
(
resp
.
content
)
self
.
assertEquals
(
self
.
input
,
output
)
self
.
assertEquals
(
self
.
input
,
output
)
def
test_encode_xml_decode_json
(
self
):
#def test_encode_xml_decode_json(self):
content
=
dict2xml
(
self
.
input
)
# content = dict2xml(self.input)
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
'application/json'
,
HTTP_ACCEPT
=
'application/json'
)
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
# output = json.loads(resp.content)
# self.assertEquals(self.input, output)
#def test_encode_form_decode_xml(self):
# content = self.input
# resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
# output = xml2dict(resp.content)
# self.assertEquals(self.input, output)
#def test_encode_json_decode_xml(self):
# content = json.dumps(self.input)
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
# output = xml2dict(resp.content)
# self.assertEquals(self.input, output)
#def test_encode_xml_decode_xml(self):
# content = dict2xml(self.input)
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
# output = xml2dict(resp.content)
# self.assertEquals(self.input, output)
class
ModelTests
(
TestCase
):
def
test_create_container
(
self
):
content
=
json
.
dumps
({
'name'
:
'example'
})
resp
=
self
.
client
.
post
(
reverse
(
views
.
ContainerFactory
),
content
,
'application/json'
)
output
=
json
.
loads
(
resp
.
content
)
output
=
json
.
loads
(
resp
.
content
)
self
.
assertEquals
(
self
.
input
,
output
)
self
.
assertEquals
(
resp
.
status_code
,
201
)
self
.
assertEquals
(
output
[
'name'
],
'example'
)
def
test_encode_form_decode_xml
(
self
):
self
.
assertEquals
(
set
(
output
.
keys
()),
set
((
'absolute_uri'
,
'name'
,
'key'
)))
content
=
self
.
input
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
HTTP_ACCEPT
=
'application/xml'
)
output
=
xml2dict
(
resp
.
content
)
self
.
assertEquals
(
self
.
input
,
output
)
def
test_encode_json_decode_xml
(
self
):
class
CreatedModelTests
(
TestCase
):
content
=
json
.
dumps
(
self
.
input
)
def
setUp
(
self
):
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
'application/json'
,
HTTP_ACCEPT
=
'application/xml'
)
content
=
json
.
dumps
({
'name'
:
'example'
}
)
output
=
xml2dict
(
resp
.
content
)
resp
=
self
.
client
.
post
(
reverse
(
views
.
ContainerFactory
),
content
,
'application/json'
,
HTTP_ACCEPT
=
'application/json'
)
self
.
assertEquals
(
self
.
input
,
outpu
t
)
self
.
container
=
json
.
loads
(
resp
.
conten
t
)
def
test_encode_xml_decode_xml
(
self
):
def
test_read_container
(
self
):
content
=
dict2xml
(
self
.
input
)
resp
=
self
.
client
.
get
(
self
.
container
[
"absolute_uri"
])
resp
=
self
.
client
.
put
(
reverse
(
views
.
WriteOnlyResource
),
content
,
'application/json'
,
HTTP_ACCEPT
=
'application/xml'
)
self
.
assertEquals
(
resp
.
status_code
,
200
)
output
=
xml2dict
(
resp
.
content
)
container
=
json
.
loads
(
resp
.
content
)
self
.
assertEquals
(
self
.
input
,
output
)
self
.
assertEquals
(
container
,
self
.
container
)
\ No newline at end of file
def
test_delete_container
(
self
):
resp
=
self
.
client
.
delete
(
self
.
container
[
"absolute_uri"
])
self
.
assertEquals
(
resp
.
status_code
,
204
)
self
.
assertEquals
(
resp
.
content
,
''
)
def
test_update_container
(
self
):
self
.
container
[
'name'
]
=
'new'
content
=
json
.
dumps
(
self
.
container
)
resp
=
self
.
client
.
put
(
self
.
container
[
"absolute_uri"
],
content
,
'application/json'
)
self
.
assertEquals
(
resp
.
status_code
,
200
)
container
=
json
.
loads
(
resp
.
content
)
self
.
assertEquals
(
container
,
self
.
container
)
\ No newline at end of file
src/testapp/urls.py
View file @
f144b769
...
@@ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views',
...
@@ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views',
(
r'^read-only$'
,
'ReadOnlyResource'
),
(
r'^read-only$'
,
'ReadOnlyResource'
),
(
r'^write-only$'
,
'WriteOnlyResource'
),
(
r'^write-only$'
,
'WriteOnlyResource'
),
(
r'^read-write$'
,
'ReadWriteResource'
),
(
r'^read-write$'
,
'ReadWriteResource'
),
(
r'^model$'
,
'ModelFormResource'
),
(
r'^container$'
,
'ContainerFactory'
),
(
r'^container/((?P<key>[^/]+))$'
,
'ContainerInstance'
),
)
)
src/testapp/views.py
View file @
f144b769
from
rest.resource
import
Resource
from
rest.resource
import
Resource
,
ModelResource
from
testapp.forms
import
ExampleForm
from
testapp.forms
import
ExampleForm
from
testapp.models
import
ExampleModel
,
ExampleContainer
class
RootResource
(
Resource
):
class
RootResource
(
Resource
):
"""This is my docstring
"""This is my docstring
"""
"""
allowed_
methods
=
(
'GET
'
,)
allowed_
operations
=
(
'read
'
,)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
return
(
200
,
{
'read-only-api'
:
self
.
reverse
(
ReadOnlyResource
),
return
(
200
,
{
'read-only-api'
:
self
.
reverse
(
ReadOnlyResource
),
'write-only-api'
:
self
.
reverse
(
WriteOnlyResource
),
'write-only-api'
:
self
.
reverse
(
WriteOnlyResource
),
'read-write-api'
:
self
.
reverse
(
ReadWriteResource
)},
{})
'read-write-api'
:
self
.
reverse
(
ReadWriteResource
),
'model-api'
:
self
.
reverse
(
ModelFormResource
),
'create-container'
:
self
.
reverse
(
ContainerFactory
)},
{})
class
ReadOnlyResource
(
Resource
):
class
ReadOnlyResource
(
Resource
):
"""This is my docstring
"""This is my docstring
"""
"""
allowed_
methods
=
(
'GET
'
,)
allowed_
operations
=
(
'read
'
,)
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
def
read
(
self
,
headers
=
{},
*
args
,
**
kwargs
):
return
(
200
,
{
'ExampleString'
:
'Example'
,
return
(
200
,
{
'ExampleString'
:
'Example'
,
...
@@ -26,13 +29,35 @@ class ReadOnlyResource(Resource):
...
@@ -26,13 +29,35 @@ class ReadOnlyResource(Resource):
class
WriteOnlyResource
(
Resource
):
class
WriteOnlyResource
(
Resource
):
"""This is my docstring
"""This is my docstring
"""
"""
allowed_
methods
=
(
'PUT
'
,)
allowed_
operations
=
(
'update
'
,)
def
update
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
def
update
(
self
,
data
,
headers
=
{},
*
args
,
**
kwargs
):
return
(
200
,
data
,
{})
return
(
200
,
data
,
{})
class
ReadWriteResource
(
Resource
):
class
ReadWriteResource
(
Resource
):
allowed_
methods
=
(
'GET'
,
'PUT'
,
'DELETE
'
)
allowed_
operations
=
(
'read'
,
'update'
,
'delete
'
)
create_form
=
ExampleForm
create_form
=
ExampleForm
update_form
=
ExampleForm
update_form
=
ExampleForm
class
ModelFormResource
(
ModelResource
):
allowed_operations
=
(
'read'
,
'update'
,
'delete'
)
model
=
ExampleModel
# Nice things: form validation is applied to any input type
# html forms for output
# output always serialized nicely
class
ContainerFactory
(
ModelResource
):
allowed_operations
=
(
'create'
,)
model
=
ExampleContainer
fields
=
(
'absolute_uri'
,
'name'
,
'key'
)
form_fields
=
(
'name'
,)
class
ContainerInstance
(
ModelResource
):
allowed_operations
=
(
'read'
,
'update'
,
'delete'
)
model
=
ExampleContainer
fields
=
(
'absolute_uri'
,
'name'
,
'key'
)
form_fields
=
(
'name'
,)
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