mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +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
|
||||
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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
Binary file not shown.
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user