diff options
| -rw-r--r-- | examples/objectstore/views.py | 14 | ||||
| -rw-r--r-- | examples/urls.py | 1 | ||||
| -rw-r--r-- | flywheel/emitters.py | 69 | ||||
| -rw-r--r-- | flywheel/parsers.py | 8 | ||||
| -rw-r--r-- | flywheel/resource.py | 39 |
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) |
