aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortom christie tom@tomchristie.com2010-12-30 23:29:01 +0000
committertom christie tom@tomchristie.com2010-12-30 23:29:01 +0000
commitc56e48f52e26a81d7a9f81fd74b0ea46d5434a90 (patch)
tree98a2d33f9d23cbcbe77053a6ec6e1d3540442e99 /src
parenta78f57847592fbaba9b483e2ace1591c9f295c71 (diff)
downloaddjango-rest-framework-c56e48f52e26a81d7a9f81fd74b0ea46d5434a90.tar.bz2
Add parsers, form validation, etc...
Diffstat (limited to 'src')
-rw-r--r--src/rest/emitters.py16
-rw-r--r--src/rest/parsers.py8
-rw-r--r--src/rest/resource.py111
-rw-r--r--src/rest/templates/emitter.html39
-rw-r--r--src/rest/templatetags/__init__.pycbin163 -> 171 bytes
-rw-r--r--src/rest/templatetags/urlize_quoted_links.pycbin4515 -> 4539 bytes
-rw-r--r--src/testapp/urls.py3
-rw-r--r--src/testapp/views.py12
8 files changed, 155 insertions, 34 deletions
diff --git a/src/rest/emitters.py b/src/rest/emitters.py
index ee8ca57f..ce126723 100644
--- a/src/rest/emitters.py
+++ b/src/rest/emitters.py
@@ -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)
diff --git a/src/rest/parsers.py b/src/rest/parsers.py
index 038065f0..0f914471 100644
--- a/src/rest/parsers.py
+++ b/src/rest/parsers.py
@@ -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
diff --git a/src/rest/resource.py b/src/rest/resource.py
index 4e9c4e05..e6b41e07 100644
--- a/src/rest/resource.py
+++ b/src/rest/resource.py
@@ -1,16 +1,21 @@
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 Resource(object):
- class HTTPException(Exception):
- def __init__(self, status, content, headers):
- self.status = status
- self.content = content
- self.headers = headers
+class ResourceException(Exception):
+ def __init__(self, status, content='', headers={}):
+ self.status = status
+ self.content = content
+ self.headers = headers
+
+
+class Resource(object):
allowed_methods = ('GET',)
@@ -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.
diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html
index c8ddc16d..f5d0df08 100644
--- a/src/rest/templates/emitter.html
+++ b/src/rest/templates/emitter.html
@@ -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> \ No newline at end of file
diff --git a/src/rest/templatetags/__init__.pyc b/src/rest/templatetags/__init__.pyc
index 69527f63..9daf8783 100644
--- a/src/rest/templatetags/__init__.pyc
+++ b/src/rest/templatetags/__init__.pyc
Binary files differ
diff --git a/src/rest/templatetags/urlize_quoted_links.pyc b/src/rest/templatetags/urlize_quoted_links.pyc
index b49e16b6..37e480ac 100644
--- a/src/rest/templatetags/urlize_quoted_links.pyc
+++ b/src/rest/templatetags/urlize_quoted_links.pyc
Binary files differ
diff --git a/src/testapp/urls.py b/src/testapp/urls.py
index a7d430bc..9cebd4ce 100644
--- a/src/testapp/urls.py
+++ b/src/testapp/urls.py
@@ -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'),
)
diff --git a/src/testapp/views.py b/src/testapp/views.py
index eca4c0ae..3bd610ff 100644
--- a/src/testapp/views.py
+++ b/src/testapp/views.py
@@ -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