Add parsers, form validation, etc...

This commit is contained in:
tom christie tom@tomchristie.com 2010-12-30 23:29:01 +00:00
parent a78f578475
commit c56e48f52e
8 changed files with 155 additions and 34 deletions

View File

@ -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
import json
class BaseEmitter(object):
def __init__(self, resource, status, headers):
def __init__(self, resource, request, status, headers):
self.request = request
self.resource = resource
self.status = status
self.headers = headers
@ -12,16 +13,23 @@ class BaseEmitter(object):
return output
class TemplatedEmitter(BaseEmitter):
template = None
def emit(self, output):
content = json.dumps(output, indent=4)
template = loader.get_template(self.template)
context = Context({
context = RequestContext(self.request, {
'content': content,
'status': self.status,
'reason': STATUS_CODE_TEXT.get(self.status, ''),
'headers': self.headers,
'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)

View File

@ -1,3 +1,5 @@
import json
class BaseParser(object):
def __init__(self, resource, request):
self.resource = resource
@ -8,11 +10,13 @@ class BaseParser(object):
class JSONParser(BaseParser):
pass
def parse(self, input):
return json.loads(input)
class XMLParser(BaseParser):
pass
class FormParser(BaseParser):
pass
def parse(self, input):
return self.request.POST

View File

@ -1,17 +1,22 @@
from django.http import HttpResponse
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
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 HTTPException(Exception):
def __init__(self, status, content, headers):
self.status = status
self.content = content
self.headers = headers
allowed_methods = ('GET',)
callmap = { 'GET': 'read', 'POST': 'create',
@ -27,6 +32,12 @@ class Resource(object):
'application/xml': parsers.XMLParser,
'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):
self = object.__new__(cls)
@ -34,15 +45,40 @@ class Resource(object):
self._request = request
return self._handle_request(request, *args, **kwargs)
def __init__(self):
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):
"""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."""
return self.parsers.values()[0]
# TODO: Raise 415 Unsupported media type
try:
return self.parsers[request.META['CONTENT_TYPE']]
except:
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
{'detail': 'Unsupported media type'})
def _determine_emitter(self, request):
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
@ -90,16 +126,40 @@ class Resource(object):
(accept_mimetype == mimetype)):
return (mimetype, emitter)
raise self.HTTPException(406, {'status': 'Not Acceptable',
'accepts': ','.join(item[0] for item in self.emitters)}, {})
raise ResourceException(STATUS_406_NOT_ACCEPTABLE,
{'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):
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:
if not method in self.allowed_methods:
raise self.HTTPException(405, {'status': 'Method Not Allowed'}, {})
self._check_method_allowed(method)
# Parse the HTTP Request content
func = getattr(self, self.callmap.get(method, ''))
@ -107,11 +167,12 @@ class Resource(object):
if method in ('PUT', 'POST'):
parser = self._determine_parser(request)
data = parser(self, request).parse(request.raw_post_data)
data = self._validate_data(method, data)
(status, ret, headers) = func(data, request.META, *args, **kwargs)
else:
(status, ret, headers) = func(request.META, *args, **kwargs)
except self.HTTPException, exc:
except ResourceException, exc:
(status, ret, headers) = (exc.status, exc.content, exc.headers)
headers['Allow'] = ', '.join(self.allowed_methods)
@ -119,11 +180,11 @@ class Resource(object):
# Serialize the HTTP Response content
try:
mimetype, emitter = self._determine_emitter(request)
except self.HTTPException, exc:
except ResourceException, exc:
(status, ret, headers) = (exc.status, exc.content, exc.headers)
mimetype, emitter = self.emitters[0]
content = emitter(self, status, headers).emit(ret)
content = emitter(self, request, status, headers).emit(ret)
# Build the HTTP Response
resp = HttpResponse(content, mimetype=mimetype, status=status)
@ -134,20 +195,20 @@ class Resource(object):
def _not_implemented(self, operation):
resource_name = self.__class__.__name__
return (500, {'status': 'Internal Server Error',
'detail': '%s %s operation is permitted but has not been implemented' % (resource_name, operation)}, {})
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
def read(self, headers={}, *args, **kwargs):
return self._not_implemented('read')
self._not_implemented('read')
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):
return self._not_implemented('update')
self._not_implemented('update')
def delete(self, headers={}, *args, **kwargs):
return self._not_implemented('delete')
self._not_implemented('delete')
def reverse(self, view, *args, **kwargs):
"""Return a fully qualified URI for a view, using the current request as the base URI.

View File

@ -5,6 +5,7 @@
<head>
<style>
pre {border: 1px solid black; padding: 1em; background: #ffd}
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
</style>
</head>
<body>
@ -14,5 +15,43 @@
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val }}
{% endfor %}
{{ 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>
</html>

Binary file not shown.

View File

@ -3,5 +3,6 @@ from django.conf.urls.defaults import patterns
urlpatterns = patterns('testapp.views',
(r'^$', 'RootResource'),
(r'^read-only$', 'ReadOnlyResource'),
(r'^mirroring-write$', 'MirroringWriteResource'),
(r'^write-only$', 'MirroringWriteResource'),
(r'^read-write$', 'ReadWriteResource'),
)

View File

@ -1,5 +1,6 @@
from rest.resource import Resource
from testapp.forms import ExampleForm
class RootResource(Resource):
"""This is my docstring
"""
@ -7,7 +8,8 @@ class RootResource(Resource):
def read(self, headers={}, *args, **kwargs):
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):
@ -28,3 +30,9 @@ class MirroringWriteResource(Resource):
def create(self, data, headers={}, *args, **kwargs):
return (200, data, {})
class ReadWriteResource(Resource):
allowed_methods = ('GET', 'PUT', 'DELETE')
create_form = ExampleForm
update_form = ExampleForm