mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Content Type tunneling
This commit is contained in:
parent
eff54c00d5
commit
8b89d7416c
|
@ -13,14 +13,14 @@ OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
|
||||||
class ObjectStoreRoot(Resource):
|
class ObjectStoreRoot(Resource):
|
||||||
"""Root of the Object Store API.
|
"""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."""
|
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."""
|
"""Return a list of all the stored object URLs."""
|
||||||
keys = sorted(os.listdir(OBJECT_STORE_DIR))
|
keys = sorted(os.listdir(OBJECT_STORE_DIR))
|
||||||
return [self.reverse(StoredObject, key=key) for key in keys]
|
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."""
|
"""Create a new stored object, with a unique key."""
|
||||||
key = str(uuid.uuid1())
|
key = str(uuid.uuid1())
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||||
|
@ -31,22 +31,22 @@ class ObjectStoreRoot(Resource):
|
||||||
class StoredObject(Resource):
|
class StoredObject(Resource):
|
||||||
"""Represents a stored object.
|
"""Represents a stored object.
|
||||||
The object may be any picklable content."""
|
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."""
|
"""Return a stored object, by unpickling the contents of a locally stored file."""
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(pathname):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status.HTTP_404_NOT_FOUND)
|
||||||
return pickle.load(open(pathname, 'rb'))
|
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."""
|
"""Update/create a stored object, by pickling the request content to a locally stored file."""
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||||
pickle.dump(content, open(pathname, 'wb'))
|
pickle.dump(content, open(pathname, 'wb'))
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def delete(self, request, key):
|
def delete(self, request, auth, key):
|
||||||
"""Delete a stored object, by removing it's pickled file."""
|
"""Delete a stored object, by removing it's pickled file."""
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(pathname):
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.contrib import admin
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
(r'pygments-example/', include('pygmentsapi.urls')),
|
||||||
(r'^blog-post-example/', include('blogpost.urls')),
|
(r'^blog-post-example/', include('blogpost.urls')),
|
||||||
(r'^object-store-example/', include('objectstore.urls')),
|
(r'^object-store-example/', include('objectstore.urls')),
|
||||||
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.template import RequestContext, loader
|
from django.template import RequestContext, loader
|
||||||
|
from django import forms
|
||||||
|
|
||||||
from flywheel.response import NoContent
|
from flywheel.response import NoContent
|
||||||
|
|
||||||
from utils import dict2xml
|
from utils import dict2xml
|
||||||
|
import string
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -20,25 +22,31 @@ class BaseEmitter(object):
|
||||||
raise Exception('emit() function on a subclass of BaseEmitter must be implemented')
|
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):
|
class DocumentingTemplateEmitter(BaseEmitter):
|
||||||
"""Emitter used to self-document the API"""
|
"""Emitter used to self-document the API"""
|
||||||
template = None
|
template = None
|
||||||
|
|
||||||
def emit(self, output=NoContent):
|
def _get_content(self, resource, output):
|
||||||
resource = self.resource
|
"""Get the content as if it had been emitted by a non-documenting emitter.
|
||||||
|
|
||||||
# Find the first valid emitter and emit the content. (Don't another 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 use another documenting emitter.)
|
||||||
emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
|
emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
|
||||||
if not emitters:
|
if not emitters:
|
||||||
content = 'No emitters were found'
|
return '[No emitters were found]'
|
||||||
else:
|
|
||||||
content = emitters[0](resource).emit(output, verbose=True)
|
|
||||||
|
|
||||||
|
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
|
# Get the form instance if we have one bound to the input
|
||||||
form_instance = resource.form_instance
|
form_instance = resource.form_instance
|
||||||
|
|
||||||
|
@ -57,8 +65,45 @@ class DocumentingTemplateEmitter(BaseEmitter):
|
||||||
except:
|
except:
|
||||||
pass
|
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:
|
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)
|
template = loader.get_template(self.template)
|
||||||
context = RequestContext(self.resource.request, {
|
context = RequestContext(self.resource.request, {
|
||||||
|
|
|
@ -8,7 +8,7 @@ except ImportError:
|
||||||
# TODO: Make all parsers only list a single media_type, rather than a list
|
# TODO: Make all parsers only list a single media_type, rather than a list
|
||||||
|
|
||||||
class BaseParser(object):
|
class BaseParser(object):
|
||||||
media_types = ()
|
media_type = None
|
||||||
|
|
||||||
def __init__(self, resource):
|
def __init__(self, resource):
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
|
@ -18,7 +18,7 @@ class BaseParser(object):
|
||||||
|
|
||||||
|
|
||||||
class JSONParser(BaseParser):
|
class JSONParser(BaseParser):
|
||||||
media_types = ('application/xml',)
|
media_type = 'application/json'
|
||||||
|
|
||||||
def parse(self, input):
|
def parse(self, input):
|
||||||
try:
|
try:
|
||||||
|
@ -27,7 +27,7 @@ class JSONParser(BaseParser):
|
||||||
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
|
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
|
||||||
|
|
||||||
class XMLParser(BaseParser):
|
class XMLParser(BaseParser):
|
||||||
media_types = ('application/xml',)
|
media_type = 'application/xml'
|
||||||
|
|
||||||
|
|
||||||
class FormParser(BaseParser):
|
class FormParser(BaseParser):
|
||||||
|
@ -35,7 +35,7 @@ class FormParser(BaseParser):
|
||||||
Return a dict containing a single value for each non-reserved parameter.
|
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):
|
def parse(self, input):
|
||||||
# The FormParser doesn't parse the input as other parsers would, since Django's already done the
|
# The FormParser doesn't parse the input as other parsers would, since Django's already done the
|
||||||
|
|
|
@ -120,14 +120,16 @@ class Resource(object):
|
||||||
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
|
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
|
||||||
return self.emitters[0]
|
return self.emitters[0]
|
||||||
|
|
||||||
# TODO:
|
@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 parsed_media_types(self):
|
@property
|
||||||
# """Return an list of all the media types that this resource can emit."""
|
def default_parser(self):
|
||||||
# return [parser.media_type for parser in self.parsers]
|
"""Return the resource's most prefered emitter.
|
||||||
|
(This has no behavioural effect, but is may be used by documenting emitters)"""
|
||||||
#def default_parser(self):
|
return self.parsers[0]
|
||||||
# return self.parsers[0]
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(self, view, *args, **kwargs):
|
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,
|
"""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."""
|
and the content types that this Resource knows how to parse."""
|
||||||
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
|
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
|
||||||
|
raw_content = request.raw_post_data
|
||||||
|
|
||||||
split = content_type.split(';', 1)
|
split = content_type.split(';', 1)
|
||||||
if len(split) > 1:
|
if len(split) > 1:
|
||||||
content_type = split[0]
|
content_type = split[0]
|
||||||
content_type = content_type.strip()
|
content_type = content_type.strip()
|
||||||
|
|
||||||
# Create a list of list of (media_type, Parser) tuples
|
# If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
|
||||||
media_type_parser_tuples = [[(media_type, parser) for media_type in parser.media_types] for parser in self.parsers]
|
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]
|
||||||
|
|
||||||
# Flatten the list and turn it into a media_type -> Parser dict
|
# Create a list of list of (media_type, Parser) tuples
|
||||||
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:
|
try:
|
||||||
return media_type_to_parser[content_type]
|
return (media_type_to_parser[content_type], raw_content)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||||
{'detail': 'Unsupported media type \'%s\'' % content_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
|
# 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.
|
# TODO: Add support for message bodys on other HTTP methods, as it is valid.
|
||||||
if method in ('PUT', 'POST'):
|
if method in ('PUT', 'POST'):
|
||||||
parser = self.determine_parser(request)
|
(parser, raw_content) = self.determine_parser(request)
|
||||||
data = parser(self).parse(request.raw_post_data)
|
data = parser(self).parse(raw_content)
|
||||||
self.form_instance = self.get_form(data)
|
self.form_instance = self.get_form(data)
|
||||||
data = self.cleanup_request(data, self.form_instance)
|
data = self.cleanup_request(data, self.form_instance)
|
||||||
response = func(request, auth_context, data, *args, **kwargs)
|
response = func(request, auth_context, data, *args, **kwargs)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user