From 5557dfb54c8aeb35a781760fc2fb8334e53e98ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Jan 2011 12:34:57 +0000 Subject: [PATCH] Various cleanups --- src/rest/emitters.py | 37 +++++++++----- src/rest/parsers.py | 23 +++++---- src/rest/resource.py | 115 ++++++++++++++++++++++--------------------- 3 files changed, 93 insertions(+), 82 deletions(-) diff --git a/src/rest/emitters.py b/src/rest/emitters.py index 87faa559b..dcbaf7b7d 100644 --- a/src/rest/emitters.py +++ b/src/rest/emitters.py @@ -4,12 +4,8 @@ import json from utils import dict2xml class BaseEmitter(object): - def __init__(self, resource, request, status, headers, form): - self.request = request + def __init__(self, resource): self.resource = resource - self.status = status - self.headers = headers - self.form = form def emit(self, output): return output @@ -18,28 +14,43 @@ class TemplatedEmitter(BaseEmitter): template = None def emit(self, output): - content = json.dumps(output, indent=4, sort_keys=True) + if output is None: + content = '' + else: + content = json.dumps(output, indent=4, sort_keys=True) + template = loader.get_template(self.template) - context = RequestContext(self.request, { + context = RequestContext(self.resource.request, { 'content': content, - 'status': self.status, - 'reason': STATUS_CODE_TEXT.get(self.status, ''), - 'headers': self.headers, + 'status': self.resource.resp_status, + 'reason': STATUS_CODE_TEXT.get(self.resource.resp_status, ''), + 'headers': self.resource.resp_headers, 'resource_name': self.resource.__class__.__name__, 'resource_doc': self.resource.__doc__, - 'create_form': self.form, - 'update_form': self.form, - 'request': self.request, + 'create_form': self.resource.form, + 'update_form': self.resource.form, + 'request': self.resource.request, 'resource': self.resource, }) + + # Munge DELETE Response code to allow us to return content + if self.resource.resp_status == 204: + self.resource.resp_status = 200 + return template.render(context) class JSONEmitter(BaseEmitter): def emit(self, output): + if output is None: + # Treat None as no message body, rather than serializing + return '' return json.dumps(output) class XMLEmitter(BaseEmitter): def emit(self, output): + if output is None: + # Treat None as no message body, rather than serializing + return '' return dict2xml(output) class HTMLEmitter(TemplatedEmitter): diff --git a/src/rest/parsers.py b/src/rest/parsers.py index 85b0e51f7..73073243f 100644 --- a/src/rest/parsers.py +++ b/src/rest/parsers.py @@ -1,9 +1,8 @@ import json class BaseParser(object): - def __init__(self, resource, request): + def __init__(self, resource): self.resource = resource - self.request = request def parse(self, input): return {} @@ -20,9 +19,9 @@ class FormParser(BaseParser): """The default parser for form data. Return a dict containing a single value for each non-reserved parameter """ - def __init__(self, resource, request): + def __init__(self, resource): - if request.method == 'PUT': + if resource.request.method == 'PUT': # Fix from piston to force Django to give PUT requests the same # form processing that POST requests get... # @@ -36,22 +35,22 @@ class FormParser(BaseParser): # the first time _load_post_and_files is called (both by wsgi.py and # modpython.py). If it's set, the request has to be 'reset' to redo # the query value parsing in POST mode. - if hasattr(request, '_post'): + if hasattr(resource.request, '_post'): del request._post del request._files try: - request.method = "POST" - request._load_post_and_files() - request.method = "PUT" + resource.request.method = "POST" + resource.request._load_post_and_files() + resource.request.method = "PUT" except AttributeError: - request.META['REQUEST_METHOD'] = 'POST' - request._load_post_and_files() - request.META['REQUEST_METHOD'] = 'PUT' + resource.request.META['REQUEST_METHOD'] = 'POST' + resource.request._load_post_and_files() + resource.request.META['REQUEST_METHOD'] = 'PUT' # self.data = {} - for (key, val) in request.POST.items(): + for (key, val) in resource.request.POST.items(): if key not in resource.RESERVED_PARAMS: self.data[key] = val diff --git a/src/rest/resource.py b/src/rest/resource.py index c57ebfad9..15ccce48b 100644 --- a/src/rest/resource.py +++ b/src/rest/resource.py @@ -57,7 +57,7 @@ class Resource(object): """Make the class callable so it can be used as a Django view.""" self = object.__new__(cls) self.__init__() - self._request = request + self.request = request try: return self._handle_request(request, *args, **kwargs) except: @@ -76,7 +76,7 @@ class Resource(object): TODO: Add SITEMAP option. Provided for convienience.""" - return self._request.build_absolute_uri(reverse(view, *args, **kwargs)) + return self.request.build_absolute_uri(reverse(view, *args, **kwargs)) def make_absolute(self, uri): @@ -84,7 +84,7 @@ class Resource(object): TODO: Add SITEMAP option. Provided for convienience.""" - return self._request.build_absolute_uri(uri) + return self.request.build_absolute_uri(uri) def read(self, headers={}, *args, **kwargs): @@ -137,36 +137,41 @@ class Resource(object): - def determine_form(self, input_data=None, return_data=None): + def determine_form(self, data=None, is_response=False): """Optionally return a Django Form instance, which may be used for validation and/or rendered by an HTML/XHTML emitter. - The input_data or return_data arguments can be used to bind the form either to the deserialized input, - or to a return object. + If data is not None the form will be bound to data. is_response indicates if data should be + treated as the input data (bind to client input) or the response data (bind to an existing object). """ if self.form: - if input_data: - return self.form(input_data) - elif return_data: - return self.form(return_data) + if data: + return self.form(data) else: return self.form() return None - def cleanup_request(self, data, form=None): + def cleanup_request(self, data): """Perform any resource-specific data deserialization and/or validation after the initial HTTP content-type deserialization has taken place. - Optionally this may use a Django Form which will have been bound to the data, - rather than using the data directly. - """ - return data + By default this uses form validation to filter the basic input into the required types.""" + if self.form is None: + return data + + if not self.form.is_valid(): + details = dict((key, map(unicode, val)) for (key, val) in self.form.errors.iteritems()) + raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details}) + + return self.form.cleaned_data def cleanup_response(self, data): """Perform any resource-specific data filtering prior to the standard HTTP - content-type serialization.""" + content-type serialization. + + Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.""" return data @@ -247,53 +252,57 @@ class Resource(object): 4. cleanup the response data 5. serialize response data into response content, using standard HTTP content negotiation """ - method = self.determine_method(request) emitter = None - form = None + + # We make these attributes to allow for a certain amount of munging, + # eg The HTML emitter needs to render this information + self.method = self.determine_method(request) + self.form = None + self.resp_status = None + self.resp_content = None + self.resp_headers = {} + try: # Before we attempt anything else determine what format to emit our response data with. mimetype, emitter = self.determine_emitter(request) # Ensure the requested operation is permitted on this resource - self.check_method_allowed(method) + self.check_method_allowed(self.method) # Get the appropriate create/read/update/delete function - func = getattr(self, self.CALLMAP.get(method, '')) + func = getattr(self, self.CALLMAP.get(self.method, '')) # Either generate the response data, deserializing and validating any request data - if method in ('PUT', 'POST'): + if self.method in ('PUT', 'POST'): parser = self.determine_parser(request) - data = parser(self, request).parse(request.raw_post_data) - form = self.determine_form(input_data=data) - data = self.cleanup_request(data, form) - (status, ret, headers) = func(data, request.META, *args, **kwargs) + data = parser(self).parse(request.raw_post_data) + self.form = self.determine_form(data) + data = self.cleanup_request(data) + (self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs) else: - (status, ret, headers) = func(request.META, *args, **kwargs) - form = self.determine_form(return_data=ret) + (self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs) + self.form = self.determine_form(ret, is_response=True) except ResourceException, exc: - (status, ret, headers) = (exc.status, exc.content, exc.headers) + (self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers) + if emitter is None: + mimetype, emitter = self.emitters[0] + if self.form is None: + self.form = self.determine_form() - # Use a default emitter if request failed without being able to determine an acceptable emitter - if emitter is None: - mimetype, emitter = self.emitters[0] - - # Create an unbound form if one has not yet been created - if form is None: - form = self.determine_form() # Always add the allow header - headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations]) + self.resp_headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations]) # Serialize the response content ret = self.cleanup_response(ret) - content = emitter(self, request, status, headers, form).emit(ret) + content = emitter(self).emit(ret) # Build the HTTP Response - resp = HttpResponse(content, mimetype=mimetype, status=status) - for (key, val) in headers.items(): + resp = HttpResponse(content, mimetype=mimetype, status=self.resp_status) + for (key, val) in self.resp_headers.items(): resp[key] = val return resp @@ -313,10 +322,10 @@ class ModelResource(Resource): fields = None form_fields = None - def determine_form(self, input_data=None, return_data=None): + def determine_form(self, data=None, is_response=False): """Return a form that may be used in validation and/or rendering an html emitter""" if self.form: - return self.form + return super(self.__class__, self).determine_form(data, is_response=is_response) elif self.model: class NewModelForm(ModelForm): @@ -324,26 +333,17 @@ class ModelResource(Resource): model = self.model fields = self.form_fields if self.form_fields else None #self.fields - if input_data: - return NewModelForm(input_data) - elif return_data: - return NewModelForm(instance=return_data) + if data and not is_response: + return NewModelForm(data) + elif data and is_response: + return NewModelForm(instance=data) else: return NewModelForm() else: return None - def cleanup_request(self, data, form=None): - """Filter data into form-cleaned data, performing validation and type coercion.""" - if form is None: - return data - if not form.is_valid(): - details = dict((key, map(unicode, val)) for (key, val) in form.errors.iteritems()) - raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details}) - - return form.cleaned_data def cleanup_response(self, data): """ @@ -614,7 +614,7 @@ class ModelResource(Resource): try: instance = self.model.objects.get(**kwargs) except self.model.DoesNotExist: - return (404, '', {}) + return (404, None, {}) return (200, instance, {}) @@ -633,13 +633,14 @@ class ModelResource(Resource): def delete(self, headers={}, *args, **kwargs): instance = self.model.objects.get(**kwargs) instance.delete() - return (204, '', {}) + return (204, None, {}) + class QueryModelResource(ModelResource): allowed_methods = ('read',) - def determine_form(self, input_data=None, return_data=None): + def determine_form(self, data=None, is_response=False): return None def read(self, headers={}, *args, **kwargs):