From b358fbdbe9cbd4ce644c4b2c7b9b4cec0811e14e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 29 Apr 2011 14:32:56 +0100 Subject: [PATCH] More refactoring - move various less core stuff into utils etc --- .../{authenticators.py => authentication.py} | 10 +-- djangorestframework/compat.py | 66 ++++++++++++-- djangorestframework/markdownwrapper.py | 51 ----------- djangorestframework/mixins.py | 10 +-- djangorestframework/parsers.py | 2 +- .../{emitters.py => renderers.py} | 86 +++++++++---------- djangorestframework/resource.py | 6 +- .../templates/{emitter.html => renderer.html} | 2 +- .../templates/{emitter.txt => renderer.txt} | 0 djangorestframework/tests/breadcrumbs.py | 2 +- djangorestframework/tests/description.py | 4 +- djangorestframework/tests/emitters.py | 76 ---------------- djangorestframework/tests/parsers.py | 2 +- djangorestframework/tests/renderers.py | 76 ++++++++++++++++ .../{utils.py => utils/__init__.py} | 11 ++- .../{ => utils}/breadcrumbs.py | 3 +- .../{ => utils}/description.py | 0 djangorestframework/{ => utils}/mediatypes.py | 0 18 files changed, 203 insertions(+), 204 deletions(-) rename djangorestframework/{authenticators.py => authentication.py} (87%) delete mode 100644 djangorestframework/markdownwrapper.py rename djangorestframework/{emitters.py => renderers.py} (75%) rename djangorestframework/templates/{emitter.html => renderer.html} (98%) rename djangorestframework/templates/{emitter.txt => renderer.txt} (100%) delete mode 100644 djangorestframework/tests/emitters.py create mode 100644 djangorestframework/tests/renderers.py rename djangorestframework/{utils.py => utils/__init__.py} (98%) rename djangorestframework/{ => utils}/breadcrumbs.py (90%) rename djangorestframework/{ => utils}/description.py (100%) rename djangorestframework/{ => utils}/mediatypes.py (100%) diff --git a/djangorestframework/authenticators.py b/djangorestframework/authentication.py similarity index 87% rename from djangorestframework/authenticators.py rename to djangorestframework/authentication.py index 74d9931ae..894b34fca 100644 --- a/djangorestframework/authenticators.py +++ b/djangorestframework/authentication.py @@ -1,8 +1,8 @@ -"""The :mod:`authenticators` modules provides for pluggable authentication behaviour. +"""The :mod:`authentication` modules provides for pluggable authentication behaviour. Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.Resource` or Django :class:`View` class. -The set of authenticators which are use is then specified by setting the :attr:`authenticators` attribute on the class, and listing a set of authenticator classes. +The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes. """ from django.contrib.auth import authenticate from django.middleware.csrf import CsrfViewMiddleware @@ -11,11 +11,11 @@ import base64 class BaseAuthenticator(object): - """All authenticators should extend BaseAuthenticator.""" + """All authentication should extend BaseAuthenticator.""" def __init__(self, view): - """Initialise the authenticator with the mixin instance as state, - in case the authenticator needs to access any metadata on the mixin object.""" + """Initialise the authentication with the mixin instance as state, + in case the authentication needs to access any metadata on the mixin object.""" self.view = view def authenticate(self, request): diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 22b571861..98fbbb62c 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -1,9 +1,24 @@ """Compatability module to provide support for backwards compatability with older versions of django/python""" +# cStringIO only if it's available +try: + import cStringIO as StringIO +except ImportError: + import StringIO + + +# parse_qs +try: + # python >= ? + from urlparse import parse_qs +except ImportError: + # python <= ? + from cgi import parse_qs + + # django.test.client.RequestFactory (Django >= 1.3) try: from django.test.client import RequestFactory - except ImportError: from django.test import Client from django.core.handlers.wsgi import WSGIRequest @@ -49,7 +64,7 @@ except ImportError: # django.views.generic.View (Django >= 1.3) try: from django.views.generic import View -except: +except ImportError: from django import http from django.utils.functional import update_wrapper # from django.utils.log import getLogger @@ -127,10 +142,47 @@ except: #) return http.HttpResponseNotAllowed(allowed_methods) -# parse_qs + try: - # python >= ? - from urlparse import parse_qs + import markdown + import re + + class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor): + """Override markdown's SetextHeaderProcessor, so that ==== headers are

and ---- headers are

. + + We use

for the resource name.""" + + # Detect Setext-style header. Must be first 2 lines of block. + RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE) + + def test(self, parent, block): + return bool(self.RE.match(block)) + + def run(self, parent, blocks): + lines = blocks.pop(0).split('\n') + # Determine level. ``=`` is 1 and ``-`` is 2. + if lines[1].startswith('='): + level = 2 + else: + level = 3 + h = markdown.etree.SubElement(parent, 'h%d' % level) + h.text = lines[0].strip() + if len(lines) > 2: + # Block contains additional lines. Add to master blocks for later. + blocks.insert(0, '\n'.join(lines[2:])) + + def apply_markdown(text): + """Simple wrapper around markdown.markdown to apply our CustomSetextHeaderProcessor, + and also set the base level of '#' style headers to

.""" + extensions = ['headerid(level=2)'] + safe_mode = False, + output_format = markdown.DEFAULT_OUTPUT_FORMAT + + md = markdown.Markdown(extensions=markdown.load_extensions(extensions), + safe_mode=safe_mode, + output_format=output_format) + md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser) + return md.convert(text) + except ImportError: - # python <= ? - from cgi import parse_qs \ No newline at end of file + apply_markdown = None \ No newline at end of file diff --git a/djangorestframework/markdownwrapper.py b/djangorestframework/markdownwrapper.py deleted file mode 100644 index 70512440b..000000000 --- a/djangorestframework/markdownwrapper.py +++ /dev/null @@ -1,51 +0,0 @@ -"""If python-markdown is installed expose an apply_markdown(text) function, -to convert markeddown text into html. Otherwise just set apply_markdown to None. - -See: http://www.freewisdom.org/projects/python-markdown/ -""" - -__all__ = ['apply_markdown'] - -try: - import markdown - import re - - class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor): - """Override markdown's SetextHeaderProcessor, so that ==== headers are

and ---- headers are

. - - We use

for the resource name.""" - - # Detect Setext-style header. Must be first 2 lines of block. - RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE) - - def test(self, parent, block): - return bool(self.RE.match(block)) - - def run(self, parent, blocks): - lines = blocks.pop(0).split('\n') - # Determine level. ``=`` is 1 and ``-`` is 2. - if lines[1].startswith('='): - level = 2 - else: - level = 3 - h = markdown.etree.SubElement(parent, 'h%d' % level) - h.text = lines[0].strip() - if len(lines) > 2: - # Block contains additional lines. Add to master blocks for later. - blocks.insert(0, '\n'.join(lines[2:])) - - def apply_markdown(text): - """Simple wrapper around markdown.markdown to apply our CustomSetextHeaderProcessor, - and also set the base level of '#' style headers to

.""" - extensions = ['headerid(level=2)'] - safe_mode = False, - output_format = markdown.DEFAULT_OUTPUT_FORMAT - - md = markdown.Markdown(extensions=markdown.load_extensions(extensions), - safe_mode=safe_mode, - output_format=output_format) - md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser) - return md.convert(text) - -except: - apply_markdown = None \ No newline at end of file diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 43b33f504..ebeee31a7 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -1,4 +1,4 @@ -from djangorestframework.mediatypes import MediaType +from djangorestframework.utils.mediatypes import MediaType from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX from djangorestframework.response import ErrorResponse from djangorestframework.parsers import FormParser, MultipartParser @@ -397,7 +397,7 @@ class ResponseMixin(object): class AuthMixin(object): """Mixin class to provide authentication and permission checking.""" - authenticators = () + authentication = () permissions = () @property @@ -407,9 +407,9 @@ class AuthMixin(object): return self._auth def _authenticate(self): - for authenticator_cls in self.authenticators: - authenticator = authenticator_cls(self) - auth = authenticator.authenticate(self.request) + for authentication_cls in self.authentication: + authentication = authentication_cls(self) + auth = authentication.authenticate(self.request) if auth: return auth return None diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 96b29a66b..6d6bd5cef 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -14,7 +14,7 @@ from django.utils import simplejson as json from djangorestframework.response import ErrorResponse from djangorestframework import status from djangorestframework.utils import as_tuple -from djangorestframework.mediatypes import MediaType +from djangorestframework.utils.mediatypes import MediaType from djangorestframework.compat import parse_qs diff --git a/djangorestframework/emitters.py b/djangorestframework/renderers.py similarity index 75% rename from djangorestframework/emitters.py rename to djangorestframework/renderers.py index 87b3e94e2..e53dc061a 100644 --- a/djangorestframework/emitters.py +++ b/djangorestframework/renderers.py @@ -1,7 +1,7 @@ -"""Emitters are used to serialize a Resource's output into specific media types. -django-rest-framework also provides HTML and PlainText emitters that help self-document the API, +"""Renderers are used to serialize a Resource's output into specific media types. +django-rest-framework also provides HTML and PlainText renderers that help self-document the API, by serializing the output along with documentation regarding the Resource, output status and headers, -and providing forms and links depending on the allowed methods, emitters and parsers on the Resource. +and providing forms and links depending on the allowed methods, renderers and parsers on the Resource. """ from django import forms from django.conf import settings @@ -10,9 +10,9 @@ from django.utils import simplejson as json from django import forms from djangorestframework.utils import dict2xml, url_resolves -from djangorestframework.markdownwrapper import apply_markdown -from djangorestframework.breadcrumbs import get_breadcrumbs -from djangorestframework.description import get_name, get_description +from djangorestframework.compat import apply_markdown +from djangorestframework.utils.breadcrumbs import get_breadcrumbs +from djangorestframework.utils.description import get_name, get_description from djangorestframework import status from urllib import quote_plus @@ -22,18 +22,18 @@ from decimal import Decimal # TODO: Rename verbose to something more appropriate # TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default, -# and only have an emitter output anything if it explicitly provides support for that. +# and only have an renderer output anything if it explicitly provides support for that. -class BaseEmitter(object): - """All emitters must extend this class, set the media_type attribute, and - override the emit() function.""" +class BaseRenderer(object): + """All renderers must extend this class, set the media_type attribute, and + override the render() function.""" media_type = None def __init__(self, resource): self.resource = resource - def emit(self, output=None, verbose=False): - """By default emit simply returns the ouput as-is. + def render(self, output=None, verbose=False): + """By default render simply returns the ouput as-is. Override this method to provide for other behaviour.""" if output is None: return '' @@ -41,13 +41,13 @@ class BaseEmitter(object): return output -class TemplateEmitter(BaseEmitter): +class TemplateRenderer(BaseRenderer): """Provided for convienience. Emit the output by simply rendering it with the given template.""" media_type = None template = None - def emit(self, output=None, verbose=False): + def render(self, output=None, verbose=False): if output is None: return '' @@ -55,23 +55,23 @@ class TemplateEmitter(BaseEmitter): return self.template.render(context) -class DocumentingTemplateEmitter(BaseEmitter): - """Base class for emitters used to self-document the API. +class DocumentingTemplateRenderer(BaseRenderer): + """Base class for renderers used to self-document the API. Implementing classes should extend this class and set the template attribute.""" template = None def _get_content(self, resource, request, output): - """Get the content as if it had been emitted by a non-documenting emitter. + """Get the content as if it had been renderted by a non-documenting renderer. (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 use another documenting emitter.) - emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)] - if not emitters: - return '[No emitters were found]' + # Find the first valid renderer and render the content. (Don't use another documenting renderer.) + renderers = [renderer for renderer in resource.renderers if not isinstance(renderer, DocumentingTemplateRenderer)] + if not renderers: + return '[No renderers were found]' - content = emitters[0](resource).emit(output, verbose=True) + content = renderers[0](resource).render(output, verbose=True) if not all(char in string.printable for char in content): return '[%d bytes of binary content]' @@ -146,7 +146,7 @@ class DocumentingTemplateEmitter(BaseEmitter): return GenericContentForm(resource) - def emit(self, output=None): + def render(self, output=None): content = self._get_content(self.resource, self.resource.request, output) form_instance = self._get_form_instance(self.resource) @@ -190,11 +190,11 @@ class DocumentingTemplateEmitter(BaseEmitter): return ret -class JSONEmitter(BaseEmitter): - """Emitter which serializes to JSON""" +class JSONRenderer(BaseRenderer): + """Renderer which serializes to JSON""" media_type = 'application/json' - def emit(self, output=None, verbose=False): + def render(self, output=None, verbose=False): if output is None: return '' if verbose: @@ -202,42 +202,42 @@ class JSONEmitter(BaseEmitter): return json.dumps(output) -class XMLEmitter(BaseEmitter): - """Emitter which serializes to XML.""" +class XMLRenderer(BaseRenderer): + """Renderer which serializes to XML.""" media_type = 'application/xml' - def emit(self, output=None, verbose=False): + def render(self, output=None, verbose=False): if output is None: return '' return dict2xml(output) -class DocumentingHTMLEmitter(DocumentingTemplateEmitter): - """Emitter which provides a browsable HTML interface for an API. +class DocumentingHTMLRenderer(DocumentingTemplateRenderer): + """Renderer which provides a browsable HTML interface for an API. See the examples listed in the django-rest-framework documentation to see this in actions.""" media_type = 'text/html' - template = 'emitter.html' + template = 'renderer.html' -class DocumentingXHTMLEmitter(DocumentingTemplateEmitter): - """Identical to DocumentingHTMLEmitter, except with an xhtml media type. +class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): + """Identical to DocumentingHTMLRenderer, except with an xhtml media type. We need this to be listed in preference to xml in order to return HTML to WebKit based browsers, given their Accept headers.""" media_type = 'application/xhtml+xml' - template = 'emitter.html' + template = 'renderer.html' -class DocumentingPlainTextEmitter(DocumentingTemplateEmitter): - """Emitter that serializes the output with the default emitter, but also provides plain-text +class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): + """Renderer that serializes the output with the default renderer, but also provides plain-text doumentation of the returned status and headers, and of the resource's name and description. Useful for browsing an API with command line tools.""" media_type = 'text/plain' - template = 'emitter.txt' + template = 'renderer.txt' -DEFAULT_EMITTERS = ( JSONEmitter, - DocumentingHTMLEmitter, - DocumentingXHTMLEmitter, - DocumentingPlainTextEmitter, - XMLEmitter ) +DEFAULT_RENDERERS = ( JSONRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer ) diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py index cb4d080c7..7879da7ce 100644 --- a/djangorestframework/resource.py +++ b/djangorestframework/resource.py @@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt from djangorestframework.compat import View from djangorestframework.response import Response, ErrorResponse from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin -from djangorestframework import renderers, parsers, authenticators, permissions, validators, status +from djangorestframework import renderers, parsers, authentication, permissions, validators, status # TODO: Figure how out references and named urls need to work nicely @@ -37,8 +37,8 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View): validators = ( validators.FormValidator, ) # List of all authenticating methods to attempt. - authenticators = ( authenticators.UserLoggedInAuthenticator, - authenticators.BasicAuthenticator ) + authentication = ( authentication.UserLoggedInAuthenticator, + authentication.BasicAuthenticator ) # List of all permissions required to access the resource permissions = () diff --git a/djangorestframework/templates/emitter.html b/djangorestframework/templates/renderer.html similarity index 98% rename from djangorestframework/templates/emitter.html rename to djangorestframework/templates/renderer.html index 1931ad39e..105ea0a2d 100644 --- a/djangorestframework/templates/emitter.html +++ b/djangorestframework/templates/renderer.html @@ -48,7 +48,7 @@

GET {{ name }}

GET - {% for media_type in resource.emitted_media_types %} + {% for media_type in resource.renderted_media_types %} {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %} [{{ media_type }}] {% endwith %} diff --git a/djangorestframework/templates/emitter.txt b/djangorestframework/templates/renderer.txt similarity index 100% rename from djangorestframework/templates/emitter.txt rename to djangorestframework/templates/renderer.txt diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py index 724f2ff56..2f9a7e9d2 100644 --- a/djangorestframework/tests/breadcrumbs.py +++ b/djangorestframework/tests/breadcrumbs.py @@ -1,6 +1,6 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase -from djangorestframework.breadcrumbs import get_breadcrumbs +from djangorestframework.utils.breadcrumbs import get_breadcrumbs from djangorestframework.resource import Resource class Root(Resource): diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py index 3e3f7b210..d34e2d110 100644 --- a/djangorestframework/tests/description.py +++ b/djangorestframework/tests/description.py @@ -1,7 +1,7 @@ from django.test import TestCase from djangorestframework.resource import Resource -from djangorestframework.markdownwrapper import apply_markdown -from djangorestframework.description import get_name, get_description +from djangorestframework.compat import apply_markdown +from djangorestframework.utils.description import get_name, get_description # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring diff --git a/djangorestframework/tests/emitters.py b/djangorestframework/tests/emitters.py deleted file mode 100644 index 21a7eb95d..000000000 --- a/djangorestframework/tests/emitters.py +++ /dev/null @@ -1,76 +0,0 @@ -from django.conf.urls.defaults import patterns, url -from django import http -from django.test import TestCase -from djangorestframework.compat import View -from djangorestframework.emitters import BaseEmitter -from djangorestframework.mixins import ResponseMixin -from djangorestframework.response import Response - -DUMMYSTATUS = 200 -DUMMYCONTENT = 'dummycontent' - -EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x -EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x - -class MockView(ResponseMixin, View): - def get(self, request): - response = Response(DUMMYSTATUS, DUMMYCONTENT) - return self.emit(response) - -class EmitterA(BaseEmitter): - media_type = 'mock/emittera' - - def emit(self, output, verbose=False): - return EMITTER_A_SERIALIZER(output) - -class EmitterB(BaseEmitter): - media_type = 'mock/emitterb' - - def emit(self, output, verbose=False): - return EMITTER_B_SERIALIZER(output) - - -urlpatterns = patterns('', - url(r'^$', MockView.as_view(emitters=[EmitterA, EmitterB])), -) - - -class EmitterIntegrationTests(TestCase): - """End-to-end testing of emitters using an EmitterMixin on a generic view.""" - - urls = 'djangorestframework.tests.emitters' - - def test_default_emitter_serializes_content(self): - """If the Accept header is not set the default emitter should serialize the response.""" - resp = self.client.get('/') - self.assertEquals(resp['Content-Type'], EmitterA.media_type) - self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEquals(resp.status_code, DUMMYSTATUS) - - def test_default_emitter_serializes_content_on_accept_any(self): - """If the Accept header is set to */* the default emitter should serialize the response.""" - resp = self.client.get('/', HTTP_ACCEPT='*/*') - self.assertEquals(resp['Content-Type'], EmitterA.media_type) - self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEquals(resp.status_code, DUMMYSTATUS) - - def test_specified_emitter_serializes_content_default_case(self): - """If the Accept header is set the specified emitter should serialize the response. - (In this case we check that works for the default emitter)""" - resp = self.client.get('/', HTTP_ACCEPT=EmitterA.media_type) - self.assertEquals(resp['Content-Type'], EmitterA.media_type) - self.assertEquals(resp.content, EMITTER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEquals(resp.status_code, DUMMYSTATUS) - - def test_specified_emitter_serializes_content_non_default_case(self): - """If the Accept header is set the specified emitter should serialize the response. - (In this case we check that works for a non-default emitter)""" - resp = self.client.get('/', HTTP_ACCEPT=EmitterB.media_type) - self.assertEquals(resp['Content-Type'], EmitterB.media_type) - self.assertEquals(resp.content, EMITTER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEquals(resp.status_code, DUMMYSTATUS) - - def test_unsatisfiable_accept_header_on_request_returns_406_status(self): - """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" - resp = self.client.get('/', HTTP_ACCEPT='foo/bar') - self.assertEquals(resp.status_code, 406) \ No newline at end of file diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py index 4753f6f39..00ebc812b 100644 --- a/djangorestframework/tests/parsers.py +++ b/djangorestframework/tests/parsers.py @@ -82,7 +82,7 @@ from django.test import TestCase from djangorestframework.compat import RequestFactory from djangorestframework.parsers import MultipartParser from djangorestframework.resource import Resource -from djangorestframework.mediatypes import MediaType +from djangorestframework.utils.mediatypes import MediaType from StringIO import StringIO def encode_multipart_formdata(fields, files): diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py new file mode 100644 index 000000000..df0d9c8d4 --- /dev/null +++ b/djangorestframework/tests/renderers.py @@ -0,0 +1,76 @@ +from django.conf.urls.defaults import patterns, url +from django import http +from django.test import TestCase +from djangorestframework.compat import View +from djangorestframework.renderers import BaseRenderer +from djangorestframework.mixins import ResponseMixin +from djangorestframework.response import Response + +DUMMYSTATUS = 200 +DUMMYCONTENT = 'dummycontent' + +RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x +RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x + +class MockView(ResponseMixin, View): + def get(self, request): + response = Response(DUMMYSTATUS, DUMMYCONTENT) + return self.render(response) + +class RendererA(BaseRenderer): + media_type = 'mock/renderera' + + def render(self, output, verbose=False): + return RENDERER_A_SERIALIZER(output) + +class RendererB(BaseRenderer): + media_type = 'mock/rendererb' + + def render(self, output, verbose=False): + return RENDERER_B_SERIALIZER(output) + + +urlpatterns = patterns('', + url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), +) + + +class RendererIntegrationTests(TestCase): + """End-to-end testing of renderers using an RendererMixin on a generic view.""" + + urls = 'djangorestframework.tests.renderers' + + def test_default_renderer_serializes_content(self): + """If the Accept header is not set the default renderer should serialize the response.""" + resp = self.client.get('/') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_default_renderer_serializes_content_on_accept_any(self): + """If the Accept header is set to */* the default renderer should serialize the response.""" + resp = self.client.get('/', HTTP_ACCEPT='*/*') + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for the default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) + self.assertEquals(resp['Content-Type'], RendererA.media_type) + self.assertEquals(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_specified_renderer_serializes_content_non_default_case(self): + """If the Accept header is set the specified renderer should serialize the response. + (In this case we check that works for a non-default renderer)""" + resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) + self.assertEquals(resp['Content-Type'], RendererB.media_type) + self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) + self.assertEquals(resp.status_code, DUMMYSTATUS) + + def test_unsatisfiable_accept_header_on_request_returns_406_status(self): + """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" + resp = self.client.get('/', HTTP_ACCEPT='foo/bar') + self.assertEquals(resp.status_code, 406) \ No newline at end of file diff --git a/djangorestframework/utils.py b/djangorestframework/utils/__init__.py similarity index 98% rename from djangorestframework/utils.py rename to djangorestframework/utils/__init__.py index f60bdee4d..9dc769be2 100644 --- a/djangorestframework/utils.py +++ b/djangorestframework/utils/__init__.py @@ -1,13 +1,12 @@ -import re -import xml.etree.ElementTree as ET from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator from django.core.urlresolvers import resolve from django.conf import settings -try: - import cStringIO as StringIO -except ImportError: - import StringIO + +from djangorestframework.compat import StringIO + +import re +import xml.etree.ElementTree as ET #def admin_media_prefix(request): diff --git a/djangorestframework/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py similarity index 90% rename from djangorestframework/breadcrumbs.py rename to djangorestframework/utils/breadcrumbs.py index ba779dd01..1e604efce 100644 --- a/djangorestframework/breadcrumbs.py +++ b/djangorestframework/utils/breadcrumbs.py @@ -1,5 +1,5 @@ from django.core.urlresolvers import resolve -from djangorestframework.description import get_name +from djangorestframework.utils.description import get_name def get_breadcrumbs(url): """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url).""" @@ -7,7 +7,6 @@ def get_breadcrumbs(url): def breadcrumbs_recursive(url, breadcrumbs_list): """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url.""" - # This is just like compsci 101 all over again... try: (view, unused_args, unused_kwargs) = resolve(url) except: diff --git a/djangorestframework/description.py b/djangorestframework/utils/description.py similarity index 100% rename from djangorestframework/description.py rename to djangorestframework/utils/description.py diff --git a/djangorestframework/mediatypes.py b/djangorestframework/utils/mediatypes.py similarity index 100% rename from djangorestframework/mediatypes.py rename to djangorestframework/utils/mediatypes.py