diff options
| author | tom christie tom@tomchristie.com | 2011-02-19 10:26:27 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-02-19 10:26:27 +0000 |
| commit | 805aa03ec1871f6a766d9052b348ddce9e9843c3 (patch) | |
| tree | 8ab5b6a7396236aa45bbc61e8404cc77fc75a9c5 /djangorestframework/emitters.py | |
| parent | b749b950a1b4bede76b7e3900a6385779904902d (diff) | |
| download | django-rest-framework-805aa03ec1871f6a766d9052b348ddce9e9843c3.tar.bz2 | |
Yowzers. Final big bunch of refactoring for 0.1 release. Now support Django 1.3's views, admin style api is all polished off, loads of tests, new test project for running the test. All sorts of goodness. Getting ready to push this out now.
Diffstat (limited to 'djangorestframework/emitters.py')
| -rw-r--r-- | djangorestframework/emitters.py | 180 |
1 files changed, 165 insertions, 15 deletions
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py index d6bb33c1..e6129b4f 100644 --- a/djangorestframework/emitters.py +++ b/djangorestframework/emitters.py @@ -4,21 +4,145 @@ by serializing the output along with documentation regarding the Resource, outpu and providing forms and links depending on the allowed methods, emitters and parsers on the Resource. """ from django.conf import settings +from django.http import HttpResponse from django.template import RequestContext, loader from django import forms -from djangorestframework.response import NoContent +from djangorestframework.response import NoContent, ResponseException, status from djangorestframework.validators import FormValidatorMixin from djangorestframework.utils import dict2xml, url_resolves +from djangorestframework.markdownwrapper import apply_markdown +from djangorestframework.breadcrumbs import get_breadcrumbs +from djangorestframework.content import OverloadedContentMixin +from djangorestframework.description import get_name, get_description from urllib import quote_plus import string +import re +from decimal import Decimal + try: import json except ImportError: import simplejson as json +_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') + + +class EmitterMixin(object): + ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params + REWRITE_IE_ACCEPT_HEADER = True + + request = None + response = None + emitters = () + + def emit(self, response): + self.response = response + + try: + emitter = self._determine_emitter(self.request) + except ResponseException, exc: + emitter = self.default_emitter + response = exc.response + + # Serialize the response content + if response.has_content_body: + content = emitter(self).emit(output=response.cleaned_content) + else: + content = emitter(self).emit() + + # Munge DELETE Response code to allow us to return content + # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output) + if response.status == 204: + response.status = 200 + + # Build the HTTP Response + # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set + resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status) + for (key, val) in response.headers.items(): + resp[key] = val + + return resp + + + def _determine_emitter(self, request): + """Return the appropriate emitter for the output, given the client's 'Accept' header, + and the content types that this Resource knows how to serve. + + See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html""" + + if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None): + # Use _accept parameter override + accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)] + elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']): + accept_list = ['text/html', '*/*'] + elif request.META.has_key('HTTP_ACCEPT'): + # Use standard HTTP Accept negotiation + accept_list = request.META["HTTP_ACCEPT"].split(',') + else: + # No accept header specified + return self.default_emitter + + # Parse the accept header into a dict of {qvalue: set of media types} + # We ignore mietype parameters + accept_dict = {} + for token in accept_list: + components = token.split(';') + mimetype = components[0].strip() + qvalue = Decimal('1.0') + + if len(components) > 1: + # Parse items that have a qvalue eg text/html;q=0.9 + try: + (q, num) = components[-1].split('=') + if q == 'q': + qvalue = Decimal(num) + except: + # Skip malformed entries + continue + + if accept_dict.has_key(qvalue): + accept_dict[qvalue].add(mimetype) + else: + accept_dict[qvalue] = set((mimetype,)) + + # Convert to a list of sets ordered by qvalue (highest first) + accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)] + + for accept_set in accept_sets: + # Return any exact match + for emitter in self.emitters: + if emitter.media_type in accept_set: + return emitter + + # Return any subtype match + for emitter in self.emitters: + if emitter.media_type.split('/')[0] + '/*' in accept_set: + return emitter + + # Return default + if '*/*' in accept_set: + return self.default_emitter + + + raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE, + {'detail': 'Could not statisfy the client\'s Accept header', + 'available_types': self.emitted_media_types}) + + @property + def emitted_media_types(self): + """Return an list of all the media types that this resource can emit.""" + return [emitter.media_type for emitter in self.emitters] + + @property + def default_emitter(self): + """Return the resource's most prefered emitter. + (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)""" + return self.emitters[0] + + # TODO: Rename verbose to something more appropriate # TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default, @@ -51,7 +175,7 @@ class TemplateEmitter(BaseEmitter): if output is NoContent: return '' - context = RequestContext(self.resource.request, output) + context = RequestContext(self.request, output) return self.template.render(context) @@ -60,7 +184,7 @@ class DocumentingTemplateEmitter(BaseEmitter): Implementing classes should extend this class and set the template attribute.""" template = None - def _get_content(self, resource, output): + def _get_content(self, resource, request, 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 @@ -88,21 +212,24 @@ class DocumentingTemplateEmitter(BaseEmitter): form_instance = None - if isinstance(self, FormValidatorMixin): - # Otherwise if this isn't an error response - # then attempt to get a form bound to the response object + if isinstance(resource, FormValidatorMixin): + # If we already have a bound form instance (IE provided by the input parser, then use that) + if resource.bound_form_instance is not None: + form_instance = resource.bound_form_instance + + # Otherwise if we have a response that is valid against the form then use that if not form_instance and resource.response.has_content_body: try: form_instance = resource.get_bound_form(resource.response.raw_content) - if form_instance: - form_instance.is_valid() + if form_instance and not form_instance.is_valid(): + form_instance = None except: form_instance = None # If we still don't have a form instance then try to get an unbound form if not form_instance: try: - form_instance = self.resource.get_bound_form() + form_instance = resource.get_bound_form() except: pass @@ -117,6 +244,11 @@ class DocumentingTemplateEmitter(BaseEmitter): """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms (Which are typically application/x-www-form-urlencoded)""" + # If we're not using content overloading there's no point in supplying a generic form, + # as the resource won't treat the form's value as the content of the request. + if not isinstance(resource, OverloadedContentMixin): + return None + # NB. http://jacobian.org/writing/dynamic-form-generation/ class GenericContentForm(forms.Form): def __init__(self, resource): @@ -143,7 +275,7 @@ class DocumentingTemplateEmitter(BaseEmitter): def emit(self, output=NoContent): - content = self._get_content(self.resource, output) + content = self._get_content(self.resource, self.resource.request, output) form_instance = self._get_form_instance(self.resource) if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): @@ -153,24 +285,36 @@ class DocumentingTemplateEmitter(BaseEmitter): login_url = None logout_url = None + name = get_name(self.resource) + description = get_description(self.resource) + + markeddown = None + if apply_markdown: + try: + markeddown = apply_markdown(description) + except AttributeError: # TODO: possibly split the get_description / get_name into a mixin class + markeddown = None + + breadcrumb_list = get_breadcrumbs(self.resource.request.path) + template = loader.get_template(self.template) context = RequestContext(self.resource.request, { 'content': content, 'resource': self.resource, 'request': self.resource.request, 'response': self.resource.response, + 'description': description, + 'name': name, + 'markeddown': markeddown, + 'breadcrumblist': breadcrumb_list, 'form': form_instance, 'login_url': login_url, 'logout_url': logout_url, + 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX }) ret = template.render(context) - # Munge DELETE Response code to allow us to return content - # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output) - if self.resource.response.status == 204: - self.resource.response.status = 200 - return ret @@ -217,5 +361,11 @@ class DocumentingPlainTextEmitter(DocumentingTemplateEmitter): Useful for browsing an API with command line tools.""" media_type = 'text/plain' template = 'emitter.txt' + +DEFAULT_EMITTERS = ( JSONEmitter, + DocumentingHTMLEmitter, + DocumentingXHTMLEmitter, + DocumentingPlainTextEmitter, + XMLEmitter ) |
