diff options
| author | tom christie tom@tomchristie.com | 2010-12-30 23:29:01 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2010-12-30 23:29:01 +0000 |
| commit | c56e48f52e26a81d7a9f81fd74b0ea46d5434a90 (patch) | |
| tree | 98a2d33f9d23cbcbe77053a6ec6e1d3540442e99 /src | |
| parent | a78f57847592fbaba9b483e2ace1591c9f295c71 (diff) | |
| download | django-rest-framework-c56e48f52e26a81d7a9f81fd74b0ea46d5434a90.tar.bz2 | |
Add parsers, form validation, etc...
Diffstat (limited to 'src')
| -rw-r--r-- | src/rest/emitters.py | 16 | ||||
| -rw-r--r-- | src/rest/parsers.py | 8 | ||||
| -rw-r--r-- | src/rest/resource.py | 111 | ||||
| -rw-r--r-- | src/rest/templates/emitter.html | 39 | ||||
| -rw-r--r-- | src/rest/templatetags/__init__.pyc | bin | 163 -> 171 bytes | |||
| -rw-r--r-- | src/rest/templatetags/urlize_quoted_links.pyc | bin | 4515 -> 4539 bytes | |||
| -rw-r--r-- | src/testapp/urls.py | 3 | ||||
| -rw-r--r-- | src/testapp/views.py | 12 |
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 Binary files differindex 69527f63..9daf8783 100644 --- a/src/rest/templatetags/__init__.pyc +++ b/src/rest/templatetags/__init__.pyc diff --git a/src/rest/templatetags/urlize_quoted_links.pyc b/src/rest/templatetags/urlize_quoted_links.pyc Binary files differindex b49e16b6..37e480ac 100644 --- a/src/rest/templatetags/urlize_quoted_links.pyc +++ b/src/rest/templatetags/urlize_quoted_links.pyc 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 |
