From b358fbdbe9cbd4ce644c4b2c7b9b4cec0811e14e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Apr 2011 14:32:56 +0100
Subject: More refactoring - move various less core stuff into utils etc
---
djangorestframework/authentication.py | 84 ++++++++++
djangorestframework/authenticators.py | 84 ----------
djangorestframework/breadcrumbs.py | 31 ----
djangorestframework/compat.py | 66 +++++++-
djangorestframework/description.py | 37 -----
djangorestframework/emitters.py | 243 ----------------------------
djangorestframework/markdownwrapper.py | 51 ------
djangorestframework/mediatypes.py | 78 ---------
djangorestframework/mixins.py | 10 +-
djangorestframework/parsers.py | 2 +-
djangorestframework/renderers.py | 243 ++++++++++++++++++++++++++++
djangorestframework/resource.py | 6 +-
djangorestframework/templates/emitter.html | 127 ---------------
djangorestframework/templates/emitter.txt | 8 -
djangorestframework/templates/renderer.html | 127 +++++++++++++++
djangorestframework/templates/renderer.txt | 8 +
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 +++++++++
djangorestframework/utils.py | 159 ------------------
djangorestframework/utils/__init__.py | 158 ++++++++++++++++++
djangorestframework/utils/breadcrumbs.py | 30 ++++
djangorestframework/utils/description.py | 37 +++++
djangorestframework/utils/mediatypes.py | 78 +++++++++
26 files changed, 913 insertions(+), 914 deletions(-)
create mode 100644 djangorestframework/authentication.py
delete mode 100644 djangorestframework/authenticators.py
delete mode 100644 djangorestframework/breadcrumbs.py
delete mode 100644 djangorestframework/description.py
delete mode 100644 djangorestframework/emitters.py
delete mode 100644 djangorestframework/markdownwrapper.py
delete mode 100644 djangorestframework/mediatypes.py
create mode 100644 djangorestframework/renderers.py
delete mode 100644 djangorestframework/templates/emitter.html
delete mode 100644 djangorestframework/templates/emitter.txt
create mode 100644 djangorestframework/templates/renderer.html
create mode 100644 djangorestframework/templates/renderer.txt
delete mode 100644 djangorestframework/tests/emitters.py
create mode 100644 djangorestframework/tests/renderers.py
delete mode 100644 djangorestframework/utils.py
create mode 100644 djangorestframework/utils/__init__.py
create mode 100644 djangorestframework/utils/breadcrumbs.py
create mode 100644 djangorestframework/utils/description.py
create mode 100644 djangorestframework/utils/mediatypes.py
(limited to 'djangorestframework')
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
new file mode 100644
index 00000000..894b34fc
--- /dev/null
+++ b/djangorestframework/authentication.py
@@ -0,0 +1,84 @@
+"""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 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
+from djangorestframework.utils import as_tuple
+import base64
+
+
+class BaseAuthenticator(object):
+ """All authentication should extend BaseAuthenticator."""
+
+ def __init__(self, view):
+ """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):
+ """Authenticate the request and return the authentication context or None.
+
+ An authentication context might be something as simple as a User object, or it might
+ be some more complicated token, for example authentication tokens which are signed
+ against a particular set of permissions for a given user, over a given timeframe.
+
+ The default permission checking on Resource will use the allowed_methods attribute
+ for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
+
+ The authentication context is available to the method calls eg Resource.get(request)
+ by accessing self.auth in order to allow them to apply any more fine grained permission
+ checking at the point the response is being generated.
+
+ This function must be overridden to be implemented."""
+ return None
+
+
+class BasicAuthenticator(BaseAuthenticator):
+ """Use HTTP Basic authentication"""
+ def authenticate(self, request):
+ from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
+
+ if 'HTTP_AUTHORIZATION' in request.META:
+ auth = request.META['HTTP_AUTHORIZATION'].split()
+ if len(auth) == 2 and auth[0].lower() == "basic":
+ try:
+ auth_parts = base64.b64decode(auth[1]).partition(':')
+ except TypeError:
+ return None
+
+ try:
+ uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
+ except DjangoUnicodeDecodeError:
+ return None
+
+ user = authenticate(username=uname, password=passwd)
+ if user is not None and user.is_active:
+ return user
+ return None
+
+
+class UserLoggedInAuthenticator(BaseAuthenticator):
+ """Use Django's built-in request session for authentication."""
+ def authenticate(self, request):
+ if getattr(request, 'user', None) and request.user.is_active:
+ # If this is a POST request we enforce CSRF validation.
+ if request.method.upper() == 'POST':
+ # Temporarily replace request.POST with .RAW_CONTENT,
+ # so that we use our more generic request parsing
+ request._post = self.view.RAW_CONTENT
+ resp = CsrfViewMiddleware().process_view(request, None, (), {})
+ del(request._post)
+ if resp is not None: # csrf failed
+ return None
+ return request.user
+ return None
+
+
+#class DigestAuthentication(BaseAuthentication):
+# pass
+#
+#class OAuthAuthentication(BaseAuthentication):
+# pass
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
deleted file mode 100644
index 74d9931a..00000000
--- a/djangorestframework/authenticators.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""The :mod:`authenticators` 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.
-"""
-from django.contrib.auth import authenticate
-from django.middleware.csrf import CsrfViewMiddleware
-from djangorestframework.utils import as_tuple
-import base64
-
-
-class BaseAuthenticator(object):
- """All authenticators 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."""
- self.view = view
-
- def authenticate(self, request):
- """Authenticate the request and return the authentication context or None.
-
- An authentication context might be something as simple as a User object, or it might
- be some more complicated token, for example authentication tokens which are signed
- against a particular set of permissions for a given user, over a given timeframe.
-
- The default permission checking on Resource will use the allowed_methods attribute
- for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
-
- The authentication context is available to the method calls eg Resource.get(request)
- by accessing self.auth in order to allow them to apply any more fine grained permission
- checking at the point the response is being generated.
-
- This function must be overridden to be implemented."""
- return None
-
-
-class BasicAuthenticator(BaseAuthenticator):
- """Use HTTP Basic authentication"""
- def authenticate(self, request):
- from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
-
- if 'HTTP_AUTHORIZATION' in request.META:
- auth = request.META['HTTP_AUTHORIZATION'].split()
- if len(auth) == 2 and auth[0].lower() == "basic":
- try:
- auth_parts = base64.b64decode(auth[1]).partition(':')
- except TypeError:
- return None
-
- try:
- uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
- except DjangoUnicodeDecodeError:
- return None
-
- user = authenticate(username=uname, password=passwd)
- if user is not None and user.is_active:
- return user
- return None
-
-
-class UserLoggedInAuthenticator(BaseAuthenticator):
- """Use Django's built-in request session for authentication."""
- def authenticate(self, request):
- if getattr(request, 'user', None) and request.user.is_active:
- # If this is a POST request we enforce CSRF validation.
- if request.method.upper() == 'POST':
- # Temporarily replace request.POST with .RAW_CONTENT,
- # so that we use our more generic request parsing
- request._post = self.view.RAW_CONTENT
- resp = CsrfViewMiddleware().process_view(request, None, (), {})
- del(request._post)
- if resp is not None: # csrf failed
- return None
- return request.user
- return None
-
-
-#class DigestAuthentication(BaseAuthentication):
-# pass
-#
-#class OAuthAuthentication(BaseAuthentication):
-# pass
diff --git a/djangorestframework/breadcrumbs.py b/djangorestframework/breadcrumbs.py
deleted file mode 100644
index ba779dd0..00000000
--- a/djangorestframework/breadcrumbs.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.core.urlresolvers import resolve
-from djangorestframework.description import get_name
-
-def get_breadcrumbs(url):
- """Given a url returns a list of breadcrumbs, which are each a tuple of (name, 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:
- pass
- else:
- if callable(view):
- breadcrumbs_list.insert(0, (get_name(view), url))
-
- if url == '':
- # All done
- return breadcrumbs_list
-
- elif url.endswith('/'):
- # Drop trailing slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
-
- # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
- return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
-
- return breadcrumbs_recursive(url, [])
-
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 22b57186..98fbbb62 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/description.py b/djangorestframework/description.py
deleted file mode 100644
index f7145c0f..00000000
--- a/djangorestframework/description.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Get a descriptive name and description for a view,
-based on class name and docstring, and override-able by 'name' and 'description' attributes"""
-import re
-
-def get_name(view):
- """Return a name for the view.
-
- If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
- if getattr(view, 'name', None) is not None:
- return view.name
-
- if getattr(view, '__name__', None) is not None:
- name = view.__name__
- elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
- name = view.__class__.__name__
- else:
- return ''
-
- return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
-
-def get_description(view):
- """Provide a description for the view.
-
- By default this is the view's docstring with nice unindention applied."""
- if getattr(view, 'description', None) is not None:
- return getattr(view, 'description')
-
- if getattr(view, '__doc__', None) is not None:
- whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
-
- if whitespace_counts:
- whitespace_pattern = '^' + (' ' * min(whitespace_counts))
- return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
-
- return view.__doc__
-
- return ''
\ No newline at end of file
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
deleted file mode 100644
index 87b3e94e..00000000
--- a/djangorestframework/emitters.py
+++ /dev/null
@@ -1,243 +0,0 @@
-"""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,
-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.
-"""
-from django import forms
-from django.conf import settings
-from django.template import RequestContext, loader
-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 import status
-
-from urllib import quote_plus
-import string
-import re
-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.
-
-class BaseEmitter(object):
- """All emitters must extend this class, set the media_type attribute, and
- override the emit() 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.
- Override this method to provide for other behaviour."""
- if output is None:
- return ''
-
- return output
-
-
-class TemplateEmitter(BaseEmitter):
- """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):
- if output is None:
- return ''
-
- context = RequestContext(self.request, output)
- return self.template.render(context)
-
-
-class DocumentingTemplateEmitter(BaseEmitter):
- """Base class for emitters 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.
-
- (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]'
-
- content = emitters[0](resource).emit(output, verbose=True)
- if not all(char in string.printable for char in content):
- return '[%d bytes of binary content]'
-
- return content
-
-
- def _get_form_instance(self, resource):
- """Get a form, possibly bound to either the input or output data.
- In the absence on of the Resource having an associated form then
- provide a form that can be used to submit arbitrary content."""
- # Get the form instance if we have one bound to the input
- #form_instance = resource.form_instance
- # TODO! Reinstate this
-
- form_instance = getattr(resource, 'bound_form_instance', None)
-
- if not form_instance and hasattr(resource, 'get_bound_form'):
- # Otherwise if we have a response that is valid against the form then use that
- if resource.response.has_content_body:
- try:
- form_instance = resource.get_bound_form(resource.response.cleaned_content)
- 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 = resource.get_bound_form()
- except:
- pass
-
- # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
- if not form_instance:
- form_instance = self._get_generic_content_form(resource)
-
- return form_instance
-
-
- def _get_generic_content_form(self, resource):
- """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 getattr(resource, 'USE_FORM_OVERLOADING', False):
- return None
-
- # NB. http://jacobian.org/writing/dynamic-form-generation/
- class GenericContentForm(forms.Form):
- def __init__(self, resource):
- """We don't know the names of the fields we want to set until the point the form is instantiated,
- as they are determined by the Resource the form is being created against.
- Add the fields dynamically."""
- super(GenericContentForm, self).__init__()
-
- contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
- initial_contenttype = resource.default_parser.media_type
-
- self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
- choices=contenttype_choices,
- initial=initial_contenttype)
- self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
- widget=forms.Textarea)
-
- # If either of these reserved parameters are turned off then content tunneling is not possible
- if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
- return None
-
- # Okey doke, let's do it
- return GenericContentForm(resource)
-
-
- def emit(self, output=None):
- 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):
- login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
- logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
- else:
- 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)
-
- return ret
-
-
-class JSONEmitter(BaseEmitter):
- """Emitter which serializes to JSON"""
- media_type = 'application/json'
-
- def emit(self, output=None, verbose=False):
- if output is None:
- return ''
- if verbose:
- return json.dumps(output, indent=4, sort_keys=True)
- return json.dumps(output)
-
-
-class XMLEmitter(BaseEmitter):
- """Emitter which serializes to XML."""
- media_type = 'application/xml'
-
- def emit(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.
- See the examples listed in the django-rest-framework documentation to see this in actions."""
- media_type = 'text/html'
- template = 'emitter.html'
-
-
-class DocumentingXHTMLEmitter(DocumentingTemplateEmitter):
- """Identical to DocumentingHTMLEmitter, 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'
-
-
-class DocumentingPlainTextEmitter(DocumentingTemplateEmitter):
- """Emitter that serializes the output with the default emitter, 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'
-
-DEFAULT_EMITTERS = ( JSONEmitter,
- DocumentingHTMLEmitter,
- DocumentingXHTMLEmitter,
- DocumentingPlainTextEmitter,
- XMLEmitter )
-
-
diff --git a/djangorestframework/markdownwrapper.py b/djangorestframework/markdownwrapper.py
deleted file mode 100644
index 70512440..00000000
--- 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/mediatypes.py b/djangorestframework/mediatypes.py
deleted file mode 100644
index 92d9264c..00000000
--- a/djangorestframework/mediatypes.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""
-Handling of media types, as found in HTTP Content-Type and Accept headers.
-
-See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
-"""
-
-from django.http.multipartparser import parse_header
-
-
-class MediaType(object):
- def __init__(self, media_type_str):
- self.orig = media_type_str
- self.media_type, self.params = parse_header(media_type_str)
- self.main_type, sep, self.sub_type = self.media_type.partition('/')
-
- def match(self, other):
- """Return true if this MediaType satisfies the constraint of the given MediaType."""
- for key in other.params.keys():
- if key != 'q' and other.params[key] != self.params.get(key, None):
- return False
-
- if other.sub_type != '*' and other.sub_type != self.sub_type:
- return False
-
- if other.main_type != '*' and other.main_type != self.main_type:
- return False
-
- return True
-
- def precedence(self):
- """
- Return a precedence level for the media type given how specific it is.
- """
- if self.main_type == '*':
- return 1
- elif self.sub_type == '*':
- return 2
- elif not self.params or self.params.keys() == ['q']:
- return 3
- return 4
-
- def quality(self):
- """
- Return a quality level for the media type.
- """
- try:
- return Decimal(self.params.get('q', '1.0'))
- except:
- return Decimal(0)
-
- def score(self):
- """
- Return an overall score for a given media type given it's quality and precedence.
- """
- # NB. quality values should only have up to 3 decimal points
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
- return self.quality * 10000 + self.precedence
-
- def is_form(self):
- """
- Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
- (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
- """
- return self.media_type == 'application/x-www-form-urlencoded' or \
- self.media_type == 'multipart/form-data'
-
- def as_tuple(self):
- return (self.main_type, self.sub_type, self.params)
-
- def __repr__(self):
- return "" % (self.as_tuple(),)
-
- def __str__(self):
- return unicode(self).encode('utf-8')
-
- def __unicode__(self):
- return self.orig
-
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 43b33f50..ebeee31a 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 96b29a66..6d6bd5ce 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/renderers.py b/djangorestframework/renderers.py
new file mode 100644
index 00000000..e53dc061
--- /dev/null
+++ b/djangorestframework/renderers.py
@@ -0,0 +1,243 @@
+"""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, renderers and parsers on the Resource.
+"""
+from django import forms
+from django.conf import settings
+from django.template import RequestContext, loader
+from django.utils import simplejson as json
+from django import forms
+
+from djangorestframework.utils import dict2xml, url_resolves
+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
+import string
+import re
+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 renderer output anything if it explicitly provides support for that.
+
+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 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 ''
+
+ return output
+
+
+class TemplateRenderer(BaseRenderer):
+ """Provided for convienience.
+ Emit the output by simply rendering it with the given template."""
+ media_type = None
+ template = None
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+
+ context = RequestContext(self.request, output)
+ return self.template.render(context)
+
+
+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 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 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 = renderers[0](resource).render(output, verbose=True)
+ if not all(char in string.printable for char in content):
+ return '[%d bytes of binary content]'
+
+ return content
+
+
+ def _get_form_instance(self, resource):
+ """Get a form, possibly bound to either the input or output data.
+ In the absence on of the Resource having an associated form then
+ provide a form that can be used to submit arbitrary content."""
+ # Get the form instance if we have one bound to the input
+ #form_instance = resource.form_instance
+ # TODO! Reinstate this
+
+ form_instance = getattr(resource, 'bound_form_instance', None)
+
+ if not form_instance and hasattr(resource, 'get_bound_form'):
+ # Otherwise if we have a response that is valid against the form then use that
+ if resource.response.has_content_body:
+ try:
+ form_instance = resource.get_bound_form(resource.response.cleaned_content)
+ 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 = resource.get_bound_form()
+ except:
+ pass
+
+ # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
+ if not form_instance:
+ form_instance = self._get_generic_content_form(resource)
+
+ return form_instance
+
+
+ def _get_generic_content_form(self, resource):
+ """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 getattr(resource, 'USE_FORM_OVERLOADING', False):
+ return None
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self, resource):
+ """We don't know the names of the fields we want to set until the point the form is instantiated,
+ as they are determined by the Resource the form is being created against.
+ Add the fields dynamically."""
+ super(GenericContentForm, self).__init__()
+
+ contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
+ initial_contenttype = resource.default_parser.media_type
+
+ self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
+ choices=contenttype_choices,
+ initial=initial_contenttype)
+ self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
+ widget=forms.Textarea)
+
+ # If either of these reserved parameters are turned off then content tunneling is not possible
+ if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
+ return None
+
+ # Okey doke, let's do it
+ return GenericContentForm(resource)
+
+
+ def render(self, output=None):
+ 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):
+ login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
+ logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
+ else:
+ 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)
+
+ return ret
+
+
+class JSONRenderer(BaseRenderer):
+ """Renderer which serializes to JSON"""
+ media_type = 'application/json'
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+ if verbose:
+ return json.dumps(output, indent=4, sort_keys=True)
+ return json.dumps(output)
+
+
+class XMLRenderer(BaseRenderer):
+ """Renderer which serializes to XML."""
+ media_type = 'application/xml'
+
+ def render(self, output=None, verbose=False):
+ if output is None:
+ return ''
+ return dict2xml(output)
+
+
+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 = 'renderer.html'
+
+
+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 = 'renderer.html'
+
+
+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 = 'renderer.txt'
+
+DEFAULT_RENDERERS = ( JSONRenderer,
+ DocumentingHTMLRenderer,
+ DocumentingXHTMLRenderer,
+ DocumentingPlainTextRenderer,
+ XMLRenderer )
+
+
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index cb4d080c..7879da7c 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/emitter.html
deleted file mode 100644
index 1931ad39..00000000
--- a/djangorestframework/templates/emitter.html
+++ /dev/null
@@ -1,127 +0,0 @@
-{% load urlize_quoted_links %}{% load add_query_param %}
-
-
-
-
-
-
- Django REST framework - {{ name }}
-
-
-
- {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} Log out{% endif %}{% else %}Anonymous {% if login_url %}Log in{% endif %}{% endif %}
-
-
-
-
- {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
- {{breadcrumb_name}} {% if not forloop.last %}›{% endif %}
- {% endfor %}
-
-
-
-
-
-
{{ name }}
-
{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}
-
-
{{ response.status }} {{ response.status_text }}{% autoescape off %}
-{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
-{% endfor %}
-{{ content|urlize_quoted_links }}
{% endautoescape %}
-
- {% if 'GET' in resource.allowed_methods %}
-
- {% endif %}
-
- {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
- *** tunneling via POST forms is enabled. ***
- *** (We could display only the POST form if method tunneling is disabled, but I think ***
- *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
-
- {% if resource.METHOD_PARAM and form %}
- {% if 'POST' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'PUT' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'DELETE' in resource.allowed_methods %}
-
- {% endif %}
- {% endif %}
-
-
-
-
-
\ No newline at end of file
diff --git a/djangorestframework/templates/emitter.txt b/djangorestframework/templates/emitter.txt
deleted file mode 100644
index 5be8c117..00000000
--- a/djangorestframework/templates/emitter.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ name }}
-
-{{ description }}
-
-{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
-{% for key, val in response.headers.items %}{{ key }}: {{ val }}
-{% endfor %}
-{{ content }}{% endautoescape %}
diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html
new file mode 100644
index 00000000..105ea0a2
--- /dev/null
+++ b/djangorestframework/templates/renderer.html
@@ -0,0 +1,127 @@
+{% load urlize_quoted_links %}{% load add_query_param %}
+
+
+
+
+
+
+ Django REST framework - {{ name }}
+
+
+
+ {% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} Log out{% endif %}{% else %}Anonymous {% if login_url %}Log in{% endif %}{% endif %}
+
+
+
+
+ {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
+ {{breadcrumb_name}} {% if not forloop.last %}›{% endif %}
+ {% endfor %}
+
+
+
+
+
+
{{ name }}
+
{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}
+
+
{{ response.status }} {{ response.status_text }}{% autoescape off %}
+{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
+{% endfor %}
+{{ content|urlize_quoted_links }}
{% endautoescape %}
+
+ {% if 'GET' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
+ *** tunneling via POST forms is enabled. ***
+ *** (We could display only the POST form if method tunneling is disabled, but I think ***
+ *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
+
+ {% if resource.METHOD_PARAM and form %}
+ {% if 'POST' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'PUT' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'DELETE' in resource.allowed_methods %}
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
\ No newline at end of file
diff --git a/djangorestframework/templates/renderer.txt b/djangorestframework/templates/renderer.txt
new file mode 100644
index 00000000..5be8c117
--- /dev/null
+++ b/djangorestframework/templates/renderer.txt
@@ -0,0 +1,8 @@
+{{ name }}
+
+{{ description }}
+
+{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
+{% for key, val in response.headers.items %}{{ key }}: {{ val }}
+{% endfor %}
+{{ content }}{% endautoescape %}
diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py
index 724f2ff5..2f9a7e9d 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 3e3f7b21..d34e2d11 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 21a7eb95..00000000
--- 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 4753f6f3..00ebc812 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 00000000..df0d9c8d
--- /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.py
deleted file mode 100644
index f60bdee4..00000000
--- a/djangorestframework/utils.py
+++ /dev/null
@@ -1,159 +0,0 @@
-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
-
-
-#def admin_media_prefix(request):
-# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
-# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
-
-MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
-
-def as_tuple(obj):
- """Given obj return a tuple"""
- if obj is None:
- return ()
- elif isinstance(obj, list):
- return tuple(obj)
- elif isinstance(obj, tuple):
- return obj
- return (obj,)
-
-
-def url_resolves(url):
- """Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
- try:
- resolve(url)
- except:
- return False
- return True
-
-
-# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
-#class object_dict(dict):
-# """object view of dict, you can
-# >>> a = object_dict()
-# >>> a.fish = 'fish'
-# >>> a['fish']
-# 'fish'
-# >>> a['water'] = 'water'
-# >>> a.water
-# 'water'
-# >>> a.test = {'value': 1}
-# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
-# >>> a.test, a.test2.name, a.test2.value
-# (1, 'test2', 2)
-# """
-# def __init__(self, initd=None):
-# if initd is None:
-# initd = {}
-# dict.__init__(self, initd)
-#
-# def __getattr__(self, item):
-# d = self.__getitem__(item)
-# # if value is the only key in object, you can omit it
-# if isinstance(d, dict) and 'value' in d and len(d) == 1:
-# return d['value']
-# else:
-# return d
-#
-# def __setattr__(self, item, value):
-# self.__setitem__(item, value)
-
-
-# From xml2dict
-class XML2Dict(object):
-
- def __init__(self):
- pass
-
- def _parse_node(self, node):
- node_tree = {}
- # Save attrs and text, hope there will not be a child with same name
- if node.text:
- node_tree = node.text
- for (k,v) in node.attrib.items():
- k,v = self._namespace_split(k, v)
- node_tree[k] = v
- #Save childrens
- for child in node.getchildren():
- tag, tree = self._namespace_split(child.tag, self._parse_node(child))
- if tag not in node_tree: # the first time, so store it in dict
- node_tree[tag] = tree
- continue
- old = node_tree[tag]
- if not isinstance(old, list):
- node_tree.pop(tag)
- node_tree[tag] = [old] # multi times, so change old dict to a list
- node_tree[tag].append(tree) # add the new one
-
- return node_tree
-
-
- def _namespace_split(self, tag, value):
- """
- Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
- ns = http://cs.sfsu.edu/csc867/myscheduler
- name = patients
- """
- result = re.compile("\{(.*)\}(.*)").search(tag)
- if result:
- value.namespace, tag = result.groups()
- return (tag, value)
-
- def parse(self, file):
- """parse a xml file to a dict"""
- f = open(file, 'r')
- return self.fromstring(f.read())
-
- def fromstring(self, s):
- """parse a string"""
- t = ET.fromstring(s)
- unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
- return root_tree
-
-
-def xml2dict(input):
- return XML2Dict().fromstring(input)
-
-
-# Piston:
-class XMLRenderer():
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in data.iteritems():
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- else:
- xml.characters(smart_unicode(data))
-
- def dict2xml(self, data):
- stream = StringIO.StringIO()
-
- xml = SimplerXMLGenerator(stream, "utf-8")
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
-
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
-def dict2xml(input):
- return XMLRenderer().dict2xml(input)
diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py
new file mode 100644
index 00000000..9dc769be
--- /dev/null
+++ b/djangorestframework/utils/__init__.py
@@ -0,0 +1,158 @@
+from django.utils.encoding import smart_unicode
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.core.urlresolvers import resolve
+from django.conf import settings
+
+from djangorestframework.compat import StringIO
+
+import re
+import xml.etree.ElementTree as ET
+
+
+#def admin_media_prefix(request):
+# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
+# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
+
+MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
+
+def as_tuple(obj):
+ """Given obj return a tuple"""
+ if obj is None:
+ return ()
+ elif isinstance(obj, list):
+ return tuple(obj)
+ elif isinstance(obj, tuple):
+ return obj
+ return (obj,)
+
+
+def url_resolves(url):
+ """Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
+ try:
+ resolve(url)
+ except:
+ return False
+ return True
+
+
+# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
+#class object_dict(dict):
+# """object view of dict, you can
+# >>> a = object_dict()
+# >>> a.fish = 'fish'
+# >>> a['fish']
+# 'fish'
+# >>> a['water'] = 'water'
+# >>> a.water
+# 'water'
+# >>> a.test = {'value': 1}
+# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
+# >>> a.test, a.test2.name, a.test2.value
+# (1, 'test2', 2)
+# """
+# def __init__(self, initd=None):
+# if initd is None:
+# initd = {}
+# dict.__init__(self, initd)
+#
+# def __getattr__(self, item):
+# d = self.__getitem__(item)
+# # if value is the only key in object, you can omit it
+# if isinstance(d, dict) and 'value' in d and len(d) == 1:
+# return d['value']
+# else:
+# return d
+#
+# def __setattr__(self, item, value):
+# self.__setitem__(item, value)
+
+
+# From xml2dict
+class XML2Dict(object):
+
+ def __init__(self):
+ pass
+
+ def _parse_node(self, node):
+ node_tree = {}
+ # Save attrs and text, hope there will not be a child with same name
+ if node.text:
+ node_tree = node.text
+ for (k,v) in node.attrib.items():
+ k,v = self._namespace_split(k, v)
+ node_tree[k] = v
+ #Save childrens
+ for child in node.getchildren():
+ tag, tree = self._namespace_split(child.tag, self._parse_node(child))
+ if tag not in node_tree: # the first time, so store it in dict
+ node_tree[tag] = tree
+ continue
+ old = node_tree[tag]
+ if not isinstance(old, list):
+ node_tree.pop(tag)
+ node_tree[tag] = [old] # multi times, so change old dict to a list
+ node_tree[tag].append(tree) # add the new one
+
+ return node_tree
+
+
+ def _namespace_split(self, tag, value):
+ """
+ Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
+ ns = http://cs.sfsu.edu/csc867/myscheduler
+ name = patients
+ """
+ result = re.compile("\{(.*)\}(.*)").search(tag)
+ if result:
+ value.namespace, tag = result.groups()
+ return (tag, value)
+
+ def parse(self, file):
+ """parse a xml file to a dict"""
+ f = open(file, 'r')
+ return self.fromstring(f.read())
+
+ def fromstring(self, s):
+ """parse a string"""
+ t = ET.fromstring(s)
+ unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
+ return root_tree
+
+
+def xml2dict(input):
+ return XML2Dict().fromstring(input)
+
+
+# Piston:
+class XMLRenderer():
+ def _to_xml(self, xml, data):
+ if isinstance(data, (list, tuple)):
+ for item in data:
+ xml.startElement("list-item", {})
+ self._to_xml(xml, item)
+ xml.endElement("list-item")
+
+ elif isinstance(data, dict):
+ for key, value in data.iteritems():
+ xml.startElement(key, {})
+ self._to_xml(xml, value)
+ xml.endElement(key)
+
+ else:
+ xml.characters(smart_unicode(data))
+
+ def dict2xml(self, data):
+ stream = StringIO.StringIO()
+
+ xml = SimplerXMLGenerator(stream, "utf-8")
+ xml.startDocument()
+ xml.startElement("root", {})
+
+ self._to_xml(xml, data)
+
+ xml.endElement("root")
+ xml.endDocument()
+ return stream.getvalue()
+
+def dict2xml(input):
+ return XMLRenderer().dict2xml(input)
diff --git a/djangorestframework/utils/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py
new file mode 100644
index 00000000..1e604efc
--- /dev/null
+++ b/djangorestframework/utils/breadcrumbs.py
@@ -0,0 +1,30 @@
+from django.core.urlresolvers import resolve
+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)."""
+
+ def breadcrumbs_recursive(url, breadcrumbs_list):
+ """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
+
+ try:
+ (view, unused_args, unused_kwargs) = resolve(url)
+ except:
+ pass
+ else:
+ if callable(view):
+ breadcrumbs_list.insert(0, (get_name(view), url))
+
+ if url == '':
+ # All done
+ return breadcrumbs_list
+
+ elif url.endswith('/'):
+ # Drop trailing slash off the end and continue to try to resolve more breadcrumbs
+ return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
+
+ # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
+ return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
+
+ return breadcrumbs_recursive(url, [])
+
diff --git a/djangorestframework/utils/description.py b/djangorestframework/utils/description.py
new file mode 100644
index 00000000..f7145c0f
--- /dev/null
+++ b/djangorestframework/utils/description.py
@@ -0,0 +1,37 @@
+"""Get a descriptive name and description for a view,
+based on class name and docstring, and override-able by 'name' and 'description' attributes"""
+import re
+
+def get_name(view):
+ """Return a name for the view.
+
+ If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
+ if getattr(view, 'name', None) is not None:
+ return view.name
+
+ if getattr(view, '__name__', None) is not None:
+ name = view.__name__
+ elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
+ name = view.__class__.__name__
+ else:
+ return ''
+
+ return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
+
+def get_description(view):
+ """Provide a description for the view.
+
+ By default this is the view's docstring with nice unindention applied."""
+ if getattr(view, 'description', None) is not None:
+ return getattr(view, 'description')
+
+ if getattr(view, '__doc__', None) is not None:
+ whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
+
+ if whitespace_counts:
+ whitespace_pattern = '^' + (' ' * min(whitespace_counts))
+ return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
+
+ return view.__doc__
+
+ return ''
\ No newline at end of file
diff --git a/djangorestframework/utils/mediatypes.py b/djangorestframework/utils/mediatypes.py
new file mode 100644
index 00000000..92d9264c
--- /dev/null
+++ b/djangorestframework/utils/mediatypes.py
@@ -0,0 +1,78 @@
+"""
+Handling of media types, as found in HTTP Content-Type and Accept headers.
+
+See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
+"""
+
+from django.http.multipartparser import parse_header
+
+
+class MediaType(object):
+ def __init__(self, media_type_str):
+ self.orig = media_type_str
+ self.media_type, self.params = parse_header(media_type_str)
+ self.main_type, sep, self.sub_type = self.media_type.partition('/')
+
+ def match(self, other):
+ """Return true if this MediaType satisfies the constraint of the given MediaType."""
+ for key in other.params.keys():
+ if key != 'q' and other.params[key] != self.params.get(key, None):
+ return False
+
+ if other.sub_type != '*' and other.sub_type != self.sub_type:
+ return False
+
+ if other.main_type != '*' and other.main_type != self.main_type:
+ return False
+
+ return True
+
+ def precedence(self):
+ """
+ Return a precedence level for the media type given how specific it is.
+ """
+ if self.main_type == '*':
+ return 1
+ elif self.sub_type == '*':
+ return 2
+ elif not self.params or self.params.keys() == ['q']:
+ return 3
+ return 4
+
+ def quality(self):
+ """
+ Return a quality level for the media type.
+ """
+ try:
+ return Decimal(self.params.get('q', '1.0'))
+ except:
+ return Decimal(0)
+
+ def score(self):
+ """
+ Return an overall score for a given media type given it's quality and precedence.
+ """
+ # NB. quality values should only have up to 3 decimal points
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
+ return self.quality * 10000 + self.precedence
+
+ def is_form(self):
+ """
+ Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
+ (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
+ """
+ return self.media_type == 'application/x-www-form-urlencoded' or \
+ self.media_type == 'multipart/form-data'
+
+ def as_tuple(self):
+ return (self.main_type, self.sub_type, self.params)
+
+ def __repr__(self):
+ return "" % (self.as_tuple(),)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ return self.orig
+
--
cgit v1.2.3