aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2011-01-13 17:38:40 +0000
committerTom Christie2011-01-13 17:38:40 +0000
commit764fbe335fbd8dab2b9a097a008bd80bf6582f89 (patch)
treeaf98e4498b00ec0d61fd500c8b6bc95a77ceeae9
parent5557dfb54c8aeb35a781760fc2fb8334e53e98ea (diff)
downloaddjango-rest-framework-764fbe335fbd8dab2b9a097a008bd80bf6582f89.tar.bz2
Various cleanup
-rw-r--r--src/rest/emitters.py9
-rw-r--r--src/rest/resource.py102
-rw-r--r--src/rest/templates/emitter.html21
-rw-r--r--src/rest/templates/emitter.txt9
-rw-r--r--src/testapp/views.py30
5 files changed, 101 insertions, 70 deletions
diff --git a/src/rest/emitters.py b/src/rest/emitters.py
index dcbaf7b7..b911f31c 100644
--- a/src/rest/emitters.py
+++ b/src/rest/emitters.py
@@ -1,5 +1,4 @@
from django.template import RequestContext, loader
-from django.core.handlers.wsgi import STATUS_CODE_TEXT
import json
from utils import dict2xml
@@ -22,14 +21,6 @@ class TemplatedEmitter(BaseEmitter):
template = loader.get_template(self.template)
context = RequestContext(self.resource.request, {
'content': content,
- 'status': self.resource.resp_status,
- 'reason': STATUS_CODE_TEXT.get(self.resource.resp_status, ''),
- 'headers': self.resource.resp_headers,
- 'resource_name': self.resource.__class__.__name__,
- 'resource_doc': self.resource.__doc__,
- 'create_form': self.resource.form,
- 'update_form': self.resource.form,
- 'request': self.resource.request,
'resource': self.resource,
})
diff --git a/src/rest/resource.py b/src/rest/resource.py
index 15ccce48..6ea19246 100644
--- a/src/rest/resource.py
+++ b/src/rest/resource.py
@@ -1,7 +1,9 @@
from django.http import HttpResponse
from django.core.urlresolvers import reverse
+from django.core.handlers.wsgi import STATUS_CODE_TEXT
from rest import emitters, parsers
from decimal import Decimal
+import re
#
STATUS_400_BAD_REQUEST = 400
@@ -22,6 +24,10 @@ class ResourceException(Exception):
class Resource(object):
# List of RESTful operations which may be performed on this resource.
allowed_operations = ('read',)
+ anon_allowed_operations = ()
+
+ # Optional form for input validation and presentation of HTML formatted responses.
+ form = None
# List of content-types the resource can respond with, ordered by preference
emitters = ( ('application/json', emitters.JSONEmitter),
@@ -36,9 +42,6 @@ class Resource(object):
'application/x-www-form-urlencoded': parsers.FormParser,
'multipart/form-data': parsers.FormParser }
- # Optional form for input validation and presentation of HTML formatted responses.
- form = None
-
# Map standard HTTP methods to RESTful operations
CALLMAP = { 'GET': 'read', 'POST': 'create',
'PUT': 'update', 'DELETE': 'delete' }
@@ -57,20 +60,34 @@ class Resource(object):
"""Make the class callable so it can be used as a Django view."""
self = object.__new__(cls)
self.__init__()
- self.request = request
- try:
- return self._handle_request(request, *args, **kwargs)
- except:
- import traceback
- traceback.print_exc()
- raise
-
+ return self._handle_request(request, *args, **kwargs)
def __init__(self):
pass
+ def name(self):
+ """Provide a name for the resource.
+ By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names',
+ although this behaviour may be overridden."""
+ class_name = self.__class__.__name__
+ return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
+
+
+ def description(self):
+ """Provide a description for the resource.
+ By default this is the class's docstring,
+ although this behaviour may be overridden."""
+ return "%s" % self.__doc__
+
+
+ def resp_status_text(self):
+ """Return reason text corrosponding to our HTTP response status code.
+ Provided for convienience."""
+ return STATUS_CODE_TEXT.get(self.resp_status, '')
+
+
def reverse(self, view, *args, **kwargs):
"""Return a fully qualified URI for a given view or resource, using the current request as the base URI.
TODO: Add SITEMAP option.
@@ -125,8 +142,14 @@ class Resource(object):
return method
+ def authenticate(self):
+ """..."""
+ # user = ...
+ # if anon_user and not anon_allowed_operations raise PermissionDenied
+ # return
+
def check_method_allowed(self, method):
- """Ensure the request method is acceptable fot this resource."""
+ """Ensure the request method is acceptable for this resource."""
if not method in self.CALLMAP.keys():
raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method})
@@ -137,13 +160,12 @@ class Resource(object):
- def determine_form(self, data=None, is_response=False):
+ def get_bound_form(self, data=None, is_response=False):
"""Optionally return a Django Form instance, which may be used for validation
and/or rendered by an HTML/XHTML emitter.
If data is not None the form will be bound to data. is_response indicates if data should be
- treated as the input data (bind to client input) or the response data (bind to an existing object).
- """
+ treated as the input data (bind to client input) or the response data (bind to an existing object)."""
if self.form:
if data:
return self.form(data)
@@ -156,15 +178,25 @@ class Resource(object):
"""Perform any resource-specific data deserialization and/or validation
after the initial HTTP content-type deserialization has taken place.
+ Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
+
By default this uses form validation to filter the basic input into the required types."""
if self.form is None:
- return data
+ return (data, None)
+
+ form_instance = self.get_bound_form(data)
+
+ if not form_instance.is_valid():
+ if not form_instance.errors:
+ details = 'No content was supplied'
+ else:
+ details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems())
+ if form_instance.non_field_errors():
+ details['_extra'] = self.form.non_field_errors()
- if not self.form.is_valid():
- details = dict((key, map(unicode, val)) for (key, val) in self.form.errors.iteritems())
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
- return self.form.cleaned_data
+ return (form_instance.cleaned_data, form_instance)
def cleanup_response(self, data):
@@ -188,7 +220,7 @@ class Resource(object):
return self.parsers[content_type]
except KeyError:
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
- {'detail': 'Unsupported content type \'%s\'' % content_type})
+ {'detail': 'Unsupported media type \'%s\'' % content_type})
def determine_emitter(self, request):
@@ -253,13 +285,13 @@ class Resource(object):
5. serialize response data into response content, using standard HTTP content negotiation
"""
emitter = None
+ method = self.determine_method(request)
# We make these attributes to allow for a certain amount of munging,
# eg The HTML emitter needs to render this information
- self.method = self.determine_method(request)
- self.form = None
+ self.request = request
+ self.form_instance = None
self.resp_status = None
- self.resp_content = None
self.resp_headers = {}
try:
@@ -267,32 +299,31 @@ class Resource(object):
mimetype, emitter = self.determine_emitter(request)
# Ensure the requested operation is permitted on this resource
- self.check_method_allowed(self.method)
+ self.check_method_allowed(method)
# Get the appropriate create/read/update/delete function
- func = getattr(self, self.CALLMAP.get(self.method, ''))
+ func = getattr(self, self.CALLMAP.get(method, ''))
# Either generate the response data, deserializing and validating any request data
- if self.method in ('PUT', 'POST'):
+ if method in ('PUT', 'POST'):
parser = self.determine_parser(request)
data = parser(self).parse(request.raw_post_data)
- self.form = self.determine_form(data)
- data = self.cleanup_request(data)
+ (data, self.form_instance) = self.cleanup_request(data)
(self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs)
else:
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs)
- self.form = self.determine_form(ret, is_response=True)
+ self.form_instance = self.get_bound_form(ret, is_response=True)
except ResourceException, exc:
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
if emitter is None:
mimetype, emitter = self.emitters[0]
- if self.form is None:
- self.form = self.determine_form()
+ if self.form_instance is None:
+ self.form_instance = self.get_bound_form()
+
-
# Always add the allow header
self.resp_headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations])
@@ -315,17 +346,16 @@ from django.db.models.query import QuerySet
from django.db.models import Model
import decimal
import inspect
-import re
class ModelResource(Resource):
model = None
fields = None
form_fields = None
- def determine_form(self, data=None, is_response=False):
+ def get_bound_form(self, data=None, is_response=False):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if self.form:
- return super(self.__class__, self).determine_form(data, is_response=is_response)
+ return super(self.__class__, self).get_bound_form(data, is_response=is_response)
elif self.model:
class NewModelForm(ModelForm):
@@ -640,7 +670,7 @@ class ModelResource(Resource):
class QueryModelResource(ModelResource):
allowed_methods = ('read',)
- def determine_form(self, data=None, is_response=False):
+ def get_bound_form(self, data=None, is_response=False):
return None
def read(self, headers={}, *args, **kwargs):
diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html
index ddc91fbf..056c52c5 100644
--- a/src/rest/templates/emitter.html
+++ b/src/rest/templates/emitter.html
@@ -7,26 +7,27 @@
pre {border: 1px solid black; padding: 1em; background: #ffd}
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
</style>
+ <title>API - {{ resource.name }}</title>
</head>
<body>
- <h1>{{ resource_name }}</h1>
- <p>{{ resource_doc }}</p>
- <pre><b>{{ status }} {{ reason }}</b>{% autoescape off %}
-{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
+ <h1>{{ resource.name }}</h1>
+ <p>{{ resource.description }}</p>
+ <pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
+{% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
{% endfor %}
{{ content|urlize_quoted_links }} </pre>{% endautoescape %}
{% if 'read' in resource.allowed_operations %}
<div class='action'>
- <a href='{{ request.path }}'>Read</a>
+ <a href='{{ resource.request.path }}'>Read</a>
</div>
{% endif %}
{% if 'create' in resource.allowed_operations %}
<div class='action'>
- <form action="{{ request.path }}" method="POST">
+ <form action="{{ resource.request.path }}" method="POST">
{% csrf_token %}
- {{ create_form.as_p }}
+ {{ resource.form_instance.as_p }}
<input type="submit" value="Create" />
</form>
</div>
@@ -34,10 +35,10 @@
{% if 'update' in resource.allowed_operations %}
<div class='action'>
- <form action="{{ request.path }}" method="POST">
+ <form action="{{ resource.request.path }}" method="POST">
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
{% csrf_token %}
- {{ create_form.as_p }}
+ {{ resource.form_instance.as_p }}
<input type="submit" value="Update" />
</form>
</div>
@@ -45,7 +46,7 @@
{% if 'delete' in resource.allowed_operations %}
<div class='action'>
- <form action="{{ request.path }}" method="POST">
+ <form action="{{ resource.request.path }}" method="POST">
{% csrf_token %}
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
<input type="submit" value="Delete" />
diff --git a/src/rest/templates/emitter.txt b/src/rest/templates/emitter.txt
index 3bf094c6..78c619df 100644
--- a/src/rest/templates/emitter.txt
+++ b/src/rest/templates/emitter.txt
@@ -1,4 +1,7 @@
-{% autoescape off %}HTTP Status {{ status }}
-{% for key, val in headers.items %}{{ key }}: {{ val }}
+{{ resource.name }}
+{{ resource.description }}
+
+{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
+{% for key, val in resource.resp_headers.items %}{{ key }}: {{ val }}
{% endfor %}
-{{ content }}{% endautoescape %} \ No newline at end of file
+{{ content }}{% endautoescape %}
diff --git a/src/testapp/views.py b/src/testapp/views.py
index eca69cc3..dee0b19b 100644
--- a/src/testapp/views.py
+++ b/src/testapp/views.py
@@ -1,6 +1,8 @@
from rest.resource import Resource, ModelResource, QueryModelResource
from testapp.models import BlogPost, Comment
-
+
+##### Root Resource #####
+
class RootResource(Resource):
"""This is the top level resource for the API.
All the sub-resources are discoverable from here."""
@@ -11,49 +13,53 @@ class RootResource(Resource):
'blog-post': self.reverse(BlogPostCreator)}, {})
-# Blog Post Resources
+##### Blog Post Resources #####
+
+BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
class BlogPostList(QueryModelResource):
"""A resource which lists all existing blog posts."""
allowed_operations = ('read', )
model = BlogPost
-
+ fields = BLOG_POST_FIELDS
class BlogPostCreator(ModelResource):
"""A resource with which blog posts may be created."""
allowed_operations = ('create',)
model = BlogPost
- fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
-
+ fields = BLOG_POST_FIELDS
class BlogPostInstance(ModelResource):
"""A resource which represents a single blog post."""
allowed_operations = ('read', 'update', 'delete')
model = BlogPost
- fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
+ fields = BLOG_POST_FIELDS
-# Comment Resources
+##### Comment Resources #####
+
+COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
class CommentList(QueryModelResource):
"""A resource which lists all existing comments for a given blog post."""
allowed_operations = ('read', )
model = Comment
-
+ fields = COMMENT_FIELDS
class CommentCreator(ModelResource):
"""A resource with which blog comments may be created for a given blog post."""
allowed_operations = ('create',)
model = Comment
- fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
-
+ fields = COMMENT_FIELDS
class CommentInstance(ModelResource):
"""A resource which represents a single comment."""
allowed_operations = ('read', 'update', 'delete')
model = Comment
- fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
-
+ fields = COMMENT_FIELDS
+
+
+
#
#'read-only-api': self.reverse(ReadOnlyResource),
# 'write-only-api': self.reverse(WriteOnlyResource),