mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +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):
|
||||
"""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):
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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.
|
||||
|
||||
# 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)]
|
||||
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, {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
@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):
|
||||
# """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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user