diff options
| author | tom christie tom@tomchristie.com | 2011-01-26 21:06:40 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-01-26 21:06:40 +0000 |
| commit | 9adb965126366bfe4b364357f565baabd819c982 (patch) | |
| tree | f924a64d0c4c8ff023f92fd9ef035060c825f647 /flywheel | |
| parent | 6807cf014cb0fde611f63c64bc352038206176cc (diff) | |
| parent | 8b89d7416cf4e2396deac4ba41c23cdcdc8b9704 (diff) | |
| download | django-rest-framework-9adb965126366bfe4b364357f565baabd819c982.tar.bz2 | |
content type tunneling
Diffstat (limited to 'flywheel')
| -rw-r--r-- | flywheel/emitters.py | 69 | ||||
| -rw-r--r-- | flywheel/parsers.py | 8 | ||||
| -rw-r--r-- | flywheel/resource.py | 41 |
3 files changed, 87 insertions, 31 deletions
diff --git a/flywheel/emitters.py b/flywheel/emitters.py index 33375200..408a65eb 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: @@ -33,25 +35,31 @@ class TemplateEmitter(BaseEmitter): return self.template.render(Context(output)) -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 @@ -70,8 +78,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 57218cc6..98232a96 100644 --- a/flywheel/parsers.py +++ b/flywheel/parsers.py @@ -11,7 +11,7 @@ class BaseParser(object): """All parsers should extend BaseParser, specifing a media_type attribute, and overriding the parse() method.""" - media_types = () + media_type = None def __init__(self, resource): """Initialise the parser with the Resource instance as state, @@ -26,7 +26,7 @@ class BaseParser(object): class JSONParser(BaseParser): - media_types = ('application/xml',) + media_type = 'application/json' def parse(self, input): try: @@ -36,7 +36,7 @@ class JSONParser(BaseParser): class XMLParser(BaseParser): - media_types = ('application/xml',) + media_type = 'application/xml' class FormParser(BaseParser): @@ -44,7 +44,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 509b102c..c15f01ed 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] - #@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] - # - #@property - #def default_parser(self): - # return self.parsers[0] + @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] + + @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 get(self, request, auth, *args, **kwargs): @@ -285,19 +287,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}) @@ -402,8 +413,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) |
