import collections import mimetypes import mimeparse from django.conf import settings from django.shortcuts import render_to_response from django.http import HttpResponse from .utils.decorators import wrap_object from .exceptions import HttpError from .utils.serializers import to_json, to_html, to_text DEFAULT_MIMETYPES = { 'application/json': to_json, 'text/html': to_html, 'text/plain': to_text } class RESTfulResponse(collections.MutableMapping, collections.Callable): """ Can be used as a decorator or an instance to properly formatted content This class provides content negotiation for RESTful requests. The content- type of a response is determined by the ACCEPT header of the reqeust or by an overriding file extension in the URL (e.g., path/to/some/resource.xml) will return the response formatted as XML. This class creates an instance that can be used directly to transform a python dict into a properly formatted response via the render_to_response method or it can be used as a decorator for class-based views or for an individual method within a class-based view. If a requested mimetype is not found amongst the supported mimetypes, the content-type of the response will default to 'application/json'. This class is inspired by an excellent blog post from James Bennett. See http://www.b-list.org/weblog/2008/nov/29/multiresponse/ for more information. """ def __init__(self, mimetype_mapping=None): self._mimetypes = {} if mimetype_mapping: self._mimetypes.update(mimetype_mapping) def __len__(self): return len(self.keys()) def __iter__(self): for key in self.keys(): yield key def __getitem__(self, mimetype): if mimetype in self._mimetypes: return self._mimetypes[mimetype] else: return DEFAULT_MIMETYPES[mimetype] def __setitem__(self, mimetype, func_or_templ): self._mimetypes[mimetype] = func_or_templ def __delitem__(self, mimetype): del self._mimetypes[mimetype] def keys(self): return list(set(self._mimetypes.keys()) | set(DEFAULT_MIMETYPES.keys())) def __call__(self, view_obj): def decorator(view_func): def wrapper(request, *args, **kwargs): try: results = view_func(request, *args, **kwargs) except HttpError, e: results = ( e.message and {'error': e.message} or None, e.status ) # TODO: What should be done about a resource that returns a normal # Django HttpResponse? Right now, if an HttpResponse is # returned, it is allowed to propogate. In other words, it # acts just as it would if content negotiation wasn't being # used. Another option would be to extract the content and # status code from the HttpResponse object and pass those # into the render_to_response method. if isinstance(results, HttpResponse): return results # Get the status code, if one was provided if isinstance(results, collections.Sequence) and len(results) == 2: try: data, status_code = results[0], int(results[1]) except Exception: data, status_code = results, 200 else: data, status_code = results, 200 response = self.render_to_response(request, data, status_code, kwargs.get('_format', None)) return response return wrapper return wrap_object(view_obj, decorator) def render_to_response(self, request, data=None, status=200, format=None): format = request.REQUEST.get('_format', None) or format mimetype = mimeparse.best_match(self.keys(), request.META.get('HTTP_ACCEPT', '')) mimetype = mimetypes.guess_type('placeholder_filename.%s' % format)[0] or mimetype content_type = '%s; charset=%s' % (mimetype, settings.DEFAULT_CHARSET) templ_or_func = self.get(mimetype) # If a template or function isn't found, return a 415 (unsupportted media type) response if not templ_or_func: return HttpResponse(status=415) if data is None: response = HttpResponse() elif isinstance(templ_or_func, str): response = render_to_response(templ_or_func, {'context': data}) else: response = HttpResponse(templ_or_func(data)) response['Content-Type'] = content_type response.status_code = status return response