More refactoring - move various less core stuff into utils etc

This commit is contained in:
Tom Christie 2011-04-29 14:32:56 +01:00
parent 93aa065fa9
commit b358fbdbe9
18 changed files with 203 additions and 204 deletions

View File

@ -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):

View File

@ -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 <h2> and ---- headers are <h3>.
We use <h1> 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 <h2>."""
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
apply_markdown = None

View File

@ -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 <h2> and ---- headers are <h3>.
We use <h1> 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 <h2>."""
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

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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 = ()

View File

@ -48,7 +48,7 @@
<h2>GET {{ name }}</h2>
<div class='submit-row' style='margin: 0; border: 0'>
<a href='{{ request.path }}' rel="nofollow" style='float: left'>GET</a>
{% 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 %}
[<a href='{{ request.path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]
{% endwith %}

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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: