Various cleanup

This commit is contained in:
Tom Christie 2011-01-13 17:38:40 +00:00
parent 5557dfb54c
commit 764fbe335f
5 changed files with 101 additions and 70 deletions

View File

@ -1,5 +1,4 @@
from django.template import RequestContext, loader from django.template import RequestContext, loader
from django.core.handlers.wsgi import STATUS_CODE_TEXT
import json import json
from utils import dict2xml from utils import dict2xml
@ -22,14 +21,6 @@ class TemplatedEmitter(BaseEmitter):
template = loader.get_template(self.template) template = loader.get_template(self.template)
context = RequestContext(self.resource.request, { context = RequestContext(self.resource.request, {
'content': content, '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, 'resource': self.resource,
}) })

View File

@ -1,7 +1,9 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from rest import emitters, parsers from rest import emitters, parsers
from decimal import Decimal from decimal import Decimal
import re
# #
STATUS_400_BAD_REQUEST = 400 STATUS_400_BAD_REQUEST = 400
@ -22,6 +24,10 @@ class ResourceException(Exception):
class Resource(object): class Resource(object):
# List of RESTful operations which may be performed on this resource. # List of RESTful operations which may be performed on this resource.
allowed_operations = ('read',) 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 # List of content-types the resource can respond with, ordered by preference
emitters = ( ('application/json', emitters.JSONEmitter), emitters = ( ('application/json', emitters.JSONEmitter),
@ -36,9 +42,6 @@ class Resource(object):
'application/x-www-form-urlencoded': parsers.FormParser, 'application/x-www-form-urlencoded': parsers.FormParser,
'multipart/form-data': 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 # Map standard HTTP methods to RESTful operations
CALLMAP = { 'GET': 'read', 'POST': 'create', CALLMAP = { 'GET': 'read', 'POST': 'create',
'PUT': 'update', 'DELETE': 'delete' } 'PUT': 'update', 'DELETE': 'delete' }
@ -57,20 +60,34 @@ class Resource(object):
"""Make the class callable so it can be used as a Django view.""" """Make the class callable so it can be used as a Django view."""
self = object.__new__(cls) self = object.__new__(cls)
self.__init__() self.__init__()
self.request = request
try:
return self._handle_request(request, *args, **kwargs) return self._handle_request(request, *args, **kwargs)
except:
import traceback
traceback.print_exc()
raise
def __init__(self): def __init__(self):
pass 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): 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. """Return a fully qualified URI for a given view or resource, using the current request as the base URI.
TODO: Add SITEMAP option. TODO: Add SITEMAP option.
@ -125,8 +142,14 @@ class Resource(object):
return method return method
def authenticate(self):
"""..."""
# user = ...
# if anon_user and not anon_allowed_operations raise PermissionDenied
# return
def check_method_allowed(self, method): 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(): if not method in self.CALLMAP.keys():
raise ResourceException(STATUS_501_NOT_IMPLEMENTED, raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method}) {'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 """Optionally return a Django Form instance, which may be used for validation
and/or rendered by an HTML/XHTML emitter. 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 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 self.form:
if data: if data:
return self.form(data) return self.form(data)
@ -156,15 +178,25 @@ class Resource(object):
"""Perform any resource-specific data deserialization and/or validation """Perform any resource-specific data deserialization and/or validation
after the initial HTTP content-type deserialization has taken place. 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.""" By default this uses form validation to filter the basic input into the required types."""
if self.form is None: 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}) 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): def cleanup_response(self, data):
@ -188,7 +220,7 @@ class Resource(object):
return self.parsers[content_type] return self.parsers[content_type]
except KeyError: except KeyError:
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE, 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): def determine_emitter(self, request):
@ -253,13 +285,13 @@ class Resource(object):
5. serialize response data into response content, using standard HTTP content negotiation 5. serialize response data into response content, using standard HTTP content negotiation
""" """
emitter = None emitter = None
method = self.determine_method(request)
# We make these attributes to allow for a certain amount of munging, # We make these attributes to allow for a certain amount of munging,
# eg The HTML emitter needs to render this information # eg The HTML emitter needs to render this information
self.method = self.determine_method(request) self.request = request
self.form = None self.form_instance = None
self.resp_status = None self.resp_status = None
self.resp_content = None
self.resp_headers = {} self.resp_headers = {}
try: try:
@ -267,30 +299,29 @@ class Resource(object):
mimetype, emitter = self.determine_emitter(request) mimetype, emitter = self.determine_emitter(request)
# Ensure the requested operation is permitted on this resource # 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 # 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 # 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) parser = self.determine_parser(request)
data = parser(self).parse(request.raw_post_data) data = parser(self).parse(request.raw_post_data)
self.form = self.determine_form(data) (data, self.form_instance) = self.cleanup_request(data)
data = self.cleanup_request(data)
(self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs) (self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs)
else: else:
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs) (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: except ResourceException, exc:
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers) (self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
if emitter is None: if emitter is None:
mimetype, emitter = self.emitters[0] mimetype, emitter = self.emitters[0]
if self.form is None: if self.form_instance is None:
self.form = self.determine_form() self.form_instance = self.get_bound_form()
# Always add the allow header # Always add the allow header
@ -315,17 +346,16 @@ from django.db.models.query import QuerySet
from django.db.models import Model from django.db.models import Model
import decimal import decimal
import inspect import inspect
import re
class ModelResource(Resource): class ModelResource(Resource):
model = None model = None
fields = None fields = None
form_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""" """Return a form that may be used in validation and/or rendering an html emitter"""
if self.form: 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: elif self.model:
class NewModelForm(ModelForm): class NewModelForm(ModelForm):
@ -640,7 +670,7 @@ class ModelResource(Resource):
class QueryModelResource(ModelResource): class QueryModelResource(ModelResource):
allowed_methods = ('read',) allowed_methods = ('read',)
def determine_form(self, data=None, is_response=False): def get_bound_form(self, data=None, is_response=False):
return None return None
def read(self, headers={}, *args, **kwargs): def read(self, headers={}, *args, **kwargs):

View File

@ -7,26 +7,27 @@
pre {border: 1px solid black; padding: 1em; background: #ffd} pre {border: 1px solid black; padding: 1em; background: #ffd}
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf} div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
</style> </style>
<title>API - {{ resource.name }}</title>
</head> </head>
<body> <body>
<h1>{{ resource_name }}</h1> <h1>{{ resource.name }}</h1>
<p>{{ resource_doc }}</p> <p>{{ resource.description }}</p>
<pre><b>{{ status }} {{ reason }}</b>{% autoescape off %} <pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
{% for key, val in headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }} {% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
{% endfor %} {% endfor %}
{{ content|urlize_quoted_links }} </pre>{% endautoescape %} {{ content|urlize_quoted_links }} </pre>{% endautoescape %}
{% if 'read' in resource.allowed_operations %} {% if 'read' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<a href='{{ request.path }}'>Read</a> <a href='{{ resource.request.path }}'>Read</a>
</div> </div>
{% endif %} {% endif %}
{% if 'create' in resource.allowed_operations %} {% if 'create' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<form action="{{ request.path }}" method="POST"> <form action="{{ resource.request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
{{ create_form.as_p }} {{ resource.form_instance.as_p }}
<input type="submit" value="Create" /> <input type="submit" value="Create" />
</form> </form>
</div> </div>
@ -34,10 +35,10 @@
{% if 'update' in resource.allowed_operations %} {% if 'update' in resource.allowed_operations %}
<div class='action'> <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" /> <input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
{% csrf_token %} {% csrf_token %}
{{ create_form.as_p }} {{ resource.form_instance.as_p }}
<input type="submit" value="Update" /> <input type="submit" value="Update" />
</form> </form>
</div> </div>
@ -45,7 +46,7 @@
{% if 'delete' in resource.allowed_operations %} {% if 'delete' in resource.allowed_operations %}
<div class='action'> <div class='action'>
<form action="{{ request.path }}" method="POST"> <form action="{{ resource.request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" /> <input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
<input type="submit" value="Delete" /> <input type="submit" value="Delete" />

View File

@ -1,4 +1,7 @@
{% autoescape off %}HTTP Status {{ status }} {{ resource.name }}
{% for key, val in headers.items %}{{ key }}: {{ val }} {{ 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 %} {% endfor %}
{{ content }}{% endautoescape %} {{ content }}{% endautoescape %}

View File

@ -1,6 +1,8 @@
from rest.resource import Resource, ModelResource, QueryModelResource from rest.resource import Resource, ModelResource, QueryModelResource
from testapp.models import BlogPost, Comment from testapp.models import BlogPost, Comment
##### Root Resource #####
class RootResource(Resource): class RootResource(Resource):
"""This is the top level resource for the API. """This is the top level resource for the API.
All the sub-resources are discoverable from here.""" All the sub-resources are discoverable from here."""
@ -11,48 +13,52 @@ class RootResource(Resource):
'blog-post': self.reverse(BlogPostCreator)}, {}) '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): class BlogPostList(QueryModelResource):
"""A resource which lists all existing blog posts.""" """A resource which lists all existing blog posts."""
allowed_operations = ('read', ) allowed_operations = ('read', )
model = BlogPost model = BlogPost
fields = BLOG_POST_FIELDS
class BlogPostCreator(ModelResource): class BlogPostCreator(ModelResource):
"""A resource with which blog posts may be created.""" """A resource with which blog posts may be created."""
allowed_operations = ('create',) allowed_operations = ('create',)
model = BlogPost model = BlogPost
fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url') fields = BLOG_POST_FIELDS
class BlogPostInstance(ModelResource): class BlogPostInstance(ModelResource):
"""A resource which represents a single blog post.""" """A resource which represents a single blog post."""
allowed_operations = ('read', 'update', 'delete') allowed_operations = ('read', 'update', 'delete')
model = BlogPost 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): class CommentList(QueryModelResource):
"""A resource which lists all existing comments for a given blog post.""" """A resource which lists all existing comments for a given blog post."""
allowed_operations = ('read', ) allowed_operations = ('read', )
model = Comment model = Comment
fields = COMMENT_FIELDS
class CommentCreator(ModelResource): class CommentCreator(ModelResource):
"""A resource with which blog comments may be created for a given blog post.""" """A resource with which blog comments may be created for a given blog post."""
allowed_operations = ('create',) allowed_operations = ('create',)
model = Comment model = Comment
fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url') fields = COMMENT_FIELDS
class CommentInstance(ModelResource): class CommentInstance(ModelResource):
"""A resource which represents a single comment.""" """A resource which represents a single comment."""
allowed_operations = ('read', 'update', 'delete') allowed_operations = ('read', 'update', 'delete')
model = Comment model = Comment
fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url') fields = COMMENT_FIELDS
# #
#'read-only-api': self.reverse(ReadOnlyResource), #'read-only-api': self.reverse(ReadOnlyResource),