mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
Add parsers, form validation, etc...
This commit is contained in:
parent
a78f578475
commit
c56e48f52e
|
@ -1,9 +1,10 @@
|
||||||
from django.template import Context, loader
|
from django.template import RequestContext, loader
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class BaseEmitter(object):
|
class BaseEmitter(object):
|
||||||
def __init__(self, resource, status, headers):
|
def __init__(self, resource, request, status, headers):
|
||||||
|
self.request = request
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
@ -12,16 +13,23 @@ class BaseEmitter(object):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
class TemplatedEmitter(BaseEmitter):
|
class TemplatedEmitter(BaseEmitter):
|
||||||
|
template = None
|
||||||
|
|
||||||
def emit(self, output):
|
def emit(self, output):
|
||||||
content = json.dumps(output, indent=4)
|
content = json.dumps(output, indent=4)
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
context = Context({
|
context = RequestContext(self.request, {
|
||||||
'content': content,
|
'content': content,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'reason': STATUS_CODE_TEXT.get(self.status, ''),
|
'reason': STATUS_CODE_TEXT.get(self.status, ''),
|
||||||
'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,
|
||||||
|
'update_form': self.resource.update_form and self.resource.update_form() or None,
|
||||||
|
'allowed_methods': self.resource.allowed_methods,
|
||||||
|
'request': self.request,
|
||||||
|
'resource': self.resource,
|
||||||
})
|
})
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
|
||||||
class BaseParser(object):
|
class BaseParser(object):
|
||||||
def __init__(self, resource, request):
|
def __init__(self, resource, request):
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
|
@ -8,11 +10,13 @@ class BaseParser(object):
|
||||||
|
|
||||||
|
|
||||||
class JSONParser(BaseParser):
|
class JSONParser(BaseParser):
|
||||||
pass
|
def parse(self, input):
|
||||||
|
return json.loads(input)
|
||||||
|
|
||||||
class XMLParser(BaseParser):
|
class XMLParser(BaseParser):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class FormParser(BaseParser):
|
class FormParser(BaseParser):
|
||||||
pass
|
def parse(self, input):
|
||||||
|
return self.request.POST
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
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
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
from rest import emitters, parsers, utils
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
for (key, val) in STATUS_CODE_TEXT.items():
|
||||||
|
locals()["STATUS_%d_%s" % (key, val.replace(' ', '_'))] = key
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceException(Exception):
|
||||||
|
def __init__(self, status, content='', headers={}):
|
||||||
|
self.status = status
|
||||||
|
self.content = content
|
||||||
|
self.headers = headers
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
class Resource(object):
|
||||||
|
|
||||||
class HTTPException(Exception):
|
|
||||||
def __init__(self, status, content, headers):
|
|
||||||
self.status = status
|
|
||||||
self.content = content
|
|
||||||
self.headers = headers
|
|
||||||
|
|
||||||
allowed_methods = ('GET',)
|
allowed_methods = ('GET',)
|
||||||
|
|
||||||
callmap = { 'GET': 'read', 'POST': 'create',
|
callmap = { 'GET': 'read', 'POST': 'create',
|
||||||
|
@ -27,6 +32,12 @@ class Resource(object):
|
||||||
'application/xml': parsers.XMLParser,
|
'application/xml': parsers.XMLParser,
|
||||||
'application/x-www-form-urlencoded': parsers.FormParser }
|
'application/x-www-form-urlencoded': parsers.FormParser }
|
||||||
|
|
||||||
|
create_form = None
|
||||||
|
update_form = None
|
||||||
|
|
||||||
|
METHOD_PARAM = '_method'
|
||||||
|
ACCEPT_PARAM = '_accept'
|
||||||
|
|
||||||
|
|
||||||
def __new__(cls, request, *args, **kwargs):
|
def __new__(cls, request, *args, **kwargs):
|
||||||
self = object.__new__(cls)
|
self = object.__new__(cls)
|
||||||
|
@ -34,15 +45,40 @@ class Resource(object):
|
||||||
self._request = request
|
self._request = request
|
||||||
return self._handle_request(request, *args, **kwargs)
|
return self._handle_request(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _determine_method(self, request):
|
||||||
|
"""Determine the HTTP method that this request should be treated as,
|
||||||
|
allowing 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.POST[self.METHOD_PARAM].upper()
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _check_method_allowed(self, method):
|
||||||
|
if not method in self.allowed_methods:
|
||||||
|
raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED,
|
||||||
|
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||||
|
|
||||||
|
if not method in self.callmap.keys():
|
||||||
|
raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
|
||||||
|
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
return self.parsers.values()[0]
|
try:
|
||||||
|
return self.parsers[request.META['CONTENT_TYPE']]
|
||||||
# TODO: Raise 415 Unsupported media type
|
except:
|
||||||
|
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
{'detail': 'Unsupported media 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,
|
||||||
|
@ -90,16 +126,40 @@ class Resource(object):
|
||||||
(accept_mimetype == mimetype)):
|
(accept_mimetype == mimetype)):
|
||||||
return (mimetype, emitter)
|
return (mimetype, emitter)
|
||||||
|
|
||||||
raise self.HTTPException(406, {'status': 'Not Acceptable',
|
raise ResourceException(STATUS_406_NOT_ACCEPTABLE,
|
||||||
'accepts': ','.join(item[0] for item in self.emitters)}, {})
|
{'detail': 'Could not statisfy the client\'s accepted content type',
|
||||||
|
'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):
|
||||||
method = request.method
|
|
||||||
|
# Hack to ensure PUT requests get the same form treatment as POST requests
|
||||||
|
utils.coerce_put_post(request)
|
||||||
|
|
||||||
|
# Get the request method, allowing for PUT and DELETE tunneling
|
||||||
|
method = self._determine_method(request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not method in self.allowed_methods:
|
self._check_method_allowed(method)
|
||||||
raise self.HTTPException(405, {'status': 'Method Not Allowed'}, {})
|
|
||||||
|
|
||||||
# Parse the HTTP Request content
|
# Parse the HTTP Request content
|
||||||
func = getattr(self, self.callmap.get(method, ''))
|
func = getattr(self, self.callmap.get(method, ''))
|
||||||
|
@ -107,11 +167,12 @@ class Resource(object):
|
||||||
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)
|
||||||
(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 self.HTTPException, exc:
|
except ResourceException, exc:
|
||||||
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
||||||
|
|
||||||
headers['Allow'] = ', '.join(self.allowed_methods)
|
headers['Allow'] = ', '.join(self.allowed_methods)
|
||||||
|
@ -119,11 +180,11 @@ class Resource(object):
|
||||||
# Serialize the HTTP Response content
|
# Serialize the HTTP Response content
|
||||||
try:
|
try:
|
||||||
mimetype, emitter = self._determine_emitter(request)
|
mimetype, emitter = self._determine_emitter(request)
|
||||||
except self.HTTPException, exc:
|
except ResourceException, exc:
|
||||||
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
(status, ret, headers) = (exc.status, exc.content, exc.headers)
|
||||||
mimetype, emitter = self.emitters[0]
|
mimetype, emitter = self.emitters[0]
|
||||||
|
|
||||||
content = emitter(self, status, headers).emit(ret)
|
content = emitter(self, request, status, headers).emit(ret)
|
||||||
|
|
||||||
# Build the HTTP Response
|
# Build the HTTP Response
|
||||||
resp = HttpResponse(content, mimetype=mimetype, status=status)
|
resp = HttpResponse(content, mimetype=mimetype, status=status)
|
||||||
|
@ -134,20 +195,20 @@ class Resource(object):
|
||||||
|
|
||||||
def _not_implemented(self, operation):
|
def _not_implemented(self, operation):
|
||||||
resource_name = self.__class__.__name__
|
resource_name = self.__class__.__name__
|
||||||
return (500, {'status': 'Internal Server Error',
|
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
|
||||||
'detail': '%s %s operation is permitted but has not been implemented' % (resource_name, operation)}, {})
|
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||||
|
|
||||||
def read(self, headers={}, *args, **kwargs):
|
def read(self, headers={}, *args, **kwargs):
|
||||||
return self._not_implemented('read')
|
self._not_implemented('read')
|
||||||
|
|
||||||
def create(self, data=None, headers={}, *args, **kwargs):
|
def create(self, data=None, headers={}, *args, **kwargs):
|
||||||
return self._not_implemented('create')
|
self._not_implemented('create')
|
||||||
|
|
||||||
def update(self, data=None, headers={}, *args, **kwargs):
|
def update(self, data=None, headers={}, *args, **kwargs):
|
||||||
return self._not_implemented('update')
|
self._not_implemented('update')
|
||||||
|
|
||||||
def delete(self, headers={}, *args, **kwargs):
|
def delete(self, headers={}, *args, **kwargs):
|
||||||
return self._not_implemented('delete')
|
self._not_implemented('delete')
|
||||||
|
|
||||||
def reverse(self, view, *args, **kwargs):
|
def reverse(self, view, *args, **kwargs):
|
||||||
"""Return a fully qualified URI for a view, using the current request as the base URI.
|
"""Return a fully qualified URI for a view, using the current request as the base URI.
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
pre {border: 1px solid black; padding: 1em; background: #ffd}
|
pre {border: 1px solid black; padding: 1em; background: #ffd}
|
||||||
|
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -14,5 +15,43 @@
|
||||||
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val }}
|
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ content|urlize_quoted_links }}{% endautoescape %} </pre>
|
{{ content|urlize_quoted_links }}{% endautoescape %} </pre>
|
||||||
|
|
||||||
|
{% if 'GET' in allowed_methods %}
|
||||||
|
<div class='action'>
|
||||||
|
<a href='{{ request.path }}'>Read</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'POST' in resource.allowed_methods %}
|
||||||
|
<div class='action'>
|
||||||
|
<form action="{{ request.path }}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ create_form.as_p }}
|
||||||
|
<input type="submit" value="Create" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'PUT' in resource.allowed_methods %}
|
||||||
|
<div class='action'>
|
||||||
|
<form action="{{ request.path }}" method="POST">
|
||||||
|
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ create_form.as_p }}
|
||||||
|
<input type="submit" value="Update" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if 'DELETE' in resource.allowed_methods %}
|
||||||
|
<div class='action'>
|
||||||
|
<form action="{{ request.path }}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
|
||||||
|
<input type="submit" value="Delete" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Binary file not shown.
Binary file not shown.
|
@ -3,5 +3,6 @@ from django.conf.urls.defaults import patterns
|
||||||
urlpatterns = patterns('testapp.views',
|
urlpatterns = patterns('testapp.views',
|
||||||
(r'^$', 'RootResource'),
|
(r'^$', 'RootResource'),
|
||||||
(r'^read-only$', 'ReadOnlyResource'),
|
(r'^read-only$', 'ReadOnlyResource'),
|
||||||
(r'^mirroring-write$', 'MirroringWriteResource'),
|
(r'^write-only$', 'MirroringWriteResource'),
|
||||||
|
(r'^read-write$', 'ReadWriteResource'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from rest.resource import Resource
|
from rest.resource import Resource
|
||||||
|
from testapp.forms import ExampleForm
|
||||||
|
|
||||||
class RootResource(Resource):
|
class RootResource(Resource):
|
||||||
"""This is my docstring
|
"""This is my docstring
|
||||||
"""
|
"""
|
||||||
|
@ -7,7 +8,8 @@ class RootResource(Resource):
|
||||||
|
|
||||||
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(MirroringWriteResource)}, {})
|
'write-only-api': self.reverse(MirroringWriteResource),
|
||||||
|
'read-write-api': self.reverse(ReadWriteResource)}, {})
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyResource(Resource):
|
class ReadOnlyResource(Resource):
|
||||||
|
@ -28,3 +30,9 @@ class MirroringWriteResource(Resource):
|
||||||
|
|
||||||
def create(self, data, headers={}, *args, **kwargs):
|
def create(self, data, headers={}, *args, **kwargs):
|
||||||
return (200, data, {})
|
return (200, data, {})
|
||||||
|
|
||||||
|
|
||||||
|
class ReadWriteResource(Resource):
|
||||||
|
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||||
|
create_form = ExampleForm
|
||||||
|
update_form = ExampleForm
|
||||||
|
|
Loading…
Reference in New Issue
Block a user