aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortom christie tom@tomchristie.com2011-01-26 20:31:47 +0000
committertom christie tom@tomchristie.com2011-01-26 20:31:47 +0000
commit8b89d7416cf4e2396deac4ba41c23cdcdc8b9704 (patch)
treeb99a1b05964a5706fc7fa8de8591cb65400b099e
parenteff54c00d514e1edd74fbc789f9064d09db40b02 (diff)
downloaddjango-rest-framework-8b89d7416cf4e2396deac4ba41c23cdcdc8b9704.tar.bz2
Content Type tunneling
-rw-r--r--examples/objectstore/views.py14
-rw-r--r--examples/urls.py1
-rw-r--r--flywheel/emitters.py69
-rw-r--r--flywheel/parsers.py8
-rw-r--r--flywheel/resource.py39
5 files changed, 94 insertions, 37 deletions
diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py
index 16c7f8e9..2eaadd3c 100644
--- a/examples/objectstore/views.py
+++ b/examples/objectstore/views.py
@@ -13,14 +13,14 @@ OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
class ObjectStoreRoot(Resource):
"""Root of the Object Store API.
Allows the client to get a complete list of all the stored objects, or to create a new stored object."""
- allowed_methods = ('GET', 'POST')
+ allowed_methods = anon_allowed_methods = ('GET', 'POST')
- def get(self, request):
+ def get(self, request, auth):
"""Return a list of all the stored object URLs."""
keys = sorted(os.listdir(OBJECT_STORE_DIR))
return [self.reverse(StoredObject, key=key) for key in keys]
- def post(self, request, content):
+ def post(self, request, auth, content):
"""Create a new stored object, with a unique key."""
key = str(uuid.uuid1())
pathname = os.path.join(OBJECT_STORE_DIR, key)
@@ -31,22 +31,22 @@ class ObjectStoreRoot(Resource):
class StoredObject(Resource):
"""Represents a stored object.
The object may be any picklable content."""
- allowed_methods = ('GET', 'PUT', 'DELETE')
+ allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
- def get(self, request, key):
+ def get(self, request, auth, key):
"""Return a stored object, by unpickling the contents of a locally stored file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):
return Response(status.HTTP_404_NOT_FOUND)
return pickle.load(open(pathname, 'rb'))
- def put(self, request, content, key):
+ def put(self, request, auth, content, key):
"""Update/create a stored object, by pickling the request content to a locally stored file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(content, open(pathname, 'wb'))
return content
- def delete(self, request, key):
+ def delete(self, request, auth, key):
"""Delete a stored object, by removing it's pickled file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):
diff --git a/examples/urls.py b/examples/urls.py
index 03791084..ebf2c9a3 100644
--- a/examples/urls.py
+++ b/examples/urls.py
@@ -4,6 +4,7 @@ from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
+ (r'pygments-example/', include('pygmentsapi.urls')),
(r'^blog-post-example/', include('blogpost.urls')),
(r'^object-store-example/', include('objectstore.urls')),
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
diff --git a/flywheel/emitters.py b/flywheel/emitters.py
index 7492b07b..57e09ce9 100644
--- a/flywheel/emitters.py
+++ b/flywheel/emitters.py
@@ -1,8 +1,10 @@
from django.template import RequestContext, loader
+from django import forms
from flywheel.response import NoContent
from utils import dict2xml
+import string
try:
import json
except ImportError:
@@ -20,25 +22,31 @@ class BaseEmitter(object):
raise Exception('emit() function on a subclass of BaseEmitter must be implemented')
-from django import forms
-class JSONForm(forms.Form):
- _contenttype = forms.CharField(max_length=256, initial='application/json', label='Content Type')
- _content = forms.CharField(label='Content', widget=forms.Textarea)
+
class DocumentingTemplateEmitter(BaseEmitter):
"""Emitter used to self-document the API"""
template = None
- def emit(self, output=NoContent):
- resource = self.resource
+ def _get_content(self, resource, output):
+ """Get the content as if it had been emitted by a non-documenting emitter.
+
+ (Typically this will be the content as it would have been if the Resource had been
+ requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
- # Find the first valid emitter and emit the content. (Don't another documenting emitter.)
+ # Find the first valid emitter and emit the content. (Don't use another documenting emitter.)
emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
if not emitters:
- content = 'No emitters were found'
- else:
- content = emitters[0](resource).emit(output, verbose=True)
-
+ return '[No emitters were found]'
+
+ content = emitters[0](resource).emit(output, verbose=True)
+ if not all(char in string.printable for char in content):
+ return '[%d bytes of binary content]'
+
+ return content
+
+
+ def _get_form_instance(self, resource):
# Get the form instance if we have one bound to the input
form_instance = resource.form_instance
@@ -57,8 +65,45 @@ class DocumentingTemplateEmitter(BaseEmitter):
except:
pass
+ # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
if not form_instance:
- form_instance = JSONForm()
+ form_instance = self._get_generic_content_form(resource)
+
+ return form_instance
+
+
+ def _get_generic_content_form(self, resource):
+ """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
+ (Which are typically application/x-www-form-urlencoded)"""
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self, resource):
+ """We don't know the names of the fields we want to set until the point the form is instantiated,
+ as they are determined by the Resource the form is being created against.
+ Add the fields dynamically."""
+ super(GenericContentForm, self).__init__()
+
+ contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
+ initial_contenttype = resource.default_parser.media_type
+
+ self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
+ choices=contenttype_choices,
+ initial=initial_contenttype)
+ self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
+ widget=forms.Textarea)
+
+ # If either of these reserved parameters are turned off then content tunneling is not possible
+ if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
+ return None
+
+ # Okey doke, let's do it
+ return GenericContentForm(resource)
+
+
+ def emit(self, output=NoContent):
+ content = self._get_content(self.resource, output)
+ form_instance = self._get_form_instance(self.resource)
template = loader.get_template(self.template)
context = RequestContext(self.resource.request, {
diff --git a/flywheel/parsers.py b/flywheel/parsers.py
index f0684612..3f0a4e5b 100644
--- a/flywheel/parsers.py
+++ b/flywheel/parsers.py
@@ -8,7 +8,7 @@ except ImportError:
# TODO: Make all parsers only list a single media_type, rather than a list
class BaseParser(object):
- media_types = ()
+ media_type = None
def __init__(self, resource):
self.resource = resource
@@ -18,7 +18,7 @@ class BaseParser(object):
class JSONParser(BaseParser):
- media_types = ('application/xml',)
+ media_type = 'application/json'
def parse(self, input):
try:
@@ -27,7 +27,7 @@ class JSONParser(BaseParser):
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class XMLParser(BaseParser):
- media_types = ('application/xml',)
+ media_type = 'application/xml'
class FormParser(BaseParser):
@@ -35,7 +35,7 @@ class FormParser(BaseParser):
Return a dict containing a single value for each non-reserved parameter.
"""
- media_types = ('application/x-www-form-urlencoded',)
+ media_type = 'application/x-www-form-urlencoded'
def parse(self, input):
# The FormParser doesn't parse the input as other parsers would, since Django's already done the
diff --git a/flywheel/resource.py b/flywheel/resource.py
index 8c20e14f..c76ce2dd 100644
--- a/flywheel/resource.py
+++ b/flywheel/resource.py
@@ -120,14 +120,16 @@ class Resource(object):
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
return self.emitters[0]
- # TODO:
-
- #def parsed_media_types(self):
- # """Return an list of all the media types that this resource can emit."""
- # return [parser.media_type for parser in self.parsers]
+ @property
+ def parsed_media_types(self):
+ """Return an list of all the media types that this resource can emit."""
+ return [parser.media_type for parser in self.parsers]
- #def default_parser(self):
- # return self.parsers[0]
+ @property
+ def default_parser(self):
+ """Return the resource's most prefered emitter.
+ (This has no behavioural effect, but is may be used by documenting emitters)"""
+ return self.parsers[0]
def reverse(self, view, *args, **kwargs):
@@ -281,19 +283,28 @@ class Resource(object):
"""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."""
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
+ raw_content = request.raw_post_data
+
split = content_type.split(';', 1)
if len(split) > 1:
content_type = split[0]
content_type = content_type.strip()
+ # If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
+ if (content_type == 'application/x-www-form-urlencoded' and
+ request.method == 'POST' and
+ self.CONTENTTYPE_PARAM and
+ self.CONTENT_PARAM and
+ request.POST.get(self.CONTENTTYPE_PARAM, None) and
+ request.POST.get(self.CONTENT_PARAM, None)):
+ raw_content = request.POST[self.CONTENT_PARAM]
+ content_type = request.POST[self.CONTENTTYPE_PARAM]
+
# Create a list of list of (media_type, Parser) tuples
- media_type_parser_tuples = [[(media_type, parser) for media_type in parser.media_types] for parser in self.parsers]
-
- # Flatten the list and turn it into a media_type -> Parser dict
- media_type_to_parser = dict(chain.from_iterable(media_type_parser_tuples))
+ media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
try:
- return media_type_to_parser[content_type]
+ return (media_type_to_parser[content_type], raw_content)
except KeyError:
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
{'detail': 'Unsupported media type \'%s\'' % content_type})
@@ -397,8 +408,8 @@ class Resource(object):
# Either generate the response data, deserializing and validating any request data
# TODO: Add support for message bodys on other HTTP methods, as it is valid.
if method in ('PUT', 'POST'):
- parser = self.determine_parser(request)
- data = parser(self).parse(request.raw_post_data)
+ (parser, raw_content) = self.determine_parser(request)
+ data = parser(self).parse(raw_content)
self.form_instance = self.get_form(data)
data = self.cleanup_request(data, self.form_instance)
response = func(request, auth_context, data, *args, **kwargs)