mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
pull in -dev as 0.2.0
This commit is contained in:
commit
b50492853f
|
@ -2,6 +2,7 @@ syntax: glob
|
|||
|
||||
*.pyc
|
||||
*.db
|
||||
assetplatform.egg-info/*
|
||||
*~
|
||||
coverage.xml
|
||||
env
|
||||
|
|
1
AUTHORS
1
AUTHORS
|
@ -4,6 +4,7 @@ Paul Bagwell <pbgwl> - Suggestions & bugfixes.
|
|||
Marko Tibold <markotibold> - Contributions & Providing the Hudson CI Server.
|
||||
Sébastien Piquemal <sebpiq> - Contributions.
|
||||
Carmen Wick <cwick> - Bugfixes.
|
||||
Alex Ehlke <aehlke> - Design Contributions.
|
||||
|
||||
THANKS TO:
|
||||
Jesper Noehr <jespern> & the django-piston contributors for providing the starting point for this project.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '0.1.1'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
106
djangorestframework/authentication.py
Normal file
106
djangorestframework/authentication.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
The :mod:`authentication` module provides a set of pluggable authentication classes.
|
||||
|
||||
Authentication behavior is provided by mixing the :class:`mixins.AuthMixin` class into a :class:`View` class.
|
||||
|
||||
The set of authentication methods which are used is then specified by setting the
|
||||
:attr:`authentication` attribute on the :class:`View` class, and listing a set of :class:`authentication` classes.
|
||||
"""
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from djangorestframework.utils import as_tuple
|
||||
import base64
|
||||
|
||||
__all__ = (
|
||||
'BaseAuthenticaton',
|
||||
'BasicAuthenticaton',
|
||||
'UserLoggedInAuthenticaton'
|
||||
)
|
||||
|
||||
|
||||
class BaseAuthenticaton(object):
|
||||
"""
|
||||
All authentication classes should extend BaseAuthentication.
|
||||
"""
|
||||
|
||||
def __init__(self, view):
|
||||
"""
|
||||
:class:`Authentication` classes are always passed the current view on creation.
|
||||
"""
|
||||
self.view = view
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
|
||||
|
||||
.. [*] The authentication context *will* typically be a :obj:`User`,
|
||||
but it need not be. It can be any user-like object so long as the
|
||||
permissions classes (see the :mod:`permissions` module) on the view can
|
||||
handle the object and use it to determine if the request has the required
|
||||
permissions or not.
|
||||
|
||||
This can be an important distinction if you're implementing some token
|
||||
based authentication mechanism, where the authentication context
|
||||
may be more involved than simply mapping to a :obj:`User`.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class BasicAuthenticaton(BaseAuthenticaton):
|
||||
"""
|
||||
Use HTTP Basic authentication.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns a :obj:`User` if a correct username and password have been supplied
|
||||
using HTTP Basic authentication. Otherwise returns :const:`None`.
|
||||
"""
|
||||
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 UserLoggedInAuthenticaton(BaseAuthenticaton):
|
||||
"""
|
||||
Use Django's session framework for authentication.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns a :obj:`User` if the request session currently has a logged in user.
|
||||
Otherwise returns :const:`None`.
|
||||
"""
|
||||
# TODO: Switch this back to request.POST, and let FormParser/MultiPartParser deal with the consequences.
|
||||
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 .DATA,
|
||||
# so that we use our more generic request parsing
|
||||
request._post = self.view.DATA
|
||||
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||
del(request._post)
|
||||
if resp is not None: # csrf failed
|
||||
return None
|
||||
return request.user
|
||||
return None
|
||||
|
||||
|
||||
# TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication
|
|
@ -1,97 +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 AuthenticatorMixin(object):
|
||||
"""Adds pluggable authentication behaviour."""
|
||||
|
||||
"""The set of authenticators to use."""
|
||||
authenticators = None
|
||||
|
||||
def authenticate(self, request):
|
||||
"""Attempt to authenticate the request, returning an authentication context or None.
|
||||
An authentication context may be any object, although in many cases it will simply be a :class:`User` instance."""
|
||||
|
||||
# Attempt authentication against each authenticator in turn,
|
||||
# and return None if no authenticators succeed in authenticating the request.
|
||||
for authenticator in as_tuple(self.authenticators):
|
||||
auth_context = authenticator(self).authenticate(request)
|
||||
if auth_context:
|
||||
return auth_context
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class BaseAuthenticator(object):
|
||||
"""All authenticators should extend BaseAuthenticator."""
|
||||
|
||||
def __init__(self, mixin):
|
||||
"""Initialise the authenticator with the mixin instance as state,
|
||||
in case the authenticator needs to access any metadata on the mixin object."""
|
||||
self.mixin = mixin
|
||||
|
||||
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 passed to the method calls eg Resource.get(request, 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.mixin.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
|
||||
|
|
@ -1,9 +1,26 @@
|
|||
"""Compatability module to provide support for backwards compatability with older versions of django/python"""
|
||||
"""
|
||||
The :mod:`compatability` module provides 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
|
||||
|
@ -12,24 +29,25 @@ except ImportError:
|
|||
# Lovely stuff
|
||||
class RequestFactory(Client):
|
||||
"""
|
||||
Class that lets you create mock Request objects for use in testing.
|
||||
Class that lets you create mock :obj:`Request` objects for use in testing.
|
||||
|
||||
Usage:
|
||||
Usage::
|
||||
|
||||
rf = RequestFactory()
|
||||
get_request = rf.get('/hello/')
|
||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||
rf = RequestFactory()
|
||||
get_request = rf.get('/hello/')
|
||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||
|
||||
This class re-uses the django.test.client.Client interface, docs here:
|
||||
http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||
This class re-uses the :class:`django.test.client.Client` interface. Of which
|
||||
you can find the docs here__.
|
||||
|
||||
Once you have a request object you can pass it to any view function,
|
||||
just as if that view had been hooked up using a URLconf.
|
||||
__ http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||
|
||||
Once you have a `request` object you can pass it to any :func:`view` function,
|
||||
just as if that :func:`view` had been hooked up using a URLconf.
|
||||
"""
|
||||
def request(self, **request):
|
||||
"""
|
||||
Similar to parent class, but returns the request object as soon as it
|
||||
Similar to parent class, but returns the :obj:`request` object as soon as it
|
||||
has created it.
|
||||
"""
|
||||
environ = {
|
||||
|
@ -49,7 +67,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
|
||||
|
@ -125,4 +143,54 @@ except:
|
|||
# 'request': self.request
|
||||
# }
|
||||
#)
|
||||
return http.HttpResponseNotAllowed(allowed_methods)
|
||||
return http.HttpResponseNotAllowed(allowed_methods)
|
||||
|
||||
|
||||
try:
|
||||
import markdown
|
||||
import re
|
||||
|
||||
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
|
||||
"""
|
||||
Override `markdown`'s :class:`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 :func:`markdown.markdown` to apply our :class:`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:
|
||||
apply_markdown = None
|
|
@ -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 ''
|
|
@ -1,374 +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.http import HttpResponse
|
||||
from django.template import RequestContext, loader
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.response import NoContent, ResponseException
|
||||
from djangorestframework.validators import FormValidatorMixin
|
||||
from djangorestframework.utils import dict2xml, url_resolves
|
||||
from djangorestframework.markdownwrapper import apply_markdown
|
||||
from djangorestframework.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.description import get_name, get_description
|
||||
from djangorestframework import status
|
||||
|
||||
from urllib import quote_plus
|
||||
import string
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
|
||||
|
||||
|
||||
class EmitterMixin(object):
|
||||
"""Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
|
||||
|
||||
Default behaviour is to use standard HTTP Accept header content negotiation.
|
||||
Also supports overidding the content type by specifying an _accept= parameter in the URL.
|
||||
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead."""
|
||||
|
||||
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||
REWRITE_IE_ACCEPT_HEADER = True
|
||||
|
||||
request = None
|
||||
response = None
|
||||
emitters = ()
|
||||
|
||||
def emit(self, response):
|
||||
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
|
||||
self.response = response
|
||||
|
||||
try:
|
||||
emitter = self._determine_emitter(self.request)
|
||||
except ResponseException, exc:
|
||||
emitter = self.default_emitter
|
||||
response = exc.response
|
||||
|
||||
# Serialize the response content
|
||||
if response.has_content_body:
|
||||
content = emitter(self).emit(output=response.cleaned_content)
|
||||
else:
|
||||
content = emitter(self).emit()
|
||||
|
||||
# Munge DELETE Response code to allow us to return content
|
||||
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
|
||||
if response.status == 204:
|
||||
response.status = 200
|
||||
|
||||
# Build the HTTP Response
|
||||
# TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
|
||||
resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
|
||||
for (key, val) in response.headers.items():
|
||||
resp[key] = val
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def _determine_emitter(self, request):
|
||||
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
|
||||
and the content types that this Resource knows how to serve.
|
||||
|
||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
|
||||
|
||||
if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
|
||||
# Use _accept parameter override
|
||||
accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
|
||||
elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
|
||||
accept_list = ['text/html', '*/*']
|
||||
elif request.META.has_key('HTTP_ACCEPT'):
|
||||
# Use standard HTTP Accept negotiation
|
||||
accept_list = request.META["HTTP_ACCEPT"].split(',')
|
||||
else:
|
||||
# No accept header specified
|
||||
return self.default_emitter
|
||||
|
||||
# Parse the accept header into a dict of {qvalue: set of media types}
|
||||
# We ignore mietype parameters
|
||||
accept_dict = {}
|
||||
for token in accept_list:
|
||||
components = token.split(';')
|
||||
mimetype = components[0].strip()
|
||||
qvalue = Decimal('1.0')
|
||||
|
||||
if len(components) > 1:
|
||||
# Parse items that have a qvalue eg text/html;q=0.9
|
||||
try:
|
||||
(q, num) = components[-1].split('=')
|
||||
if q == 'q':
|
||||
qvalue = Decimal(num)
|
||||
except:
|
||||
# Skip malformed entries
|
||||
continue
|
||||
|
||||
if accept_dict.has_key(qvalue):
|
||||
accept_dict[qvalue].add(mimetype)
|
||||
else:
|
||||
accept_dict[qvalue] = set((mimetype,))
|
||||
|
||||
# Convert to a list of sets ordered by qvalue (highest first)
|
||||
accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
|
||||
|
||||
for accept_set in accept_sets:
|
||||
# Return any exact match
|
||||
for emitter in self.emitters:
|
||||
if emitter.media_type in accept_set:
|
||||
return emitter
|
||||
|
||||
# Return any subtype match
|
||||
for emitter in self.emitters:
|
||||
if emitter.media_type.split('/')[0] + '/*' in accept_set:
|
||||
return emitter
|
||||
|
||||
# Return default
|
||||
if '*/*' in accept_set:
|
||||
return self.default_emitter
|
||||
|
||||
|
||||
raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
|
||||
{'detail': 'Could not statisfy the client\'s Accept header',
|
||||
'available_types': self.emitted_media_types})
|
||||
|
||||
@property
|
||||
def emitted_media_types(self):
|
||||
"""Return an list of all the media types that this resource can emit."""
|
||||
return [emitter.media_type for emitter in self.emitters]
|
||||
|
||||
@property
|
||||
def default_emitter(self):
|
||||
"""Return the resource's most prefered emitter.
|
||||
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
|
||||
return self.emitters[0]
|
||||
|
||||
|
||||
|
||||
# TODO: Rename verbose to something more appropriate
|
||||
# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default,
|
||||
# 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=NoContent, verbose=False):
|
||||
"""By default emit simply returns the ouput as-is.
|
||||
Override this method to provide for other behaviour."""
|
||||
if output is NoContent:
|
||||
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=NoContent, verbose=False):
|
||||
if output is NoContent:
|
||||
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 = None
|
||||
|
||||
if isinstance(resource, FormValidatorMixin):
|
||||
# If we already have a bound form instance (IE provided by the input parser, then use that)
|
||||
if resource.bound_form_instance is not None:
|
||||
form_instance = resource.bound_form_instance
|
||||
|
||||
# Otherwise if we have a response that is valid against the form then use that
|
||||
if not form_instance and resource.response.has_content_body:
|
||||
try:
|
||||
form_instance = resource.get_bound_form(resource.response.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=NoContent):
|
||||
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=NoContent, verbose=False):
|
||||
if output is NoContent:
|
||||
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=NoContent, verbose=False):
|
||||
if output is NoContent:
|
||||
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 )
|
||||
|
||||
|
|
@ -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
|
|
@ -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 "<MediaType %s>" % (self.as_tuple(),)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.orig
|
||||
|
591
djangorestframework/mixins.py
Normal file
591
djangorestframework/mixins.py
Normal file
|
@ -0,0 +1,591 @@
|
|||
"""
|
||||
The :mod:`mixins` module provides a set of reusable `mixin`
|
||||
classes that can be added to a `View`.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.http import HttpResponse
|
||||
from django.http.multipartparser import LimitBytes
|
||||
|
||||
from djangorestframework import status
|
||||
from djangorestframework.parsers import FormParser, MultiPartParser
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||
|
||||
from decimal import Decimal
|
||||
import re
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
__all__ = (
|
||||
# Base behavior mixins
|
||||
'RequestMixin',
|
||||
'ResponseMixin',
|
||||
'AuthMixin',
|
||||
'ResourceMixin',
|
||||
# Reverse URL lookup behavior
|
||||
'InstanceMixin',
|
||||
# Model behavior mixins
|
||||
'ReadModelMixin',
|
||||
'CreateModelMixin',
|
||||
'UpdateModelMixin',
|
||||
'DeleteModelMixin',
|
||||
'ListModelMixin'
|
||||
)
|
||||
|
||||
|
||||
########## Request Mixin ##########
|
||||
|
||||
class RequestMixin(object):
|
||||
"""
|
||||
`Mixin` class to provide request parsing behavior.
|
||||
"""
|
||||
|
||||
_USE_FORM_OVERLOADING = True
|
||||
_METHOD_PARAM = '_method'
|
||||
_CONTENTTYPE_PARAM = '_content_type'
|
||||
_CONTENT_PARAM = '_content'
|
||||
|
||||
"""
|
||||
The set of request parsers that the view can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
||||
"""
|
||||
parsers = ()
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""
|
||||
Returns the HTTP method.
|
||||
|
||||
This should be used instead of just reading :const:`request.method`, as it allows the `method`
|
||||
to be overridden by using a hidden `form` field on a form POST request.
|
||||
"""
|
||||
if not hasattr(self, '_method'):
|
||||
self._load_method_and_content_type()
|
||||
return self._method
|
||||
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""
|
||||
Returns the content type header.
|
||||
|
||||
This should be used instead of ``request.META.get('HTTP_CONTENT_TYPE')``,
|
||||
as it allows the content type to be overridden by using a hidden form
|
||||
field on a form POST request.
|
||||
"""
|
||||
if not hasattr(self, '_content_type'):
|
||||
self._load_method_and_content_type()
|
||||
return self._content_type
|
||||
|
||||
|
||||
@property
|
||||
def DATA(self):
|
||||
"""
|
||||
Parses the request body and returns the data.
|
||||
|
||||
Similar to ``request.POST``, except that it handles arbitrary parsers,
|
||||
and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
if not hasattr(self, '_data'):
|
||||
self._load_data_and_files()
|
||||
return self._data
|
||||
|
||||
|
||||
@property
|
||||
def FILES(self):
|
||||
"""
|
||||
Parses the request body and returns the files.
|
||||
Similar to ``request.FILES``, except that it handles arbitrary parsers,
|
||||
and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
if not hasattr(self, '_files'):
|
||||
self._load_data_and_files()
|
||||
return self._files
|
||||
|
||||
|
||||
def _load_data_and_files(self):
|
||||
"""
|
||||
Parse the request content into self.DATA and self.FILES.
|
||||
"""
|
||||
if not hasattr(self, '_content_type'):
|
||||
self._load_method_and_content_type()
|
||||
|
||||
if not hasattr(self, '_data'):
|
||||
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
|
||||
|
||||
|
||||
def _load_method_and_content_type(self):
|
||||
"""
|
||||
Set the method and content_type, and then check if they've been overridden.
|
||||
"""
|
||||
self._method = self.request.method
|
||||
self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
|
||||
self._perform_form_overloading()
|
||||
|
||||
|
||||
def _get_stream(self):
|
||||
"""
|
||||
Returns an object that may be used to stream the request content.
|
||||
"""
|
||||
request = self.request
|
||||
|
||||
try:
|
||||
content_length = int(request.META.get('CONTENT_LENGTH', request.META.get('HTTP_CONTENT_LENGTH')))
|
||||
except (ValueError, TypeError):
|
||||
content_length = 0
|
||||
|
||||
# TODO: Add 1.3's LimitedStream to compat and use that.
|
||||
# NOTE: Currently only supports parsing request body as a stream with 1.3
|
||||
if content_length == 0:
|
||||
return None
|
||||
elif hasattr(request, 'read'):
|
||||
return request
|
||||
return StringIO(request.raw_post_data)
|
||||
|
||||
|
||||
def _perform_form_overloading(self):
|
||||
"""
|
||||
If this is a form POST request, then we need to check if the method and content/content_type have been
|
||||
overridden by setting them in hidden form fields or not.
|
||||
"""
|
||||
|
||||
# We only need to use form overloading on form POST requests.
|
||||
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
|
||||
return
|
||||
|
||||
# At this point we're committed to parsing the request as form data.
|
||||
self._data = data = self.request.POST
|
||||
self._files = self.request.FILES
|
||||
|
||||
# Method overloading - change the method and remove the param from the content.
|
||||
if self._METHOD_PARAM in data:
|
||||
self._method = data[self._METHOD_PARAM].upper()
|
||||
|
||||
# Content overloading - modify the content type, and re-parse.
|
||||
if self._CONTENT_PARAM in data and self._CONTENTTYPE_PARAM in data:
|
||||
self._content_type = data[self._CONTENTTYPE_PARAM]
|
||||
stream = StringIO(data[self._CONTENT_PARAM])
|
||||
(self._data, self._files) = self._parse(stream, self._content_type)
|
||||
|
||||
|
||||
def _parse(self, stream, content_type):
|
||||
"""
|
||||
Parse the request content.
|
||||
|
||||
May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request).
|
||||
"""
|
||||
if stream is None or content_type is None:
|
||||
return (None, None)
|
||||
|
||||
parsers = as_tuple(self.parsers)
|
||||
|
||||
for parser_cls in parsers:
|
||||
parser = parser_cls(self)
|
||||
if parser.can_handle_request(content_type):
|
||||
return parser.parse(stream)
|
||||
|
||||
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
{'error': 'Unsupported media type in request \'%s\'.' %
|
||||
content_type})
|
||||
|
||||
|
||||
@property
|
||||
def _parsed_media_types(self):
|
||||
"""
|
||||
Return a list of all the media types that this view can parse.
|
||||
"""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
|
||||
@property
|
||||
def _default_parser(self):
|
||||
"""
|
||||
Return the view's default parser class.
|
||||
"""
|
||||
return self.parsers[0]
|
||||
|
||||
|
||||
|
||||
########## ResponseMixin ##########
|
||||
|
||||
class ResponseMixin(object):
|
||||
"""
|
||||
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
||||
|
||||
Default behavior is to use standard HTTP Accept header content negotiation.
|
||||
Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL.
|
||||
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
|
||||
"""
|
||||
|
||||
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||
_IGNORE_IE_ACCEPT_HEADER = True
|
||||
|
||||
"""
|
||||
The set of response renderers that the view can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`renderers` module.
|
||||
"""
|
||||
renderers = ()
|
||||
|
||||
|
||||
# TODO: wrap this behavior around dispatch(), ensuring it works
|
||||
# out of the box with existing Django classes that use render_to_response.
|
||||
def render(self, response):
|
||||
"""
|
||||
Takes a :obj:`Response` object and returns an :obj:`HttpResponse`.
|
||||
"""
|
||||
self.response = response
|
||||
|
||||
try:
|
||||
renderer, media_type = self._determine_renderer(self.request)
|
||||
except ErrorResponse, exc:
|
||||
renderer = self._default_renderer(self)
|
||||
media_type = renderer.media_type
|
||||
response = exc.response
|
||||
|
||||
# Set the media type of the response
|
||||
# Note that the renderer *could* override it in .render() if required.
|
||||
response.media_type = renderer.media_type
|
||||
|
||||
# Serialize the response content
|
||||
if response.has_content_body:
|
||||
content = renderer.render(response.cleaned_content, media_type)
|
||||
else:
|
||||
content = renderer.render()
|
||||
|
||||
# Build the HTTP Response
|
||||
resp = HttpResponse(content, mimetype=response.media_type, status=response.status)
|
||||
for (key, val) in response.headers.items():
|
||||
resp[key] = val
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def _determine_renderer(self, request):
|
||||
"""
|
||||
Determines the appropriate renderer for the output, given the client's 'Accept' header,
|
||||
and the :attr:`renderers` set on this class.
|
||||
|
||||
Returns a 2-tuple of `(renderer, media_type)`
|
||||
|
||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
"""
|
||||
|
||||
if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None):
|
||||
# Use _accept parameter override
|
||||
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
||||
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
||||
request.META.has_key('HTTP_USER_AGENT') and
|
||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
|
||||
# Ignore MSIE's broken accept behavior and do something sensible instead
|
||||
accept_list = ['text/html', '*/*']
|
||||
elif request.META.has_key('HTTP_ACCEPT'):
|
||||
# Use standard HTTP Accept negotiation
|
||||
accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
|
||||
else:
|
||||
# No accept header specified
|
||||
return (self._default_renderer(self), self._default_renderer.media_type)
|
||||
|
||||
# Check the acceptable media types against each renderer,
|
||||
# attempting more specific media types first
|
||||
# NB. The inner loop here isn't as bad as it first looks :)
|
||||
# Worst case is we're looping over len(accept_list) * len(self.renderers)
|
||||
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
|
||||
|
||||
for media_type_lst in order_by_precedence(accept_list):
|
||||
for renderer in renderers:
|
||||
for media_type in media_type_lst:
|
||||
if renderer.can_handle_response(media_type):
|
||||
return renderer, media_type
|
||||
|
||||
# No acceptable renderers were found
|
||||
raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE,
|
||||
{'detail': 'Could not satisfy the client\'s Accept header',
|
||||
'available_types': self._rendered_media_types})
|
||||
|
||||
|
||||
@property
|
||||
def _rendered_media_types(self):
|
||||
"""
|
||||
Return an list of all the media types that this view can render.
|
||||
"""
|
||||
return [renderer.media_type for renderer in self.renderers]
|
||||
|
||||
@property
|
||||
def _default_renderer(self):
|
||||
"""
|
||||
Return the view's default renderer class.
|
||||
"""
|
||||
return self.renderers[0]
|
||||
|
||||
|
||||
########## Auth Mixin ##########
|
||||
|
||||
class AuthMixin(object):
|
||||
"""
|
||||
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
|
||||
"""
|
||||
|
||||
"""
|
||||
The set of authentication types that this view can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`authentication` module.
|
||||
"""
|
||||
authentication = ()
|
||||
|
||||
"""
|
||||
The set of permissions that will be enforced on this view.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`permissions` module.
|
||||
"""
|
||||
permissions = ()
|
||||
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
"""
|
||||
Returns the :obj:`user` for the current request, as determined by the set of
|
||||
:class:`authentication` classes applied to the :class:`View`.
|
||||
"""
|
||||
if not hasattr(self, '_user'):
|
||||
self._user = self._authenticate()
|
||||
return self._user
|
||||
|
||||
|
||||
def _authenticate(self):
|
||||
"""
|
||||
Attempt to authenticate the request using each authentication class in turn.
|
||||
Returns a ``User`` object, which may be ``AnonymousUser``.
|
||||
"""
|
||||
for authentication_cls in self.authentication:
|
||||
authentication = authentication_cls(self)
|
||||
user = authentication.authenticate(self.request)
|
||||
if user:
|
||||
return user
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
# TODO: wrap this behavior around dispatch()
|
||||
def _check_permissions(self):
|
||||
"""
|
||||
Check user permissions and either raise an ``ErrorResponse`` or return.
|
||||
"""
|
||||
user = self.user
|
||||
for permission_cls in self.permissions:
|
||||
permission = permission_cls(self)
|
||||
permission.check_permission(user)
|
||||
|
||||
|
||||
########## Resource Mixin ##########
|
||||
|
||||
class ResourceMixin(object):
|
||||
"""
|
||||
Provides request validation and response filtering behavior.
|
||||
|
||||
Should be a class as described in the :mod:`resources` module.
|
||||
|
||||
The :obj:`resource` is an object that maps a view onto it's representation on the server.
|
||||
|
||||
It provides validation on the content of incoming requests,
|
||||
and filters the object representation into a serializable object for the response.
|
||||
"""
|
||||
resource = None
|
||||
|
||||
@property
|
||||
def CONTENT(self):
|
||||
"""
|
||||
Returns the cleaned, validated request content.
|
||||
|
||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request).
|
||||
"""
|
||||
if not hasattr(self, '_content'):
|
||||
self._content = self.validate_request(self.DATA, self.FILES)
|
||||
return self._content
|
||||
|
||||
@property
|
||||
def PARAMS(self):
|
||||
"""
|
||||
Returns the cleaned, validated query parameters.
|
||||
|
||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request).
|
||||
"""
|
||||
return self.validate_request(self.request.GET)
|
||||
|
||||
@property
|
||||
def _resource(self):
|
||||
if self.resource:
|
||||
return self.resource(self)
|
||||
elif getattr(self, 'model', None):
|
||||
return ModelResource(self)
|
||||
elif getattr(self, 'form', None):
|
||||
return FormResource(self)
|
||||
elif getattr(self, '%s_form' % self.method.lower(), None):
|
||||
return FormResource(self)
|
||||
return Resource(self)
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
"""
|
||||
Given the request *data* and optional *files*, return the cleaned, validated content.
|
||||
May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
||||
"""
|
||||
return self._resource.validate_request(data, files)
|
||||
|
||||
def filter_response(self, obj):
|
||||
"""
|
||||
Given the response content, filter it into a serializable object.
|
||||
"""
|
||||
return self._resource.filter_response(obj)
|
||||
|
||||
def get_bound_form(self, content=None, method=None):
|
||||
return self._resource.get_bound_form(content, method=method)
|
||||
|
||||
|
||||
|
||||
##########
|
||||
|
||||
class InstanceMixin(object):
|
||||
"""
|
||||
`Mixin` class that is used to identify a `View` class as being the canonical identifier
|
||||
for the resources it is mapped to.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
"""
|
||||
Store the callable object on the resource class that has been associated with this view.
|
||||
"""
|
||||
view = super(InstanceMixin, cls).as_view(**initkwargs)
|
||||
resource = getattr(cls(**initkwargs), 'resource', None)
|
||||
if resource:
|
||||
# We do a little dance when we store the view callable...
|
||||
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
|
||||
# as a function when we later look it up (rather than turning it into a method).
|
||||
# This makes sure our URL reversing works ok.
|
||||
resource.view_callable = (view,)
|
||||
return view
|
||||
|
||||
|
||||
########## Model Mixins ##########
|
||||
|
||||
class ReadModelMixin(object):
|
||||
"""
|
||||
Behavior to read a `model` instance on GET requests
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class CreateModelMixin(object):
|
||||
"""
|
||||
Behavior to create a `model` instance on POST requests
|
||||
"""
|
||||
def post(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
# translated 'related_field' kwargs into 'related_field_id'
|
||||
for related_name in [field.name for field in model._meta.fields if isinstance(field, RelatedField)]:
|
||||
if kwargs.has_key(related_name):
|
||||
kwargs[related_name + '_id'] = kwargs[related_name]
|
||||
del kwargs[related_name]
|
||||
|
||||
all_kw_args = dict(self.CONTENT.items() + kwargs.items())
|
||||
if args:
|
||||
instance = model(pk=args[-1], **all_kw_args)
|
||||
else:
|
||||
instance = model(**all_kw_args)
|
||||
instance.save()
|
||||
headers = {}
|
||||
if hasattr(instance, 'get_absolute_url'):
|
||||
headers['Location'] = self.resource(self).url(instance)
|
||||
return Response(status.HTTP_201_CREATED, instance, headers)
|
||||
|
||||
|
||||
class UpdateModelMixin(object):
|
||||
"""
|
||||
Behavior to update a `model` instance on PUT requests
|
||||
"""
|
||||
def put(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = model.objects.get(**kwargs)
|
||||
|
||||
for (key, val) in self.CONTENT.items():
|
||||
setattr(instance, key, val)
|
||||
except model.DoesNotExist:
|
||||
instance = model(**self.CONTENT)
|
||||
instance.save()
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class DeleteModelMixin(object):
|
||||
"""
|
||||
Behavior to delete a `model` instance on DELETE requests
|
||||
"""
|
||||
def delete(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||
|
||||
instance.delete()
|
||||
return
|
||||
|
||||
|
||||
class ListModelMixin(object):
|
||||
"""
|
||||
Behavior to list a set of `model` instances on GET requests
|
||||
"""
|
||||
|
||||
# NB. Not obvious to me if it would be better to set this on the resource?
|
||||
#
|
||||
# Presumably it's more useful to have on the view, because that way you can
|
||||
# have multiple views across different querysets mapping to the same resource.
|
||||
#
|
||||
# Perhaps it ought to be:
|
||||
#
|
||||
# 1) View.queryset
|
||||
# 2) if None fall back to Resource.queryset
|
||||
# 3) if None fall back to Resource.model.objects.all()
|
||||
#
|
||||
# Any feedback welcomed.
|
||||
queryset = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
queryset = self.queryset if self.queryset else self.resource.model.objects.all()
|
||||
ordering = getattr(self.resource, 'ordering', None)
|
||||
if ordering:
|
||||
args = as_tuple(ordering)
|
||||
queryset = queryset.order_by(*args)
|
||||
return queryset.filter(**kwargs)
|
||||
|
||||
|
|
@ -1,431 +0,0 @@
|
|||
from django.forms import ModelForm
|
||||
from django.db.models import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.fields.related import RelatedField
|
||||
|
||||
from djangorestframework.response import Response, ResponseException
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.validators import ModelFormValidatorMixin
|
||||
from djangorestframework import status
|
||||
|
||||
import decimal
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
||||
class ModelResource(Resource, ModelFormValidatorMixin):
|
||||
"""A specialized type of Resource, for resources that map directly to a Django Model.
|
||||
Useful things this provides:
|
||||
|
||||
0. Default input validation based on ModelForms.
|
||||
1. Nice serialization of returned Models and QuerySets.
|
||||
2. A default set of create/read/update/delete operations."""
|
||||
|
||||
# The model attribute refers to the Django Model which this Resource maps to.
|
||||
# (The Model's class, rather than an instance of the Model)
|
||||
model = None
|
||||
|
||||
# By default the set of returned fields will be the set of:
|
||||
#
|
||||
# 0. All the fields on the model, excluding 'id'.
|
||||
# 1. All the properties on the model.
|
||||
# 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
|
||||
#
|
||||
# If you wish to override this behaviour,
|
||||
# you should explicitly set the fields attribute on your class.
|
||||
fields = None
|
||||
|
||||
# By default the form used with be a ModelForm for self.model
|
||||
# If you wish to override this behaviour or provide a sub-classed ModelForm
|
||||
# you should explicitly set the form attribute on your class.
|
||||
form = None
|
||||
|
||||
# By default the set of input fields will be the same as the set of output fields
|
||||
# If you wish to override this behaviour you should explicitly set the
|
||||
# form_fields attribute on your class.
|
||||
#form_fields = None
|
||||
|
||||
|
||||
#def get_form(self, content=None):
|
||||
# """Return a form that may be used in validation and/or rendering an html emitter"""
|
||||
# if self.form:
|
||||
# return super(self.__class__, self).get_form(content)
|
||||
#
|
||||
# elif self.model:
|
||||
#
|
||||
# class NewModelForm(ModelForm):
|
||||
# class Meta:
|
||||
# model = self.model
|
||||
# fields = self.form_fields if self.form_fields else None
|
||||
#
|
||||
# if content and isinstance(content, Model):
|
||||
# return NewModelForm(instance=content)
|
||||
# elif content:
|
||||
# return NewModelForm(content)
|
||||
#
|
||||
# return NewModelForm()
|
||||
#
|
||||
# return None
|
||||
|
||||
|
||||
#def cleanup_request(self, data, form_instance):
|
||||
# """Override cleanup_request to drop read-only fields from the input prior to validation.
|
||||
# This ensures that we don't error out with 'non-existent field' when these fields are supplied,
|
||||
# and allows for a pragmatic approach to resources which include read-only elements.
|
||||
#
|
||||
# I would actually like to be strict and verify the value of correctness of the values in these fields,
|
||||
# although that gets tricky as it involves validating at the point that we get the model instance.
|
||||
#
|
||||
# See here for another example of this approach:
|
||||
# http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
|
||||
# https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
|
||||
# read_only_fields = set(self.fields) - set(self.form_instance.fields)
|
||||
# input_fields = set(data.keys())
|
||||
#
|
||||
# clean_data = {}
|
||||
# for key in input_fields - read_only_fields:
|
||||
# clean_data[key] = data[key]
|
||||
#
|
||||
# return super(ModelResource, self).cleanup_request(clean_data, form_instance)
|
||||
|
||||
|
||||
def cleanup_response(self, data):
|
||||
"""A munging of Piston's pre-serialization. Returns a dict"""
|
||||
|
||||
def _any(thing, fields=()):
|
||||
"""
|
||||
Dispatch, all types are routed through here.
|
||||
"""
|
||||
ret = None
|
||||
|
||||
if isinstance(thing, QuerySet):
|
||||
ret = _qs(thing, fields=fields)
|
||||
elif isinstance(thing, (tuple, list)):
|
||||
ret = _list(thing)
|
||||
elif isinstance(thing, dict):
|
||||
ret = _dict(thing)
|
||||
elif isinstance(thing, int):
|
||||
ret = thing
|
||||
elif isinstance(thing, bool):
|
||||
ret = thing
|
||||
elif isinstance(thing, type(None)):
|
||||
ret = thing
|
||||
elif isinstance(thing, decimal.Decimal):
|
||||
ret = str(thing)
|
||||
elif isinstance(thing, Model):
|
||||
ret = _model(thing, fields=fields)
|
||||
#elif isinstance(thing, HttpResponse): TRC
|
||||
# raise HttpStatusCode(thing)
|
||||
elif inspect.isfunction(thing):
|
||||
if not inspect.getargspec(thing)[0]:
|
||||
ret = _any(thing())
|
||||
elif hasattr(thing, '__emittable__'):
|
||||
f = thing.__emittable__
|
||||
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
|
||||
ret = _any(f())
|
||||
else:
|
||||
ret = unicode(thing) # TRC TODO: Change this back!
|
||||
|
||||
return ret
|
||||
|
||||
def _fk(data, field):
|
||||
"""
|
||||
Foreign keys.
|
||||
"""
|
||||
return _any(getattr(data, field.name))
|
||||
|
||||
def _related(data, fields=()):
|
||||
"""
|
||||
Foreign keys.
|
||||
"""
|
||||
return [ _model(m, fields) for m in data.iterator() ]
|
||||
|
||||
def _m2m(data, field, fields=()):
|
||||
"""
|
||||
Many to many (re-route to `_model`.)
|
||||
"""
|
||||
return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
|
||||
|
||||
|
||||
def _method_fields(data, fields):
|
||||
if not data:
|
||||
return { }
|
||||
|
||||
has = dir(data)
|
||||
ret = dict()
|
||||
|
||||
for field in fields:
|
||||
if field in has:
|
||||
ret[field] = getattr(data, field)
|
||||
|
||||
return ret
|
||||
|
||||
def _model(data, fields=()):
|
||||
"""
|
||||
Models. Will respect the `fields` and/or
|
||||
`exclude` on the handler (see `typemapper`.)
|
||||
"""
|
||||
ret = { }
|
||||
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
|
||||
handler = None # TRC
|
||||
get_absolute_url = False
|
||||
|
||||
if handler or fields:
|
||||
v = lambda f: getattr(data, f.attname)
|
||||
|
||||
if not fields:
|
||||
"""
|
||||
Fields was not specified, try to find teh correct
|
||||
version in the typemapper we were sent.
|
||||
"""
|
||||
mapped = self.in_typemapper(type(data), self.anonymous)
|
||||
get_fields = set(mapped.fields)
|
||||
exclude_fields = set(mapped.exclude).difference(get_fields)
|
||||
|
||||
if not get_fields:
|
||||
get_fields = set([ f.attname.replace("_id", "", 1)
|
||||
for f in data._meta.fields ])
|
||||
|
||||
# sets can be negated.
|
||||
for exclude in exclude_fields:
|
||||
if isinstance(exclude, basestring):
|
||||
get_fields.discard(exclude)
|
||||
|
||||
elif isinstance(exclude, re._pattern_type):
|
||||
for field in get_fields.copy():
|
||||
if exclude.match(field):
|
||||
get_fields.discard(field)
|
||||
|
||||
get_absolute_url = True
|
||||
|
||||
else:
|
||||
get_fields = set(fields)
|
||||
if 'absolute_url' in get_fields: # MOVED (TRC)
|
||||
get_absolute_url = True
|
||||
|
||||
met_fields = _method_fields(handler, get_fields) # TRC
|
||||
|
||||
for f in data._meta.local_fields:
|
||||
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
|
||||
if not f.rel:
|
||||
if f.attname in get_fields:
|
||||
ret[f.attname] = _any(v(f))
|
||||
get_fields.remove(f.attname)
|
||||
else:
|
||||
if f.attname[:-3] in get_fields:
|
||||
ret[f.name] = _fk(data, f)
|
||||
get_fields.remove(f.name)
|
||||
|
||||
for mf in data._meta.many_to_many:
|
||||
if mf.serialize and mf.attname not in met_fields:
|
||||
if mf.attname in get_fields:
|
||||
ret[mf.name] = _m2m(data, mf)
|
||||
get_fields.remove(mf.name)
|
||||
|
||||
# try to get the remainder of fields
|
||||
for maybe_field in get_fields:
|
||||
|
||||
if isinstance(maybe_field, (list, tuple)):
|
||||
model, fields = maybe_field
|
||||
inst = getattr(data, model, None)
|
||||
|
||||
if inst:
|
||||
if hasattr(inst, 'all'):
|
||||
ret[model] = _related(inst, fields)
|
||||
elif callable(inst):
|
||||
if len(inspect.getargspec(inst)[0]) == 1:
|
||||
ret[model] = _any(inst(), fields)
|
||||
else:
|
||||
ret[model] = _model(inst, fields)
|
||||
|
||||
elif maybe_field in met_fields:
|
||||
# Overriding normal field which has a "resource method"
|
||||
# so you can alter the contents of certain fields without
|
||||
# using different names.
|
||||
ret[maybe_field] = _any(met_fields[maybe_field](data))
|
||||
|
||||
else:
|
||||
maybe = getattr(data, maybe_field, None)
|
||||
if maybe:
|
||||
if callable(maybe):
|
||||
if len(inspect.getargspec(maybe)[0]) == 1:
|
||||
ret[maybe_field] = _any(maybe())
|
||||
else:
|
||||
ret[maybe_field] = _any(maybe)
|
||||
else:
|
||||
pass # TRC
|
||||
#handler_f = getattr(handler or self.handler, maybe_field, None)
|
||||
#
|
||||
#if handler_f:
|
||||
# ret[maybe_field] = _any(handler_f(data))
|
||||
|
||||
else:
|
||||
# Add absolute_url if it exists
|
||||
get_absolute_url = True
|
||||
|
||||
# Add all the fields
|
||||
for f in data._meta.fields:
|
||||
if f.attname != 'id':
|
||||
ret[f.attname] = _any(getattr(data, f.attname))
|
||||
|
||||
# Add all the propertiess
|
||||
klass = data.__class__
|
||||
for attr in dir(klass):
|
||||
if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
|
||||
#if attr.endswith('_url') or attr.endswith('_uri'):
|
||||
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
|
||||
#else:
|
||||
ret[attr] = _any(getattr(data, attr))
|
||||
#fields = dir(data.__class__) + ret.keys()
|
||||
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
|
||||
#print add_ons
|
||||
###print dir(data.__class__)
|
||||
#from django.db.models import Model
|
||||
#model_fields = dir(Model)
|
||||
|
||||
#for attr in dir(data):
|
||||
## #if attr.startswith('_'):
|
||||
## # continue
|
||||
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
|
||||
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
|
||||
|
||||
#for k in add_ons:
|
||||
# ret[k] = _any(getattr(data, k))
|
||||
|
||||
# TRC
|
||||
# resouce uri
|
||||
#if self.in_typemapper(type(data), self.anonymous):
|
||||
# handler = self.in_typemapper(type(data), self.anonymous)
|
||||
# if hasattr(handler, 'resource_uri'):
|
||||
# url_id, fields = handler.resource_uri()
|
||||
# ret['resource_uri'] = permalink( lambda: (url_id,
|
||||
# (getattr(data, f) for f in fields) ) )()
|
||||
|
||||
# TRC
|
||||
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
|
||||
# try: ret['resource_uri'] = data.get_api_url()
|
||||
# except: pass
|
||||
|
||||
# absolute uri
|
||||
if hasattr(data, 'get_absolute_url') and get_absolute_url:
|
||||
try: ret['absolute_url'] = data.get_absolute_url()
|
||||
except: pass
|
||||
|
||||
#for key, val in ret.items():
|
||||
# if key.endswith('_url') or key.endswith('_uri'):
|
||||
# ret[key] = self.add_domain(val)
|
||||
|
||||
return ret
|
||||
|
||||
def _qs(data, fields=()):
|
||||
"""
|
||||
Querysets.
|
||||
"""
|
||||
return [ _any(v, fields) for v in data ]
|
||||
|
||||
def _list(data):
|
||||
"""
|
||||
Lists.
|
||||
"""
|
||||
return [ _any(v) for v in data ]
|
||||
|
||||
def _dict(data):
|
||||
"""
|
||||
Dictionaries.
|
||||
"""
|
||||
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
|
||||
|
||||
# Kickstart the seralizin'.
|
||||
return _any(data, self.fields)
|
||||
|
||||
|
||||
def post(self, request, auth, content, *args, **kwargs):
|
||||
# TODO: test creation on a non-existing resource url
|
||||
|
||||
# translated related_field into related_field_id
|
||||
for related_name in [field.name for field in self.model._meta.fields if isinstance(field, RelatedField)]:
|
||||
if kwargs.has_key(related_name):
|
||||
kwargs[related_name + '_id'] = kwargs[related_name]
|
||||
del kwargs[related_name]
|
||||
|
||||
all_kw_args = dict(content.items() + kwargs.items())
|
||||
if args:
|
||||
instance = self.model(pk=args[-1], **all_kw_args)
|
||||
else:
|
||||
instance = self.model(**all_kw_args)
|
||||
instance.save()
|
||||
headers = {}
|
||||
if hasattr(instance, 'get_absolute_url'):
|
||||
headers['Location'] = instance.get_absolute_url()
|
||||
return Response(status.HTTP_201_CREATED, instance, headers)
|
||||
|
||||
def get(self, request, auth, *args, **kwargs):
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
raise ResponseException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return instance
|
||||
|
||||
def put(self, request, auth, content, *args, **kwargs):
|
||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
|
||||
for (key, val) in content.items():
|
||||
setattr(instance, key, val)
|
||||
except self.model.DoesNotExist:
|
||||
instance = self.model(**content)
|
||||
instance.save()
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def delete(self, request, auth, *args, **kwargs):
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
raise ResponseException(status.HTTP_404_NOT_FOUND, None, {})
|
||||
|
||||
instance.delete()
|
||||
return
|
||||
|
||||
|
||||
class RootModelResource(ModelResource):
|
||||
"""A Resource which provides default operations for list and create."""
|
||||
allowed_methods = ('GET', 'POST')
|
||||
queryset = None
|
||||
|
||||
def get(self, request, auth, *args, **kwargs):
|
||||
queryset = self.queryset if self.queryset else self.model.objects.all()
|
||||
return queryset.filter(**kwargs)
|
||||
|
||||
|
||||
class QueryModelResource(ModelResource):
|
||||
"""Resource with default operations for list.
|
||||
TODO: provide filter/order/num_results/paging, and a create operation to create queries."""
|
||||
allowed_methods = ('GET',)
|
||||
queryset = None
|
||||
|
||||
def get_form(self, data=None):
|
||||
return None
|
||||
|
||||
def get(self, request, auth, *args, **kwargs):
|
||||
queryset = self.queryset if self.queryset else self.model.objects.all()
|
||||
return queryset.filer(**kwargs)
|
||||
|
|
@ -1,187 +1,141 @@
|
|||
"""Django supports parsing the content of an HTTP request, but only for form POST requests.
|
||||
That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well
|
||||
"""
|
||||
Django supports parsing the content of an HTTP request, but only for form POST requests.
|
||||
That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well
|
||||
to general HTTP requests.
|
||||
|
||||
We need a method to be able to:
|
||||
|
||||
1) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
|
||||
2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
|
||||
1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
|
||||
|
||||
2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
|
||||
and multipart/form-data. (eg also handle multipart/json)
|
||||
"""
|
||||
from django.http.multipartparser import MultiPartParser as DjangoMPParser
|
||||
|
||||
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.response import ResponseException
|
||||
from djangorestframework import status
|
||||
from djangorestframework.compat import parse_qs
|
||||
from djangorestframework.response import ErrorResponse
|
||||
from djangorestframework.utils import as_tuple
|
||||
from djangorestframework.mediatypes import MediaType
|
||||
from djangorestframework.utils.mediatypes import media_type_matches
|
||||
|
||||
try:
|
||||
from urlparse import parse_qs
|
||||
except ImportError:
|
||||
from cgi import parse_qs
|
||||
|
||||
class ParserMixin(object):
|
||||
parsers = ()
|
||||
|
||||
def parse(self, stream, content_type):
|
||||
"""
|
||||
Parse the request content.
|
||||
|
||||
May raise a 415 ResponseException (Unsupported Media Type),
|
||||
or a 400 ResponseException (Bad Request).
|
||||
"""
|
||||
parsers = as_tuple(self.parsers)
|
||||
|
||||
parser = None
|
||||
for parser_cls in parsers:
|
||||
if parser_cls.handles(content_type):
|
||||
parser = parser_cls(self)
|
||||
break
|
||||
|
||||
if parser is None:
|
||||
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
{'error': 'Unsupported media type in request \'%s\'.' %
|
||||
content_type.media_type})
|
||||
|
||||
return parser.parse(stream)
|
||||
|
||||
@property
|
||||
def parsed_media_types(self):
|
||||
"""Return an list of all the media types that this ParserMixin can parse."""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
@property
|
||||
def default_parser(self):
|
||||
"""Return the ParerMixin's most prefered emitter.
|
||||
(This has no behavioural effect, but is may be used by documenting emitters)"""
|
||||
return self.parsers[0]
|
||||
__all__ = (
|
||||
'BaseParser',
|
||||
'JSONParser',
|
||||
'PlainTextParser',
|
||||
'FormParser',
|
||||
'MultiPartParser',
|
||||
)
|
||||
|
||||
|
||||
class BaseParser(object):
|
||||
"""All parsers should extend BaseParser, specifying a media_type attribute,
|
||||
and overriding the parse() method."""
|
||||
"""
|
||||
All parsers should extend :class:`BaseParser`, specifying a :attr:`media_type` attribute,
|
||||
and overriding the :meth:`parse` method.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
|
||||
def __init__(self, view):
|
||||
"""
|
||||
Initialise the parser with the View instance as state,
|
||||
in case the parser needs to access any metadata on the View object.
|
||||
|
||||
Initialize the parser with the ``View`` instance as state,
|
||||
in case the parser needs to access any metadata on the :obj:`View` object.
|
||||
"""
|
||||
self.view = view
|
||||
|
||||
@classmethod
|
||||
def handles(self, media_type):
|
||||
def can_handle_request(self, content_type):
|
||||
"""
|
||||
Returns `True` if this parser is able to deal with the given MediaType.
|
||||
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
||||
|
||||
The default implementation for this function is to check the *content_type*
|
||||
argument against the :attr:`media_type` attribute set on the class to see if
|
||||
they match.
|
||||
|
||||
This may be overridden to provide for other behavior, but typically you'll
|
||||
instead want to just set the :attr:`media_type` attribute on the class.
|
||||
"""
|
||||
return media_type.match(self.media_type)
|
||||
return media_type_matches(self.media_type, content_type)
|
||||
|
||||
def parse(self, stream):
|
||||
"""Given a stream to read from, return the deserialized output.
|
||||
The return value may be of any type, but for many parsers it might typically be a dict-like object."""
|
||||
"""
|
||||
Given a *stream* to read from, return the deserialized output.
|
||||
Should return a 2-tuple of (data, files).
|
||||
"""
|
||||
raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
|
||||
|
||||
|
||||
class JSONParser(BaseParser):
|
||||
media_type = MediaType('application/json')
|
||||
"""
|
||||
Parses JSON-serialized data.
|
||||
"""
|
||||
|
||||
media_type = 'application/json'
|
||||
|
||||
def parse(self, stream):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be an object which is the parsed content of the response.
|
||||
`files` will always be `None`.
|
||||
"""
|
||||
try:
|
||||
return json.load(stream)
|
||||
return (json.load(stream), None)
|
||||
except ValueError, exc:
|
||||
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
|
||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
||||
{'detail': 'JSON parse error - %s' % unicode(exc)})
|
||||
|
||||
|
||||
class DataFlatener(object):
|
||||
"""Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data."""
|
||||
|
||||
def flatten_data(self, data):
|
||||
"""Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary
|
||||
with information provided by the method "is_a_list"."""
|
||||
flatdata = dict()
|
||||
for key, val_list in data.items():
|
||||
if self.is_a_list(key, val_list):
|
||||
flatdata[key] = val_list
|
||||
else:
|
||||
if val_list:
|
||||
flatdata[key] = val_list[0]
|
||||
else:
|
||||
# If the list is empty, but the parameter is not a list,
|
||||
# we strip this parameter.
|
||||
data.pop(key)
|
||||
return flatdata
|
||||
class PlainTextParser(BaseParser):
|
||||
"""
|
||||
Plain text parser.
|
||||
"""
|
||||
|
||||
def is_a_list(self, key, val_list):
|
||||
"""Returns True if the parameter with name *key* is expected to be a list, or False otherwise.
|
||||
*val_list* which is the received value for parameter *key* can be used to guess the answer."""
|
||||
return False
|
||||
|
||||
|
||||
class FormParser(BaseParser, DataFlatener):
|
||||
"""The default parser for form data.
|
||||
Return a dict containing a single value for each non-reserved parameter.
|
||||
|
||||
In order to handle select multiple (and having possibly more than a single value for each parameter),
|
||||
you can customize the output by subclassing the method 'is_a_list'."""
|
||||
|
||||
media_type = MediaType('application/x-www-form-urlencoded')
|
||||
|
||||
"""The value of the parameter when the select multiple is empty.
|
||||
Browsers are usually stripping the select multiple that have no option selected from the parameters sent.
|
||||
A common hack to avoid this is to send the parameter with a value specifying that the list is empty.
|
||||
This value will always be stripped before the data is returned."""
|
||||
EMPTY_VALUE = '_empty'
|
||||
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
|
||||
media_type = 'text/plain'
|
||||
|
||||
def parse(self, stream):
|
||||
data = parse_qs(stream.read(), keep_blank_values=True)
|
||||
|
||||
# removing EMPTY_VALUEs from the lists and flatening the data
|
||||
for key, val_list in data.items():
|
||||
self.remove_empty_val(val_list)
|
||||
data = self.flatten_data(data)
|
||||
|
||||
# Strip any parameters that we are treating as reserved
|
||||
for key in data.keys():
|
||||
if key in self.RESERVED_FORM_PARAMS:
|
||||
data.pop(key)
|
||||
|
||||
return data
|
||||
|
||||
def remove_empty_val(self, val_list):
|
||||
""" """
|
||||
while(1): # Because there might be several times EMPTY_VALUE in the list
|
||||
try:
|
||||
ind = val_list.index(self.EMPTY_VALUE)
|
||||
except ValueError:
|
||||
break
|
||||
else:
|
||||
val_list.pop(ind)
|
||||
|
||||
|
||||
class MultipartData(dict):
|
||||
def __init__(self, data, files):
|
||||
dict.__init__(self, data)
|
||||
self.FILES = files
|
||||
|
||||
class MultipartParser(BaseParser, DataFlatener):
|
||||
media_type = MediaType('multipart/form-data')
|
||||
RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
|
||||
|
||||
def parse(self, stream):
|
||||
upload_handlers = self.view.request._get_upload_handlers()
|
||||
django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers)
|
||||
data, files = django_mpp.parse()
|
||||
|
||||
# Flatening data, files and combining them
|
||||
data = self.flatten_data(dict(data.iterlists()))
|
||||
files = self.flatten_data(dict(files.iterlists()))
|
||||
|
||||
# Strip any parameters that we are treating as reserved
|
||||
for key in data.keys():
|
||||
if key in self.RESERVED_FORM_PARAMS:
|
||||
data.pop(key)
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
return MultipartData(data, files)
|
||||
`data` will simply be a string representing the body of the request.
|
||||
`files` will always be `None`.
|
||||
"""
|
||||
return (stream.read(), None)
|
||||
|
||||
|
||||
class FormParser(BaseParser):
|
||||
"""
|
||||
Parser for form data.
|
||||
"""
|
||||
|
||||
media_type = 'application/x-www-form-urlencoded'
|
||||
|
||||
def parse(self, stream):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||
`files` will always be :const:`None`.
|
||||
"""
|
||||
data = parse_qs(stream.read(), keep_blank_values=True)
|
||||
return (data, None)
|
||||
|
||||
|
||||
class MultiPartParser(BaseParser):
|
||||
"""
|
||||
Parser for multipart form data, which may include file data.
|
||||
"""
|
||||
|
||||
media_type = 'multipart/form-data'
|
||||
|
||||
def parse(self, stream):
|
||||
"""
|
||||
Returns a 2-tuple of `(data, files)`.
|
||||
|
||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||
`files` will be a :class:`QueryDict` containing all the form files.
|
||||
"""
|
||||
upload_handlers = self.view.request._get_upload_handlers()
|
||||
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
|
||||
return django_parser.parse()
|
||||
|
||||
|
|
124
djangorestframework/permissions.py
Normal file
124
djangorestframework/permissions.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
"""
|
||||
The :mod:`permissions` module bundles a set of permission classes that are used
|
||||
for checking if a request passes a certain set of constraints. You can assign a permision
|
||||
class to your view by setting your View's :attr:`permissions` class attribute.
|
||||
"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from djangorestframework import status
|
||||
from djangorestframework.response import ErrorResponse
|
||||
import time
|
||||
|
||||
__all__ = (
|
||||
'BasePermission',
|
||||
'FullAnonAccess',
|
||||
'IsAuthenticated',
|
||||
'IsAdminUser',
|
||||
'IsUserOrIsAnonReadOnly',
|
||||
'PerUserThrottling'
|
||||
)
|
||||
|
||||
|
||||
_403_FORBIDDEN_RESPONSE = ErrorResponse(
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
{'detail': 'You do not have permission to access this resource. ' +
|
||||
'You may need to login or otherwise authenticate the request.'})
|
||||
|
||||
_503_THROTTLED_RESPONSE = ErrorResponse(
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
{'detail': 'request was throttled'})
|
||||
|
||||
|
||||
|
||||
class BasePermission(object):
|
||||
"""
|
||||
A base class from which all permission classes should inherit.
|
||||
"""
|
||||
def __init__(self, view):
|
||||
"""
|
||||
Permission classes are always passed the current view on creation.
|
||||
"""
|
||||
self.view = view
|
||||
|
||||
def check_permission(self, auth):
|
||||
"""
|
||||
Should simply return, or raise an :exc:`response.ErrorResponse`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FullAnonAccess(BasePermission):
|
||||
"""
|
||||
Allows full access.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
pass
|
||||
|
||||
|
||||
class IsAuthenticated(BasePermission):
|
||||
"""
|
||||
Allows access only to authenticated users.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
if not user.is_authenticated():
|
||||
raise _403_FORBIDDEN_RESPONSE
|
||||
|
||||
|
||||
class IsAdminUser(BasePermission):
|
||||
"""
|
||||
Allows access only to admin users.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
if not user.is_admin():
|
||||
raise _403_FORBIDDEN_RESPONSE
|
||||
|
||||
|
||||
class IsUserOrIsAnonReadOnly(BasePermission):
|
||||
"""
|
||||
The request is authenticated as a user, or is a read-only request.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
if (not user.is_authenticated() and
|
||||
self.view.method != 'GET' and
|
||||
self.view.method != 'HEAD'):
|
||||
raise _403_FORBIDDEN_RESPONSE
|
||||
|
||||
|
||||
class PerUserThrottling(BasePermission):
|
||||
"""
|
||||
Rate throttling of requests on a per-user basis.
|
||||
|
||||
The rate (requests / seconds) is set by a :attr:`throttle` attribute on the ``View`` class.
|
||||
The attribute is a two tuple of the form (number of requests, duration in seconds).
|
||||
|
||||
The user id will be used as a unique identifier if the user is authenticated.
|
||||
For anonymous requests, the IP address of the client will be used.
|
||||
|
||||
Previous request information used for throttling is stored in the cache.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
(num_requests, duration) = getattr(self.view, 'throttle', (0, 0))
|
||||
|
||||
if user.is_authenticated():
|
||||
ident = str(auth)
|
||||
else:
|
||||
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
||||
|
||||
key = 'throttle_%s' % ident
|
||||
history = cache.get(key, [])
|
||||
now = time.time()
|
||||
|
||||
# Drop any requests from the history which have now passed the throttle duration
|
||||
while history and history[0] < now - duration:
|
||||
history.pop()
|
||||
|
||||
if len(history) >= num_requests:
|
||||
raise _503_THROTTLED_RESPONSE
|
||||
|
||||
history.insert(0, now)
|
||||
cache.set(key, history, duration)
|
347
djangorestframework/renderers.py
Normal file
347
djangorestframework/renderers.py
Normal file
|
@ -0,0 +1,347 @@
|
|||
"""
|
||||
Renderers are used to serialize a View'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 View, output status and headers,
|
||||
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||
"""
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DateTimeAwareJSONEncoder
|
||||
from django.template import RequestContext, loader
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework import status
|
||||
from djangorestframework.compat import apply_markdown
|
||||
from djangorestframework.utils import dict2xml, url_resolves
|
||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.utils.description import get_name, get_description
|
||||
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
||||
|
||||
from decimal import Decimal
|
||||
import re
|
||||
import string
|
||||
from urllib import quote_plus
|
||||
|
||||
__all__ = (
|
||||
'BaseRenderer',
|
||||
'TemplateRenderer',
|
||||
'JSONRenderer',
|
||||
'DocumentingHTMLRenderer',
|
||||
'DocumentingXHTMLRenderer',
|
||||
'DocumentingPlainTextRenderer',
|
||||
'XMLRenderer'
|
||||
)
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
"""
|
||||
All renderers must extend this class, set the :attr:`media_type` attribute,
|
||||
and override the :meth:`render` method.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
|
||||
def can_handle_response(self, accept):
|
||||
"""
|
||||
Returns :const:`True` if this renderer is able to deal with the given
|
||||
*accept* media type.
|
||||
|
||||
The default implementation for this function is to check the *accept*
|
||||
argument against the :attr:`media_type` attribute set on the class to see if
|
||||
they match.
|
||||
|
||||
This may be overridden to provide for other behavior, but typically you'll
|
||||
instead want to just set the :attr:`media_type` attribute on the class.
|
||||
"""
|
||||
return media_type_matches(self.media_type, accept)
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
Given an object render it into a string.
|
||||
|
||||
The requested media type is also passed to this method,
|
||||
as it may contain parameters relevant to how the parser
|
||||
should render the output.
|
||||
EG: ``application/json; indent=4``
|
||||
|
||||
By default render simply returns the output as-is.
|
||||
Override this method to provide for other behavior.
|
||||
"""
|
||||
if obj is None:
|
||||
return ''
|
||||
|
||||
return str(obj)
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderer which serializes to JSON
|
||||
"""
|
||||
|
||||
media_type = 'application/json'
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
Renders *obj* into serialized JSON.
|
||||
"""
|
||||
if obj is None:
|
||||
return ''
|
||||
|
||||
# If the media type looks like 'application/json; indent=4', then
|
||||
# pretty print the result.
|
||||
indent = get_media_type_params(media_type).get('indent', None)
|
||||
sort_keys = False
|
||||
try:
|
||||
indent = max(min(int(indent), 8), 0)
|
||||
sort_keys = True
|
||||
except (ValueError, TypeError):
|
||||
indent = None
|
||||
|
||||
return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys)
|
||||
|
||||
|
||||
class XMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderer which serializes to XML.
|
||||
"""
|
||||
media_type = 'application/xml'
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
Renders *obj* into serialized XML.
|
||||
"""
|
||||
if obj is None:
|
||||
return ''
|
||||
return dict2xml(obj)
|
||||
|
||||
|
||||
class TemplateRenderer(BaseRenderer):
|
||||
"""
|
||||
A Base class provided for convenience.
|
||||
|
||||
Render the object simply by using the given template.
|
||||
To create a template renderer, subclass this class, and set
|
||||
the :attr:`media_type` and :attr:`template` attributes.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
template = None
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
Renders *obj* using the :attr:`template` specified on the class.
|
||||
"""
|
||||
if obj is None:
|
||||
return ''
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(self.view.request, {'object': obj})
|
||||
return 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, view, request, obj, media_type):
|
||||
"""
|
||||
Get the content as if it had been rendered 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 view.renderers if not isinstance(renderer, DocumentingTemplateRenderer)]
|
||||
if not renderers:
|
||||
return '[No renderers were found]'
|
||||
|
||||
media_type = add_media_type_param(media_type, 'indent', '4')
|
||||
content = renderers[0](view).render(obj, media_type)
|
||||
if not all(char in string.printable for char in content):
|
||||
return '[%d bytes of binary content]'
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def _get_form_instance(self, view, method):
|
||||
"""
|
||||
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 = None
|
||||
if method == view.method.lower():
|
||||
form_instance = getattr(view, 'bound_form_instance', None)
|
||||
|
||||
if not form_instance and hasattr(view, 'get_bound_form'):
|
||||
# Otherwise if we have a response that is valid against the form then use that
|
||||
if view.response.has_content_body:
|
||||
try:
|
||||
form_instance = view.get_bound_form(view.response.cleaned_content, method=method)
|
||||
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 = view.get_bound_form(method=method)
|
||||
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(view)
|
||||
|
||||
return form_instance
|
||||
|
||||
|
||||
def _get_generic_content_form(self, view):
|
||||
"""
|
||||
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 view won't treat the form's value as the content of the request.
|
||||
if not getattr(view, '_USE_FORM_OVERLOADING', False):
|
||||
return None
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self, view):
|
||||
"""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 view._parsed_media_types]
|
||||
initial_contenttype = view._default_parser.media_type
|
||||
|
||||
self.fields[view._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
||||
choices=contenttype_choices,
|
||||
initial=initial_contenttype)
|
||||
self.fields[view._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.view._CONTENTTYPE_PARAM is None or self.view._CONTENT_PARAM is None:
|
||||
return None
|
||||
|
||||
# Okey doke, let's do it
|
||||
return GenericContentForm(view)
|
||||
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
"""
|
||||
Renders *obj* using the :attr:`template` set on the class.
|
||||
|
||||
The context used in the template contains all the information
|
||||
needed to self-document the response to this request.
|
||||
"""
|
||||
content = self._get_content(self.view, self.view.request, obj, media_type)
|
||||
|
||||
put_form_instance = self._get_form_instance(self.view, 'put')
|
||||
post_form_instance = self._get_form_instance(self.view, 'post')
|
||||
|
||||
if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
|
||||
login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.view.request.path))
|
||||
logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.view.request.path))
|
||||
else:
|
||||
login_url = None
|
||||
logout_url = None
|
||||
|
||||
name = get_name(self.view)
|
||||
description = get_description(self.view)
|
||||
|
||||
markeddown = None
|
||||
if apply_markdown:
|
||||
try:
|
||||
markeddown = apply_markdown(description)
|
||||
except AttributeError:
|
||||
markeddown = None
|
||||
|
||||
breadcrumb_list = get_breadcrumbs(self.view.request.path)
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(self.view.request, {
|
||||
'content': content,
|
||||
'view': self.view,
|
||||
'request': self.view.request, # TODO: remove
|
||||
'response': self.view.response,
|
||||
'description': description,
|
||||
'name': name,
|
||||
'markeddown': markeddown,
|
||||
'breadcrumblist': breadcrumb_list,
|
||||
'available_media_types': self.view._rendered_media_types,
|
||||
'put_form': put_form_instance,
|
||||
'post_form': post_form_instance,
|
||||
'login_url': login_url,
|
||||
'logout_url': logout_url,
|
||||
'ACCEPT_PARAM': getattr(self.view, '_ACCEPT_QUERY_PARAM', None),
|
||||
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
|
||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
|
||||
})
|
||||
|
||||
ret = template.render(context)
|
||||
|
||||
# Munge DELETE Response code to allow us to return content
|
||||
# (Do this *after* we've rendered the template so that we include
|
||||
# the normal deletion response code in the output)
|
||||
if self.view.response.status == 204:
|
||||
self.view.response.status = 200
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
|
||||
"""
|
||||
Renderer which provides a browsable HTML interface for an API.
|
||||
See the examples at http://api.django-rest-framework.org to see this in action.
|
||||
"""
|
||||
|
||||
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 object with the default renderer, but also provides plain-text
|
||||
documentation 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 )
|
||||
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
from djangorestframework.mediatypes import MediaType
|
||||
#from djangorestframework.requestparsing import parse, load_parser
|
||||
from django.http.multipartparser import LimitBytes
|
||||
from StringIO import StringIO
|
||||
|
||||
class RequestMixin(object):
|
||||
"""Delegate class that supplements an HttpRequest object with additional behaviour."""
|
||||
|
||||
USE_FORM_OVERLOADING = True
|
||||
METHOD_PARAM = "_method"
|
||||
CONTENTTYPE_PARAM = "_content_type"
|
||||
CONTENT_PARAM = "_content"
|
||||
|
||||
def _get_method(self):
|
||||
"""
|
||||
Returns the HTTP method for the current view.
|
||||
"""
|
||||
if not hasattr(self, '_method'):
|
||||
self._method = self.request.method
|
||||
return self._method
|
||||
|
||||
|
||||
def _set_method(self, method):
|
||||
"""
|
||||
Set the method for the current view.
|
||||
"""
|
||||
self._method = method
|
||||
|
||||
|
||||
def _get_content_type(self):
|
||||
"""
|
||||
Returns a MediaType object, representing the request's content type header.
|
||||
"""
|
||||
if not hasattr(self, '_content_type'):
|
||||
content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
|
||||
self._content_type = MediaType(content_type)
|
||||
return self._content_type
|
||||
|
||||
|
||||
def _set_content_type(self, content_type):
|
||||
"""
|
||||
Set the content type. Should be a MediaType object.
|
||||
"""
|
||||
self._content_type = content_type
|
||||
|
||||
|
||||
def _get_accept(self):
|
||||
"""
|
||||
Returns a list of MediaType objects, representing the request's accept header.
|
||||
"""
|
||||
if not hasattr(self, '_accept'):
|
||||
accept = self.request.META.get('HTTP_ACCEPT', '*/*')
|
||||
self._accept = [MediaType(elem) for elem in accept.split(',')]
|
||||
return self._accept
|
||||
|
||||
|
||||
def _set_accept(self):
|
||||
"""
|
||||
Set the acceptable media types. Should be a list of MediaType objects.
|
||||
"""
|
||||
self._accept = accept
|
||||
|
||||
|
||||
def _get_stream(self):
|
||||
"""
|
||||
Returns an object that may be used to stream the request content.
|
||||
"""
|
||||
if not hasattr(self, '_stream'):
|
||||
request = self.request
|
||||
|
||||
# Currently only supports parsing request body as a stream with 1.3
|
||||
if hasattr(request, 'read'):
|
||||
# It's not at all clear if this needs to be byte limited or not.
|
||||
# Maybe I'm just being dumb but it looks to me like there's some issues
|
||||
# with that in Django.
|
||||
#
|
||||
# Either:
|
||||
# 1. It *can't* be treated as a limited byte stream, and you _do_ need to
|
||||
# respect CONTENT_LENGTH, in which case that ought to be documented,
|
||||
# and there probably ought to be a feature request for it to be
|
||||
# treated as a limited byte stream.
|
||||
# 2. It *can* be treated as a limited byte stream, in which case there's a
|
||||
# minor bug in the test client, and potentially some redundant
|
||||
# code in MultipartParser.
|
||||
#
|
||||
# It's an issue because it affects if you can pass a request off to code that
|
||||
# does something like:
|
||||
#
|
||||
# while stream.read(BUFFER_SIZE):
|
||||
# [do stuff]
|
||||
#
|
||||
#try:
|
||||
# content_length = int(request.META.get('CONTENT_LENGTH',0))
|
||||
#except (ValueError, TypeError):
|
||||
# content_length = 0
|
||||
# self._stream = LimitedStream(request, content_length)
|
||||
self._stream = request
|
||||
else:
|
||||
self._stream = StringIO(request.raw_post_data)
|
||||
return self._stream
|
||||
|
||||
|
||||
def _set_stream(self, stream):
|
||||
"""
|
||||
Set the stream representing the request body.
|
||||
"""
|
||||
self._stream = stream
|
||||
|
||||
|
||||
def _get_raw_content(self):
|
||||
"""
|
||||
Returns the parsed content of the request
|
||||
"""
|
||||
if not hasattr(self, '_raw_content'):
|
||||
self._raw_content = self.parse(self.stream, self.content_type)
|
||||
return self._raw_content
|
||||
|
||||
|
||||
def _get_content(self):
|
||||
"""
|
||||
Returns the parsed and validated content of the request
|
||||
"""
|
||||
if not hasattr(self, '_content'):
|
||||
self._content = self.validate(self.RAW_CONTENT)
|
||||
|
||||
return self._content
|
||||
|
||||
|
||||
def perform_form_overloading(self):
|
||||
"""
|
||||
Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
|
||||
If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
|
||||
delegating them to the original request.
|
||||
"""
|
||||
if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form():
|
||||
return
|
||||
|
||||
content = self.RAW_CONTENT
|
||||
if self.METHOD_PARAM in content:
|
||||
self.method = content[self.METHOD_PARAM].upper()
|
||||
del self._raw_content[self.METHOD_PARAM]
|
||||
|
||||
if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
|
||||
self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
|
||||
self._stream = StringIO(content[self.CONTENT_PARAM])
|
||||
del(self._raw_content)
|
||||
|
||||
method = property(_get_method, _set_method)
|
||||
content_type = property(_get_content_type, _set_content_type)
|
||||
accept = property(_get_accept, _set_accept)
|
||||
stream = property(_get_stream, _set_stream)
|
||||
RAW_CONTENT = property(_get_raw_content)
|
||||
CONTENT = property(_get_content)
|
||||
|
||||
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from djangorestframework.compat import View
|
||||
from djangorestframework.emitters import EmitterMixin
|
||||
from djangorestframework.parsers import ParserMixin
|
||||
from djangorestframework.authenticators import AuthenticatorMixin
|
||||
from djangorestframework.validators import FormValidatorMixin
|
||||
from djangorestframework.response import Response, ResponseException
|
||||
from djangorestframework.request import RequestMixin
|
||||
from djangorestframework import emitters, parsers, authenticators, status
|
||||
|
||||
|
||||
# TODO: Figure how out references and named urls need to work nicely
|
||||
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
||||
#
|
||||
# NEXT: Exceptions on func() -> 500, tracebacks emitted if settings.DEBUG
|
||||
|
||||
__all__ = ['Resource']
|
||||
|
||||
|
||||
class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
|
||||
"""Handles incoming requests and maps them to REST operations,
|
||||
performing authentication, input deserialization, input validation, output serialization."""
|
||||
|
||||
# List of RESTful operations which may be performed on this resource.
|
||||
# These are going to get dropped at some point, the allowable methods will be defined simply by
|
||||
# which methods are present on the request (in the same way as Django's generic View)
|
||||
allowed_methods = ('GET',)
|
||||
anon_allowed_methods = ()
|
||||
|
||||
# List of emitters the resource can serialize the response with, ordered by preference.
|
||||
emitters = ( emitters.JSONEmitter,
|
||||
emitters.DocumentingHTMLEmitter,
|
||||
emitters.DocumentingXHTMLEmitter,
|
||||
emitters.DocumentingPlainTextEmitter,
|
||||
emitters.XMLEmitter )
|
||||
|
||||
# List of parsers the resource can parse the request with.
|
||||
parsers = ( parsers.JSONParser,
|
||||
parsers.FormParser,
|
||||
parsers.MultipartParser )
|
||||
|
||||
# List of all authenticating methods to attempt.
|
||||
authenticators = ( authenticators.UserLoggedInAuthenticator,
|
||||
authenticators.BasicAuthenticator )
|
||||
|
||||
# Optional form for input validation and presentation of HTML formatted responses.
|
||||
form = None
|
||||
|
||||
# Allow name and description for the Resource to be set explicitly,
|
||||
# overiding the default classname/docstring behaviour.
|
||||
# These are used for documentation in the standard html and text emitters.
|
||||
name = None
|
||||
description = None
|
||||
|
||||
# Map standard HTTP methods to function calls
|
||||
callmap = { 'GET': 'get', 'POST': 'post',
|
||||
'PUT': 'put', 'DELETE': 'delete' }
|
||||
|
||||
def get(self, request, auth, *args, **kwargs):
|
||||
"""Must be subclassed to be implemented."""
|
||||
self.not_implemented('GET')
|
||||
|
||||
|
||||
def post(self, request, auth, content, *args, **kwargs):
|
||||
"""Must be subclassed to be implemented."""
|
||||
self.not_implemented('POST')
|
||||
|
||||
|
||||
def put(self, request, auth, content, *args, **kwargs):
|
||||
"""Must be subclassed to be implemented."""
|
||||
self.not_implemented('PUT')
|
||||
|
||||
|
||||
def delete(self, request, auth, *args, **kwargs):
|
||||
"""Must be subclassed to be implemented."""
|
||||
self.not_implemented('DELETE')
|
||||
|
||||
|
||||
def not_implemented(self, operation):
|
||||
"""Return an HTTP 500 server error if an operation is called which has been allowed by
|
||||
allowed_methods, but which has not been implemented."""
|
||||
raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||
|
||||
|
||||
def check_method_allowed(self, method, auth):
|
||||
"""Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
|
||||
|
||||
if not method in self.callmap.keys():
|
||||
raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED,
|
||||
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
||||
|
||||
if not method in self.allowed_methods:
|
||||
raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||
|
||||
if auth is None and not method in self.anon_allowed_methods:
|
||||
raise ResponseException(status.HTTP_403_FORBIDDEN,
|
||||
{'detail': 'You do not have permission to access this resource. ' +
|
||||
'You may need to login or otherwise authenticate the request.'})
|
||||
|
||||
|
||||
def cleanup_response(self, data):
|
||||
"""Perform any resource-specific data filtering prior to the standard HTTP
|
||||
content-type serialization.
|
||||
|
||||
Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
|
||||
|
||||
TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
|
||||
the EmitterMixin and Emitter classes."""
|
||||
return data
|
||||
|
||||
# Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""This method is the core of Resource, through which all requests are passed.
|
||||
|
||||
Broadly this consists of the following procedure:
|
||||
|
||||
0. ensure the operation is permitted
|
||||
1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
|
||||
2. cleanup and validate request data (PUT/POST only)
|
||||
3. call the core method to get the response data
|
||||
4. cleanup the response data
|
||||
5. serialize response data into response content, using standard HTTP content negotiation
|
||||
"""
|
||||
|
||||
self.request = request
|
||||
|
||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||
set_script_prefix(prefix)
|
||||
|
||||
try:
|
||||
# Authenticate the request, and store any context so that the resource operations can
|
||||
# do more fine grained authentication if required.
|
||||
#
|
||||
# Typically the context will be a user, or None if this is an anonymous request,
|
||||
# but it could potentially be more complex (eg the context of a request key which
|
||||
# has been signed against a particular set of permissions)
|
||||
auth_context = self.authenticate(request)
|
||||
|
||||
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
||||
# self.method, self.content_type, self.CONTENT appropriately.
|
||||
self.perform_form_overloading()
|
||||
|
||||
# Ensure the requested operation is permitted on this resource
|
||||
self.check_method_allowed(self.method, auth_context)
|
||||
|
||||
# Get the appropriate create/read/update/delete function
|
||||
func = getattr(self, self.callmap.get(self.method, None))
|
||||
|
||||
# Either generate the response data, deserializing and validating any request data
|
||||
# TODO: This is going to change to: func(request, *args, **kwargs)
|
||||
# That'll work out now that we have the lazily evaluated self.CONTENT property.
|
||||
if self.method in ('PUT', 'POST'):
|
||||
response_obj = func(request, auth_context, self.CONTENT, *args, **kwargs)
|
||||
|
||||
else:
|
||||
response_obj = func(request, auth_context, *args, **kwargs)
|
||||
|
||||
# Allow return value to be either Response, or an object, or None
|
||||
if isinstance(response_obj, Response):
|
||||
response = response_obj
|
||||
elif response_obj is not None:
|
||||
response = Response(status.HTTP_200_OK, response_obj)
|
||||
else:
|
||||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.cleanup_response(response.raw_content)
|
||||
|
||||
except ResponseException, exc:
|
||||
response = exc.response
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
# TODO - this isn't actually the correct way to set the vary header,
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
response.headers['Vary'] = 'Authenticate, Accept'
|
||||
|
||||
return self.emit(response)
|
||||
|
494
djangorestframework/resources.py
Normal file
494
djangorestframework/resources.py
Normal file
|
@ -0,0 +1,494 @@
|
|||
from django import forms
|
||||
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.utils.encoding import smart_unicode
|
||||
|
||||
from djangorestframework.response import ErrorResponse
|
||||
from djangorestframework.utils import as_tuple
|
||||
|
||||
import decimal
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
||||
# TODO: _IgnoreFieldException
|
||||
|
||||
# Map model classes to resource classes
|
||||
#_model_to_resource = {}
|
||||
|
||||
|
||||
def _model_to_dict(instance, resource=None):
|
||||
"""
|
||||
Given a model instance, return a ``dict`` representing the model.
|
||||
|
||||
The implementation is similar to Django's ``django.forms.model_to_dict``, except:
|
||||
|
||||
* It doesn't coerce related objects into primary keys.
|
||||
* It doesn't drop ``editable=False`` fields.
|
||||
* It also supports attribute or method fields on the instance or resource.
|
||||
"""
|
||||
opts = instance._meta
|
||||
data = {}
|
||||
|
||||
#print [rel.name for rel in opts.get_all_related_objects()]
|
||||
#related = [rel.get_accessor_name() for rel in opts.get_all_related_objects()]
|
||||
#print [getattr(instance, rel) for rel in related]
|
||||
#if resource.fields:
|
||||
# fields = resource.fields
|
||||
#else:
|
||||
# fields = set(opts.fields + opts.many_to_many)
|
||||
|
||||
fields = resource and resource.fields or ()
|
||||
include = resource and resource.include or ()
|
||||
exclude = resource and resource.exclude or ()
|
||||
|
||||
extra_fields = fields and list(resource.fields) or []
|
||||
|
||||
# Model fields
|
||||
for f in opts.fields + opts.many_to_many:
|
||||
if fields and not f.name in fields:
|
||||
continue
|
||||
if exclude and f.name in exclude:
|
||||
continue
|
||||
if isinstance(f, models.ForeignKey):
|
||||
data[f.name] = getattr(instance, f.name)
|
||||
else:
|
||||
data[f.name] = f.value_from_object(instance)
|
||||
|
||||
if extra_fields and f.name in extra_fields:
|
||||
extra_fields.remove(f.name)
|
||||
|
||||
# Method fields
|
||||
for fname in extra_fields:
|
||||
if hasattr(resource, fname):
|
||||
# check the resource first, to allow it to override fields
|
||||
obj = getattr(resource, fname)
|
||||
# if it's a method like foo(self, instance), then call it
|
||||
if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) == 2:
|
||||
obj = obj(instance)
|
||||
elif hasattr(instance, fname):
|
||||
# now check the object instance
|
||||
obj = getattr(instance, fname)
|
||||
else:
|
||||
continue
|
||||
|
||||
# TODO: It would be nicer if this didn't recurse here.
|
||||
# Let's keep _model_to_dict flat, and _object_to_data recursive.
|
||||
data[fname] = _object_to_data(obj)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _object_to_data(obj, resource=None):
|
||||
"""
|
||||
Convert an object into a serializable representation.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
# dictionaries
|
||||
# TODO: apply same _model_to_dict logic fields/exclude here
|
||||
return dict([ (key, _object_to_data(val)) for key, val in obj.iteritems() ])
|
||||
if isinstance(obj, (tuple, list, set, QuerySet)):
|
||||
# basic iterables
|
||||
return [_object_to_data(item, resource) for item in obj]
|
||||
if isinstance(obj, models.Manager):
|
||||
# Manager objects
|
||||
return [_object_to_data(item, resource) for item in obj.all()]
|
||||
if isinstance(obj, models.Model):
|
||||
# Model instances
|
||||
return _object_to_data(_model_to_dict(obj, resource))
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
# Decimals (force to string representation)
|
||||
return str(obj)
|
||||
if inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
|
||||
# function with no args
|
||||
return _object_to_data(obj(), resource)
|
||||
if inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
|
||||
# bound method
|
||||
return _object_to_data(obj(), resource)
|
||||
|
||||
return smart_unicode(obj, strings_only=True)
|
||||
|
||||
|
||||
class BaseResource(object):
|
||||
"""
|
||||
Base class for all Resource classes, which simply defines the interface they provide.
|
||||
"""
|
||||
fields = None
|
||||
include = None
|
||||
exclude = None
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
"""
|
||||
Given the request content return the cleaned, validated content.
|
||||
Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
||||
"""
|
||||
return data
|
||||
|
||||
def filter_response(self, obj):
|
||||
"""
|
||||
Given the response content, filter it into a serializable object.
|
||||
"""
|
||||
return _object_to_data(obj, self)
|
||||
|
||||
|
||||
class Resource(BaseResource):
|
||||
"""
|
||||
A Resource determines how a python object maps to some serializable data.
|
||||
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
|
||||
"""
|
||||
|
||||
# The model attribute refers to the Django Model which this Resource maps to.
|
||||
# (The Model's class, rather than an instance of the Model)
|
||||
model = None
|
||||
|
||||
# By default the set of returned fields will be the set of:
|
||||
#
|
||||
# 0. All the fields on the model, excluding 'id'.
|
||||
# 1. All the properties on the model.
|
||||
# 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
|
||||
#
|
||||
# If you wish to override this behaviour,
|
||||
# you should explicitly set the fields attribute on your class.
|
||||
fields = None
|
||||
|
||||
|
||||
class FormResource(Resource):
|
||||
"""
|
||||
Resource class that uses forms for validation.
|
||||
Also provides a :meth:`get_bound_form` method which may be used by some renderers.
|
||||
|
||||
On calling :meth:`validate_request` this validator may set a :attr:`bound_form_instance` attribute on the
|
||||
view, which may be used by some renderers.
|
||||
"""
|
||||
|
||||
"""
|
||||
The :class:`Form` class that should be used for request validation.
|
||||
This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
|
||||
"""
|
||||
form = None
|
||||
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
||||
|
||||
Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied.
|
||||
|
||||
On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
||||
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
|
||||
If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``.
|
||||
"""
|
||||
return self._validate(data, files)
|
||||
|
||||
|
||||
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
|
||||
"""
|
||||
Wrapped by validate to hide the extra flags that are used in the implementation.
|
||||
|
||||
allowed_extra_fields is a list of fields which are not defined by the form, but which we still
|
||||
expect to see on the input.
|
||||
|
||||
fake_data is a string that should be used as an extra key, as a kludge to force .errors
|
||||
to be populated when an empty dict is supplied in `data`
|
||||
"""
|
||||
|
||||
# We'd like nice error messages even if no content is supplied.
|
||||
# Typically if an empty dict is given to a form Django will
|
||||
# return .is_valid() == False, but .errors == {}
|
||||
#
|
||||
# To get around this case we revalidate with some fake data.
|
||||
if fake_data:
|
||||
data[fake_data] = '_fake_data'
|
||||
allowed_extra_fields = allowed_extra_fields + ('_fake_data',)
|
||||
|
||||
bound_form = self.get_bound_form(data, files)
|
||||
|
||||
if bound_form is None:
|
||||
return data
|
||||
|
||||
self.view.bound_form_instance = bound_form
|
||||
|
||||
data = data and data or {}
|
||||
files = files and files or {}
|
||||
|
||||
seen_fields_set = set(data.keys())
|
||||
form_fields_set = set(bound_form.fields.keys())
|
||||
allowed_extra_fields_set = set(allowed_extra_fields)
|
||||
|
||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept')) # TODO: Ugh.
|
||||
|
||||
# Check using both regular validation, and our stricter no additional fields rule
|
||||
if bound_form.is_valid() and not unknown_fields:
|
||||
# Validation succeeded...
|
||||
cleaned_data = bound_form.cleaned_data
|
||||
|
||||
cleaned_data.update(bound_form.files)
|
||||
|
||||
# Add in any extra fields to the cleaned content...
|
||||
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
|
||||
cleaned_data[key] = data[key]
|
||||
|
||||
return cleaned_data
|
||||
|
||||
# Validation failed...
|
||||
detail = {}
|
||||
|
||||
if not bound_form.errors and not unknown_fields:
|
||||
# is_valid() was False, but errors was empty.
|
||||
# If we havn't already done so attempt revalidation with some fake data
|
||||
# to force django to give us an errors dict.
|
||||
if fake_data is None:
|
||||
return self._validate(data, files, allowed_extra_fields, '_fake_data')
|
||||
|
||||
# If we've already set fake_dict and we're still here, fallback gracefully.
|
||||
detail = {u'errors': [u'No content was supplied.']}
|
||||
|
||||
else:
|
||||
# Add any non-field errors
|
||||
if bound_form.non_field_errors():
|
||||
detail[u'errors'] = bound_form.non_field_errors()
|
||||
|
||||
# Add standard field errors
|
||||
field_errors = dict(
|
||||
(key, map(unicode, val))
|
||||
for (key, val)
|
||||
in bound_form.errors.iteritems()
|
||||
if not key.startswith('__')
|
||||
)
|
||||
|
||||
# Add any unknown field errors
|
||||
for key in unknown_fields:
|
||||
field_errors[key] = [u'This field does not exist.']
|
||||
|
||||
if field_errors:
|
||||
detail[u'field-errors'] = field_errors
|
||||
|
||||
# Return HTTP 400 response (BAD REQUEST)
|
||||
raise ErrorResponse(400, detail)
|
||||
|
||||
|
||||
def get_bound_form(self, data=None, files=None, method=None):
|
||||
"""
|
||||
Given some content return a Django form bound to that content.
|
||||
If form validation is turned off (:attr:`form` class attribute is :const:`None`) then returns :const:`None`.
|
||||
"""
|
||||
|
||||
# A form on the view overrides a form on the resource.
|
||||
form = getattr(self.view, 'form', self.form)
|
||||
|
||||
# Use the requested method or determine the request method
|
||||
if method is None and hasattr(self.view, 'request') and hasattr(self.view, 'method'):
|
||||
method = self.view.method
|
||||
elif method is None and hasattr(self.view, 'request'):
|
||||
method = self.view.request.method
|
||||
|
||||
# A method form on the view or resource overrides the general case.
|
||||
# Method forms are attributes like `get_form` `post_form` `put_form`.
|
||||
if method:
|
||||
form = getattr(self, '%s_form' % method.lower(), form)
|
||||
form = getattr(self.view, '%s_form' % method.lower(), form)
|
||||
|
||||
if not form:
|
||||
return None
|
||||
|
||||
if data is not None:
|
||||
return form(data, files)
|
||||
|
||||
return form()
|
||||
|
||||
|
||||
|
||||
#class _RegisterModelResource(type):
|
||||
# """
|
||||
# Auto register new ModelResource classes into ``_model_to_resource``
|
||||
# """
|
||||
# def __new__(cls, name, bases, dct):
|
||||
# resource_cls = type.__new__(cls, name, bases, dct)
|
||||
# model_cls = dct.get('model', None)
|
||||
# if model_cls:
|
||||
# _model_to_resource[model_cls] = resource_cls
|
||||
# return resource_cls
|
||||
|
||||
|
||||
|
||||
class ModelResource(FormResource):
|
||||
"""
|
||||
Resource class that uses forms for validation and otherwise falls back to a model form if no form is set.
|
||||
Also provides a :meth:`get_bound_form` method which may be used by some renderers.
|
||||
"""
|
||||
|
||||
# Auto-register new ModelResource classes into _model_to_resource
|
||||
#__metaclass__ = _RegisterModelResource
|
||||
|
||||
"""
|
||||
The form class that should be used for request validation.
|
||||
If set to :const:`None` then the default model form validation will be used.
|
||||
|
||||
This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
|
||||
"""
|
||||
form = None
|
||||
|
||||
"""
|
||||
The model class which this resource maps to.
|
||||
|
||||
This can be overridden by a :attr:`model` attribute on the :class:`views.View`.
|
||||
"""
|
||||
model = None
|
||||
|
||||
"""
|
||||
The list of fields to use on the output.
|
||||
|
||||
May be any of:
|
||||
|
||||
The name of a model field.
|
||||
The name of an attribute on the model.
|
||||
The name of an attribute on the resource.
|
||||
The name of a method on the model, with a signature like ``func(self)``.
|
||||
The name of a method on the resource, with a signature like ``func(self, instance)``.
|
||||
"""
|
||||
fields = None
|
||||
|
||||
"""
|
||||
The list of fields to exclude. This is only used if :attr:`fields` is not set.
|
||||
"""
|
||||
exclude = ('id', 'pk')
|
||||
|
||||
"""
|
||||
The list of extra fields to include. This is only used if :attr:`fields` is not set.
|
||||
"""
|
||||
include = ('url',)
|
||||
|
||||
|
||||
def __init__(self, view):
|
||||
"""
|
||||
Allow :attr:`form` and :attr:`model` attributes set on the
|
||||
:class:`View` to override the :attr:`form` and :attr:`model`
|
||||
attributes set on the :class:`Resource`.
|
||||
"""
|
||||
super(ModelResource, self).__init__(view)
|
||||
|
||||
if getattr(view, 'model', None):
|
||||
self.model = view.model
|
||||
|
||||
def validate_request(self, data, files=None):
|
||||
"""
|
||||
Given some content as input return some cleaned, validated content.
|
||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
||||
|
||||
Validation is standard form or model form validation,
|
||||
with an additional constraint that no extra unknown fields may be supplied,
|
||||
and that all fields specified by the fields class attribute must be supplied,
|
||||
even if they are not validated by the form/model form.
|
||||
|
||||
On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
||||
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
|
||||
If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}.
|
||||
"""
|
||||
return self._validate(data, files, allowed_extra_fields=self._property_fields_set)
|
||||
|
||||
|
||||
def get_bound_form(self, data=None, files=None, method=None):
|
||||
"""
|
||||
Given some content return a ``Form`` instance bound to that content.
|
||||
|
||||
If the :attr:`form` class attribute has been explicitly set then that class will be used
|
||||
to create the Form, otherwise the model will be used to create a ModelForm.
|
||||
"""
|
||||
|
||||
form = super(ModelResource, self).get_bound_form(data, files, method=method)
|
||||
|
||||
# Use an explict Form if it exists
|
||||
if form:
|
||||
return form
|
||||
|
||||
elif self.model:
|
||||
# Fall back to ModelForm which we create on the fly
|
||||
class OnTheFlyModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
#fields = tuple(self._model_fields_set)
|
||||
|
||||
# Instantiate the ModelForm as appropriate
|
||||
if data and isinstance(data, models.Model):
|
||||
# Bound to an existing model instance
|
||||
return OnTheFlyModelForm(instance=content)
|
||||
elif data is not None:
|
||||
return OnTheFlyModelForm(data, files)
|
||||
return OnTheFlyModelForm()
|
||||
|
||||
# Both form and model not set? Okay bruv, whatevs...
|
||||
return None
|
||||
|
||||
|
||||
def url(self, instance):
|
||||
"""
|
||||
Attempts to reverse resolve the url of the given model *instance* for this resource.
|
||||
|
||||
Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource.
|
||||
|
||||
This method can be overridden if you need to set the resource url reversing explicitly.
|
||||
"""
|
||||
|
||||
# dis does teh magicks...
|
||||
urlconf = get_urlconf()
|
||||
resolver = get_resolver(urlconf)
|
||||
|
||||
possibilities = resolver.reverse_dict.getlist(self.view_callable[0])
|
||||
for tuple_item in possibilities:
|
||||
possibility = tuple_item[0]
|
||||
# pattern = tuple_item[1]
|
||||
# Note: defaults = tuple_item[2] for django >= 1.3
|
||||
for result, params in possibility:
|
||||
|
||||
#instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ])
|
||||
|
||||
instance_attrs = {}
|
||||
for param in params:
|
||||
if not hasattr(instance, param):
|
||||
continue
|
||||
attr = getattr(instance, param)
|
||||
if isinstance(attr, models.Model):
|
||||
instance_attrs[param] = attr.pk
|
||||
else:
|
||||
instance_attrs[param] = attr
|
||||
|
||||
try:
|
||||
return reverse(self.view_callable[0], kwargs=instance_attrs)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
raise NoReverseMatch
|
||||
|
||||
|
||||
@property
|
||||
def _model_fields_set(self):
|
||||
"""
|
||||
Return a set containing the names of validated fields on the model.
|
||||
"""
|
||||
model_fields = set(field.name for field in self.model._meta.fields)
|
||||
|
||||
if fields:
|
||||
return model_fields & set(as_tuple(self.fields))
|
||||
|
||||
return model_fields - set(as_tuple(self.exclude))
|
||||
|
||||
@property
|
||||
def _property_fields_set(self):
|
||||
"""
|
||||
Returns a set containing the names of validated properties on the model.
|
||||
"""
|
||||
property_fields = set(attr for attr in dir(self.model) if
|
||||
isinstance(getattr(self.model, attr, None), property)
|
||||
and not attr.startswith('_'))
|
||||
|
||||
if self.fields:
|
||||
return property_fields & set(as_tuple(self.fields))
|
||||
|
||||
return property_fields.union(set(as_tuple(self.include))) - set(as_tuple(self.exclude))
|
|
@ -1,33 +1,43 @@
|
|||
"""
|
||||
The :mod:`response` module provides Response classes you can use in your
|
||||
views to return a certain HTTP response. Typically a response is *rendered*
|
||||
into a HTTP response depending on what renderers are set on your view and
|
||||
als depending on the accept header of the request.
|
||||
"""
|
||||
|
||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||
|
||||
__all__ =['NoContent', 'Response', ]
|
||||
|
||||
|
||||
|
||||
class NoContent(object):
|
||||
"""Used to indicate no body in http response.
|
||||
(We cannot just use None, as that is a valid, serializable response object.)
|
||||
|
||||
TODO: On relflection I'm going to get rid of this and just not support serailized 'None' responses.
|
||||
"""
|
||||
pass
|
||||
__all__ = ('Response', 'ErrorResponse')
|
||||
|
||||
# TODO: remove raw_content/cleaned_content and just use content?
|
||||
|
||||
class Response(object):
|
||||
def __init__(self, status=200, content=NoContent, headers={}):
|
||||
"""
|
||||
An HttpResponse that may include content that hasn't yet been serialized.
|
||||
"""
|
||||
|
||||
def __init__(self, status=200, content=None, headers={}):
|
||||
self.status = status
|
||||
self.has_content_body = not content is NoContent # TODO: remove and just use content
|
||||
self.raw_content = content # content prior to filtering - TODO: remove and just use content
|
||||
self.cleaned_content = content # content after filtering TODO: remove and just use content
|
||||
self.media_type = None
|
||||
self.has_content_body = content is not None
|
||||
self.raw_content = content # content prior to filtering
|
||||
self.cleaned_content = content # content after filtering
|
||||
self.headers = headers
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
"""Return reason text corrosponding to our HTTP response status code.
|
||||
Provided for convienience."""
|
||||
"""
|
||||
Return reason text corresponding to our HTTP response status code.
|
||||
Provided for convenience.
|
||||
"""
|
||||
return STATUS_CODE_TEXT.get(self.status, '')
|
||||
|
||||
|
||||
class ResponseException(BaseException):
|
||||
def __init__(self, status, content=NoContent, headers={}):
|
||||
class ErrorResponse(BaseException):
|
||||
"""
|
||||
An exception representing an Response that should be returned immediately.
|
||||
Any content should be serialized as-is, without being filtered.
|
||||
"""
|
||||
|
||||
def __init__(self, status, content=None, headers={}):
|
||||
self.response = Response(status, content=content, headers=headers)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Descriptive HTTP status codes, for code readability.
|
||||
"""
|
||||
Descriptive HTTP status codes, for code readability.
|
||||
|
||||
See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
Also, django.core.handlers.wsgi.STATUS_CODE_TEXT"""
|
||||
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
|
||||
"""
|
||||
|
||||
# Verbose format
|
||||
HTTP_100_CONTINUE = 100
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div id="content" class="colM">
|
||||
|
||||
<div id="content-main">
|
||||
<form method="post" action="{% url djangorestframework.views.api_login %}" id="login-form">
|
||||
<form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<label for="id_username">Username:</label> {{ form.username }}
|
||||
|
|
|
@ -42,15 +42,15 @@
|
|||
{% endfor %}
|
||||
{{ content|urlize_quoted_links }}</pre>{% endautoescape %}</div>
|
||||
|
||||
{% if 'GET' in resource.allowed_methods %}
|
||||
{% if 'GET' in view.allowed_methods %}
|
||||
<form>
|
||||
<fieldset class='module aligned'>
|
||||
<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 %}
|
||||
{% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
|
||||
[<a href='{{ request.path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]
|
||||
<a href='{{ request.get_full_path }}' rel="nofollow" style='float: left'>GET</a>
|
||||
{% for media_type in available_media_types %}
|
||||
{% with ACCEPT_PARAM|add:"="|add:media_type as param %}
|
||||
[<a href='{{ request.get_full_path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>]
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -58,19 +58,16 @@
|
|||
</form>
|
||||
{% 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 %}
|
||||
<form action="{{ request.path }}" method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||
{# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled. #}
|
||||
{% if METHOD_PARAM %}
|
||||
|
||||
{% if 'POST' in view.allowed_methods %}
|
||||
<form action="{{ request.get_full_path }}" method="post" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||
<fieldset class='module aligned'>
|
||||
<h2>POST {{ name }}</h2>
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form %}
|
||||
{{ post_form.non_field_errors }}
|
||||
{% for field in post_form %}
|
||||
<div class='form-row'>
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
|
@ -85,14 +82,14 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if 'PUT' in resource.allowed_methods %}
|
||||
<form action="{{ request.path }}" method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||
{% if 'PUT' in view.allowed_methods %}
|
||||
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||
<fieldset class='module aligned'>
|
||||
<h2>PUT {{ name }}</h2>
|
||||
<input type="hidden" name="{{ resource.METHOD_PARAM }}" value="PUT" />
|
||||
<input type="hidden" name="{{ METHOD_PARAM }}" value="PUT" />
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form %}
|
||||
{{ put_form.non_field_errors }}
|
||||
{% for field in put_form %}
|
||||
<div class='form-row'>
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
|
@ -107,18 +104,19 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if 'DELETE' in resource.allowed_methods %}
|
||||
<form action="{{ request.path }}" method="post">
|
||||
{% if 'DELETE' in view.allowed_methods %}
|
||||
<form action="{{ request.get_full_path }}" method="post">
|
||||
<fieldset class='module aligned'>
|
||||
<h2>DELETE {{ name }}</h2>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ resource.METHOD_PARAM }}" value="DELETE" />
|
||||
<input type="hidden" name="{{ METHOD_PARAM }}" value="DELETE" />
|
||||
<div class='submit-row' style='margin: 0; border: 0'>
|
||||
<input type="submit" value="DELETE" class="default" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -4,7 +4,7 @@ from urllib import quote
|
|||
register = Library()
|
||||
|
||||
def add_query_param(url, param):
|
||||
(key, val) = param.split('=')
|
||||
(key, sep, val) = param.partition('=')
|
||||
param = '%s=%s' % (key, quote(val))
|
||||
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
||||
if query:
|
||||
|
|
|
@ -63,11 +63,11 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
# Make URL we want to point to.
|
||||
url = None
|
||||
if middle.startswith('http://') or middle.startswith('https://'):
|
||||
url = urlquote(middle, safe='/&=:;#?+*')
|
||||
url = middle
|
||||
elif middle.startswith('www.') or ('@' not in middle and \
|
||||
middle and middle[0] in string.ascii_letters + string.digits and \
|
||||
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
|
||||
url = urlquote('http://%s' % middle, safe='/&=:;#?+*')
|
||||
url = 'http://%s' % middle
|
||||
elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
|
||||
url = 'mailto:%s' % middle
|
||||
nofollow_attr = ''
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
|
||||
|
||||
# See: http://www.useragentstring.com/
|
||||
|
@ -18,13 +18,16 @@ class UserAgentMungingTest(TestCase):
|
|||
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
|
||||
|
||||
def setUp(self):
|
||||
class MockResource(Resource):
|
||||
anon_allowed_methods = allowed_methods = ('GET',)
|
||||
def get(self, request, auth):
|
||||
|
||||
class MockView(View):
|
||||
permissions = ()
|
||||
|
||||
def get(self, request):
|
||||
return {'a':1, 'b':2, 'c':3}
|
||||
|
||||
self.req = RequestFactory()
|
||||
self.MockResource = MockResource
|
||||
self.view = MockResource.as_view()
|
||||
self.MockView = MockView
|
||||
self.view = MockView.as_view()
|
||||
|
||||
def test_munge_msie_accept_header(self):
|
||||
"""Send MSIE user agent strings and ensure that we get an HTML response,
|
||||
|
@ -37,9 +40,9 @@ class UserAgentMungingTest(TestCase):
|
|||
self.assertEqual(resp['Content-Type'], 'text/html')
|
||||
|
||||
def test_dont_rewrite_msie_accept_header(self):
|
||||
"""Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||
that we get a JSON response if we set a */* accept header."""
|
||||
view = self.MockResource.as_view(REWRITE_IE_ACCEPT_HEADER=False)
|
||||
view = self.MockView.as_view(_IGNORE_IE_ACCEPT_HEADER=False)
|
||||
|
||||
for user_agent in (MSIE_9_USER_AGENT,
|
||||
MSIE_8_USER_AGENT,
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import login
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import login
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework import permissions
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
class MockResource(Resource):
|
||||
allowed_methods = ('POST',)
|
||||
|
||||
def post(self, request, auth, content):
|
||||
class MockView(View):
|
||||
permissions = ( permissions.IsAuthenticated, )
|
||||
def post(self, request):
|
||||
return {'a':1, 'b':2, 'c':3}
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', MockResource.as_view()),
|
||||
(r'^$', MockView.as_view()),
|
||||
)
|
||||
|
||||
|
||||
|
@ -86,3 +87,4 @@ class SessionAuthTests(TestCase):
|
|||
"""Ensure POSTing form over session authentication without logged in user fails."""
|
||||
response = self.csrf_client.post('/', {'example': 'example'})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django.test import TestCase
|
||||
from djangorestframework.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.views import View
|
||||
|
||||
class Root(Resource):
|
||||
class Root(View):
|
||||
pass
|
||||
|
||||
class ResourceRoot(Resource):
|
||||
class ResourceRoot(View):
|
||||
pass
|
||||
|
||||
class ResourceInstance(Resource):
|
||||
class ResourceInstance(View):
|
||||
pass
|
||||
|
||||
class NestedResourceRoot(Resource):
|
||||
class NestedResourceRoot(View):
|
||||
pass
|
||||
|
||||
class NestedResourceInstance(Resource):
|
||||
class NestedResourceInstance(View):
|
||||
pass
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', Root),
|
||||
url(r'^resource/$', ResourceRoot),
|
||||
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance),
|
||||
url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot),
|
||||
url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance),
|
||||
url(r'^$', Root.as_view()),
|
||||
url(r'^resource/$', ResourceRoot.as_view()),
|
||||
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
|
||||
url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
|
||||
url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
|
||||
)
|
||||
|
||||
|
||||
class BreadcrumbTests(TestCase):
|
||||
"""Tests the breadcrumb functionality used by the HTML emitter."""
|
||||
"""Tests the breadcrumb functionality used by the HTML renderer."""
|
||||
|
||||
urls = 'djangorestframework.tests.breadcrumbs'
|
||||
|
||||
|
|
|
@ -1,122 +1,78 @@
|
|||
# TODO: refactor these tests
|
||||
#from django.test import TestCase
|
||||
#from djangorestframework.compat import RequestFactory
|
||||
#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
|
||||
#
|
||||
#
|
||||
#class TestContentMixins(TestCase):
|
||||
# def setUp(self):
|
||||
# self.req = RequestFactory()
|
||||
#
|
||||
# # Interface tests
|
||||
#
|
||||
# def test_content_mixin_interface(self):
|
||||
# """Ensure the ContentMixin interface is as expected."""
|
||||
# self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
|
||||
#
|
||||
# def test_standard_content_mixin_interface(self):
|
||||
# """Ensure the OverloadedContentMixin interface is as expected."""
|
||||
# self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
|
||||
# getattr(StandardContentMixin, 'determine_content')
|
||||
#
|
||||
# def test_overloaded_content_mixin_interface(self):
|
||||
# """Ensure the OverloadedContentMixin interface is as expected."""
|
||||
# self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
|
||||
# getattr(OverloadedContentMixin, 'CONTENT_PARAM')
|
||||
# getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
|
||||
# getattr(OverloadedContentMixin, 'determine_content')
|
||||
#
|
||||
#
|
||||
# # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
|
||||
#
|
||||
# def ensure_determines_no_content_GET(self, mixin):
|
||||
# """Ensure determine_content(request) returns None for GET request with no content."""
|
||||
# request = self.req.get('/')
|
||||
# self.assertEqual(mixin.determine_content(request), None)
|
||||
#
|
||||
# def ensure_determines_form_content_POST(self, mixin):
|
||||
# """Ensure determine_content(request) returns content for POST request with content."""
|
||||
# form_data = {'qwerty': 'uiop'}
|
||||
# request = self.req.post('/', data=form_data)
|
||||
# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
|
||||
#
|
||||
# def ensure_determines_non_form_content_POST(self, mixin):
|
||||
# """Ensure determine_content(request) returns (content type, content) for POST request with content."""
|
||||
# content = 'qwerty'
|
||||
# content_type = 'text/plain'
|
||||
# request = self.req.post('/', content, content_type=content_type)
|
||||
# self.assertEqual(mixin.determine_content(request), (content_type, content))
|
||||
#
|
||||
# def ensure_determines_form_content_PUT(self, mixin):
|
||||
# """Ensure determine_content(request) returns content for PUT request with content."""
|
||||
# form_data = {'qwerty': 'uiop'}
|
||||
# request = self.req.put('/', data=form_data)
|
||||
# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
|
||||
#
|
||||
# def ensure_determines_non_form_content_PUT(self, mixin):
|
||||
# """Ensure determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
# content = 'qwerty'
|
||||
# content_type = 'text/plain'
|
||||
# request = self.req.put('/', content, content_type=content_type)
|
||||
# self.assertEqual(mixin.determine_content(request), (content_type, content))
|
||||
#
|
||||
# # StandardContentMixin behavioural tests
|
||||
#
|
||||
# def test_standard_behaviour_determines_no_content_GET(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
|
||||
# self.ensure_determines_no_content_GET(StandardContentMixin())
|
||||
#
|
||||
# def test_standard_behaviour_determines_form_content_POST(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
|
||||
# self.ensure_determines_form_content_POST(StandardContentMixin())
|
||||
#
|
||||
# def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
|
||||
# self.ensure_determines_non_form_content_POST(StandardContentMixin())
|
||||
#
|
||||
# def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
|
||||
# self.ensure_determines_form_content_PUT(StandardContentMixin())
|
||||
#
|
||||
# def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
# self.ensure_determines_non_form_content_PUT(StandardContentMixin())
|
||||
#
|
||||
# # OverloadedContentMixin behavioural tests
|
||||
#
|
||||
# def test_overloaded_behaviour_determines_no_content_GET(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
|
||||
# self.ensure_determines_no_content_GET(OverloadedContentMixin())
|
||||
#
|
||||
# def test_overloaded_behaviour_determines_form_content_POST(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
|
||||
# self.ensure_determines_form_content_POST(OverloadedContentMixin())
|
||||
#
|
||||
# def test_overloaded_behaviour_determines_non_form_content_POST(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
|
||||
# self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
|
||||
#
|
||||
# def test_overloaded_behaviour_determines_form_content_PUT(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
|
||||
# self.ensure_determines_form_content_PUT(OverloadedContentMixin())
|
||||
#
|
||||
# def test_overloaded_behaviour_determines_non_form_content_PUT(self):
|
||||
# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
|
||||
# self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
|
||||
#
|
||||
# def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
# """Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
|
||||
# content = 'qwerty'
|
||||
# content_type = 'text/plain'
|
||||
# form_data = {OverloadedContentMixin.CONTENT_PARAM: content,
|
||||
# OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
|
||||
# request = self.req.post('/', form_data)
|
||||
# self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
|
||||
# self.assertEqual(request.META['CONTENT_TYPE'], content_type)
|
||||
#
|
||||
# def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
|
||||
# """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
|
||||
# content = 'qwerty'
|
||||
# request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
|
||||
# self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
|
||||
"""
|
||||
Tests for content parsing, and form-overloaded content parsing.
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.mixins import RequestMixin
|
||||
from djangorestframework.parsers import FormParser, MultiPartParser, PlainTextParser
|
||||
|
||||
|
||||
class TestContentParsing(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
def ensure_determines_no_content_GET(self, view):
|
||||
"""Ensure view.DATA returns None for GET request with no content."""
|
||||
view.request = self.req.get('/')
|
||||
self.assertEqual(view.DATA, None)
|
||||
|
||||
def ensure_determines_form_content_POST(self, view):
|
||||
"""Ensure view.DATA returns content for POST request with form content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
self.assertEqual(view.DATA.items(), form_data.items())
|
||||
|
||||
def ensure_determines_non_form_content_POST(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for POST request with non-form content."""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
view.parsers = (PlainTextParser,)
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
||||
def ensure_determines_form_content_PUT(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with form content."""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.put('/', data=form_data)
|
||||
self.assertEqual(view.DATA.items(), form_data.items())
|
||||
|
||||
def ensure_determines_non_form_content_PUT(self, view):
|
||||
"""Ensure view.RAW_CONTENT returns content for PUT request with non-form content."""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
view.parsers = (PlainTextParser,)
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
"""Ensure view.DATA returns None for GET request with no content."""
|
||||
self.ensure_determines_no_content_GET(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_POST(self):
|
||||
"""Ensure view.DATA returns content for POST request with form content."""
|
||||
self.ensure_determines_form_content_POST(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_POST(self):
|
||||
"""Ensure view.DATA returns content for POST request with non-form content."""
|
||||
self.ensure_determines_non_form_content_POST(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_form_content_PUT(self):
|
||||
"""Ensure view.DATA returns content for PUT request with form content."""
|
||||
self.ensure_determines_form_content_PUT(RequestMixin())
|
||||
|
||||
def test_standard_behaviour_determines_non_form_content_PUT(self):
|
||||
"""Ensure view.DATA returns content for PUT request with non-form content."""
|
||||
self.ensure_determines_non_form_content_PUT(RequestMixin())
|
||||
|
||||
def test_overloaded_behaviour_allows_content_tunnelling(self):
|
||||
"""Ensure request.DATA returns content for overloaded POST request"""
|
||||
content = 'qwerty'
|
||||
content_type = 'text/plain'
|
||||
view = RequestMixin()
|
||||
form_data = {view._CONTENT_PARAM: content,
|
||||
view._CONTENTTYPE_PARAM: content_type}
|
||||
view.request = self.req.post('/', form_data)
|
||||
view.parsers = (PlainTextParser,)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
|
|
@ -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.views import View
|
||||
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
|
||||
|
@ -32,23 +32,24 @@ MARKED_DOWN = """<h2>an example docstring</h2>
|
|||
<h2 id="hash_style_header">hash style header</h2>"""
|
||||
|
||||
|
||||
class TestResourceNamesAndDescriptions(TestCase):
|
||||
class TestViewNamesAndDescriptions(TestCase):
|
||||
def test_resource_name_uses_classname_by_default(self):
|
||||
"""Ensure Resource names are based on the classname by default."""
|
||||
class MockResource(Resource):
|
||||
class MockView(View):
|
||||
pass
|
||||
self.assertEquals(get_name(MockResource()), 'Mock Resource')
|
||||
self.assertEquals(get_name(MockView()), 'Mock')
|
||||
|
||||
def test_resource_name_can_be_set_explicitly(self):
|
||||
"""Ensure Resource names can be set using the 'name' class attribute."""
|
||||
example = 'Some Other Name'
|
||||
class MockResource(Resource):
|
||||
name = example
|
||||
self.assertEquals(get_name(MockResource()), example)
|
||||
# This has been turned off now.
|
||||
#def test_resource_name_can_be_set_explicitly(self):
|
||||
# """Ensure Resource names can be set using the 'name' class attribute."""
|
||||
# example = 'Some Other Name'
|
||||
# class MockView(View):
|
||||
# name = example
|
||||
# self.assertEquals(get_name(MockView()), example)
|
||||
|
||||
def test_resource_description_uses_docstring_by_default(self):
|
||||
"""Ensure Resource names are based on the docstring by default."""
|
||||
class MockResource(Resource):
|
||||
class MockView(View):
|
||||
"""an example docstring
|
||||
====================
|
||||
|
||||
|
@ -64,28 +65,29 @@ class TestResourceNamesAndDescriptions(TestCase):
|
|||
|
||||
# hash style header #"""
|
||||
|
||||
self.assertEquals(get_description(MockResource()), DESCRIPTION)
|
||||
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
||||
|
||||
def test_resource_description_can_be_set_explicitly(self):
|
||||
"""Ensure Resource descriptions can be set using the 'description' class attribute."""
|
||||
example = 'Some other description'
|
||||
class MockResource(Resource):
|
||||
"""docstring"""
|
||||
description = example
|
||||
self.assertEquals(get_description(MockResource()), example)
|
||||
# This has been turned off now
|
||||
#def test_resource_description_can_be_set_explicitly(self):
|
||||
# """Ensure Resource descriptions can be set using the 'description' class attribute."""
|
||||
# example = 'Some other description'
|
||||
# class MockView(View):
|
||||
# """docstring"""
|
||||
# description = example
|
||||
# self.assertEquals(get_description(MockView()), example)
|
||||
|
||||
def test_resource_description_does_not_require_docstring(self):
|
||||
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
||||
example = 'Some other description'
|
||||
class MockResource(Resource):
|
||||
description = example
|
||||
self.assertEquals(get_description(MockResource()), example)
|
||||
#def test_resource_description_does_not_require_docstring(self):
|
||||
# """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
||||
# example = 'Some other description'
|
||||
# class MockView(View):
|
||||
# description = example
|
||||
# self.assertEquals(get_description(MockView()), example)
|
||||
|
||||
def test_resource_description_can_be_empty(self):
|
||||
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
|
||||
class MockResource(Resource):
|
||||
class MockView(View):
|
||||
pass
|
||||
self.assertEquals(get_description(MockResource()), '')
|
||||
self.assertEquals(get_description(MockView()), '')
|
||||
|
||||
def test_markdown(self):
|
||||
"""Ensure markdown to HTML works as expected"""
|
||||
|
|
|
@ -1,75 +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 EmitterMixin, BaseEmitter
|
||||
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(EmitterMixin, 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)
|
|
@ -1,7 +1,8 @@
|
|||
from django.test import TestCase
|
||||
from django import forms
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.resources import FormResource
|
||||
import StringIO
|
||||
|
||||
class UploadFilesTests(TestCase):
|
||||
|
@ -15,19 +16,21 @@ class UploadFilesTests(TestCase):
|
|||
class FileForm(forms.Form):
|
||||
file = forms.FileField
|
||||
|
||||
class MockResource(Resource):
|
||||
allowed_methods = anon_allowed_methods = ('POST',)
|
||||
class MockResource(FormResource):
|
||||
form = FileForm
|
||||
|
||||
def post(self, request, auth, content, *args, **kwargs):
|
||||
#self.uploaded = content.file
|
||||
return {'FILE_NAME': content['file'].name,
|
||||
'FILE_CONTENT': content['file'].read()}
|
||||
class MockView(View):
|
||||
permissions = ()
|
||||
resource = MockResource
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return {'FILE_NAME': self.CONTENT['file'][0].name,
|
||||
'FILE_CONTENT': self.CONTENT['file'][0].read()}
|
||||
|
||||
file = StringIO.StringIO('stuff')
|
||||
file.name = 'stuff.txt'
|
||||
request = self.factory.post('/', {'file': file})
|
||||
view = MockResource.as_view()
|
||||
view = MockView.as_view()
|
||||
response = view(request)
|
||||
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
||||
|
||||
|
|
|
@ -1,53 +1,26 @@
|
|||
# TODO: Refactor these tests
|
||||
#from django.test import TestCase
|
||||
#from djangorestframework.compat import RequestFactory
|
||||
#from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
|
||||
#
|
||||
#
|
||||
#class TestMethodMixins(TestCase):
|
||||
# def setUp(self):
|
||||
# self.req = RequestFactory()
|
||||
#
|
||||
# # Interface tests
|
||||
#
|
||||
# def test_method_mixin_interface(self):
|
||||
# """Ensure the base ContentMixin interface is as expected."""
|
||||
# self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
|
||||
#
|
||||
# def test_standard_method_mixin_interface(self):
|
||||
# """Ensure the StandardMethodMixin interface is as expected."""
|
||||
# self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
|
||||
# getattr(StandardMethodMixin, 'determine_method')
|
||||
#
|
||||
# def test_overloaded_method_mixin_interface(self):
|
||||
# """Ensure the OverloadedPOSTMethodMixin interface is as expected."""
|
||||
# self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
|
||||
# getattr(OverloadedPOSTMethodMixin, 'METHOD_PARAM')
|
||||
# getattr(OverloadedPOSTMethodMixin, 'determine_method')
|
||||
#
|
||||
# # Behavioural tests
|
||||
#
|
||||
# def test_standard_behaviour_determines_GET(self):
|
||||
# """GET requests identified as GET method with StandardMethodMixin"""
|
||||
# request = self.req.get('/')
|
||||
# self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
|
||||
#
|
||||
# def test_standard_behaviour_determines_POST(self):
|
||||
# """POST requests identified as POST method with StandardMethodMixin"""
|
||||
# request = self.req.post('/')
|
||||
# self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
|
||||
#
|
||||
# def test_overloaded_POST_behaviour_determines_GET(self):
|
||||
# """GET requests identified as GET method with OverloadedPOSTMethodMixin"""
|
||||
# request = self.req.get('/')
|
||||
# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
|
||||
#
|
||||
# def test_overloaded_POST_behaviour_determines_POST(self):
|
||||
# """POST requests identified as POST method with OverloadedPOSTMethodMixin"""
|
||||
# request = self.req.post('/')
|
||||
# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
|
||||
#
|
||||
# def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||
# """POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
|
||||
# request = self.req.post('/', {OverloadedPOSTMethodMixin.METHOD_PARAM: 'DELETE'})
|
||||
# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
|
||||
from django.test import TestCase
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.mixins import RequestMixin
|
||||
|
||||
|
||||
class TestMethodOverloading(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
def test_standard_behaviour_determines_GET(self):
|
||||
"""GET requests identified"""
|
||||
view = RequestMixin()
|
||||
view.request = self.req.get('/')
|
||||
self.assertEqual(view.method, 'GET')
|
||||
|
||||
def test_standard_behaviour_determines_POST(self):
|
||||
"""POST requests identified"""
|
||||
view = RequestMixin()
|
||||
view.request = self.req.post('/')
|
||||
self.assertEqual(view.method, 'POST')
|
||||
|
||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
||||
view = RequestMixin()
|
||||
view.request = self.req.post('/', {view._METHOD_PARAM: 'DELETE'})
|
||||
self.assertEqual(view.method, 'DELETE')
|
||||
|
|
|
@ -1,130 +1,133 @@
|
|||
"""
|
||||
..
|
||||
>>> from djangorestframework.parsers import FormParser
|
||||
>>> from djangorestframework.compat import RequestFactory
|
||||
>>> from djangorestframework.resource import Resource
|
||||
>>> from StringIO import StringIO
|
||||
>>> from urllib import urlencode
|
||||
>>> req = RequestFactory().get('/')
|
||||
>>> some_resource = Resource()
|
||||
>>> some_resource.request = req # Make as if this request had been dispatched
|
||||
|
||||
FormParser
|
||||
============
|
||||
|
||||
Data flatening
|
||||
----------------
|
||||
|
||||
Here is some example data, which would eventually be sent along with a post request :
|
||||
|
||||
>>> inpt = urlencode([
|
||||
... ('key1', 'bla1'),
|
||||
... ('key2', 'blo1'), ('key2', 'blo2'),
|
||||
... ])
|
||||
|
||||
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||
|
||||
>>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
True
|
||||
|
||||
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||
|
||||
>>> class MyFormParser(FormParser):
|
||||
...
|
||||
... def is_a_list(self, key, val_list):
|
||||
... return len(val_list) > 1
|
||||
|
||||
This new parser only flattens the lists of parameters that contain a single value.
|
||||
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
True
|
||||
|
||||
.. note:: The same functionality is available for :class:`parsers.MultipartParser`.
|
||||
|
||||
Submitting an empty list
|
||||
--------------------------
|
||||
|
||||
When submitting an empty select multiple, like this one ::
|
||||
|
||||
<select multiple="multiple" name="key2"></select>
|
||||
|
||||
The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
||||
|
||||
<select multiple="multiple" name="key2"><option value="_empty"></select>
|
||||
|
||||
:class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
||||
|
||||
>>> inpt = urlencode([
|
||||
... ('key1', 'blo1'), ('key1', '_empty'),
|
||||
... ('key2', '_empty'),
|
||||
... ])
|
||||
|
||||
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
||||
True
|
||||
|
||||
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||
|
||||
>>> class MyFormParser(FormParser):
|
||||
...
|
||||
... def is_a_list(self, key, val_list):
|
||||
... return key == 'key2'
|
||||
...
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
||||
True
|
||||
|
||||
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||
"""
|
||||
import httplib, mimetypes
|
||||
from tempfile import TemporaryFile
|
||||
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 StringIO import StringIO
|
||||
|
||||
def encode_multipart_formdata(fields, files):
|
||||
"""For testing multipart parser.
|
||||
fields is a sequence of (name, value) elements for regular form fields.
|
||||
files is a sequence of (name, filename, value) elements for data to be uploaded as files
|
||||
Return (content_type, body)."""
|
||||
BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
|
||||
CRLF = '\r\n'
|
||||
L = []
|
||||
for (key, value) in fields:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||
L.append('')
|
||||
L.append(value)
|
||||
for (key, filename, value) in files:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||
L.append('Content-Type: %s' % get_content_type(filename))
|
||||
L.append('')
|
||||
L.append(value)
|
||||
L.append('--' + BOUNDARY + '--')
|
||||
L.append('')
|
||||
body = CRLF.join(L)
|
||||
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
return content_type, body
|
||||
|
||||
def get_content_type(filename):
|
||||
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
|
||||
class TestMultipartParser(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')],
|
||||
[('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')])
|
||||
|
||||
def test_multipartparser(self):
|
||||
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters."""
|
||||
post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
|
||||
resource = Resource()
|
||||
resource.request = post_req
|
||||
parsed = MultipartParser(resource).parse(StringIO(self.body))
|
||||
self.assertEqual(parsed['key1'], 'val1')
|
||||
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
|
||||
# """
|
||||
# ..
|
||||
# >>> from djangorestframework.parsers import FormParser
|
||||
# >>> from djangorestframework.compat import RequestFactory
|
||||
# >>> from djangorestframework.views import View
|
||||
# >>> from StringIO import StringIO
|
||||
# >>> from urllib import urlencode
|
||||
# >>> req = RequestFactory().get('/')
|
||||
# >>> some_view = View()
|
||||
# >>> some_view.request = req # Make as if this request had been dispatched
|
||||
#
|
||||
# FormParser
|
||||
# ============
|
||||
#
|
||||
# Data flatening
|
||||
# ----------------
|
||||
#
|
||||
# Here is some example data, which would eventually be sent along with a post request :
|
||||
#
|
||||
# >>> inpt = urlencode([
|
||||
# ... ('key1', 'bla1'),
|
||||
# ... ('key2', 'blo1'), ('key2', 'blo2'),
|
||||
# ... ])
|
||||
#
|
||||
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||
#
|
||||
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
||||
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
# True
|
||||
#
|
||||
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||
#
|
||||
# >>> class MyFormParser(FormParser):
|
||||
# ...
|
||||
# ... def is_a_list(self, key, val_list):
|
||||
# ... return len(val_list) > 1
|
||||
#
|
||||
# This new parser only flattens the lists of parameters that contain a single value.
|
||||
#
|
||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
# True
|
||||
#
|
||||
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
||||
#
|
||||
# Submitting an empty list
|
||||
# --------------------------
|
||||
#
|
||||
# When submitting an empty select multiple, like this one ::
|
||||
#
|
||||
# <select multiple="multiple" name="key2"></select>
|
||||
#
|
||||
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
||||
#
|
||||
# <select multiple="multiple" name="key2"><option value="_empty"></select>
|
||||
#
|
||||
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
||||
#
|
||||
# >>> inpt = urlencode([
|
||||
# ... ('key1', 'blo1'), ('key1', '_empty'),
|
||||
# ... ('key2', '_empty'),
|
||||
# ... ])
|
||||
#
|
||||
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||
#
|
||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
# >>> data == {'key1': 'blo1'}
|
||||
# True
|
||||
#
|
||||
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||
#
|
||||
# >>> class MyFormParser(FormParser):
|
||||
# ...
|
||||
# ... def is_a_list(self, key, val_list):
|
||||
# ... return key == 'key2'
|
||||
# ...
|
||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||
# >>> data == {'key1': 'blo1', 'key2': []}
|
||||
# True
|
||||
#
|
||||
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||
# """
|
||||
# import httplib, mimetypes
|
||||
# from tempfile import TemporaryFile
|
||||
# from django.test import TestCase
|
||||
# from djangorestframework.compat import RequestFactory
|
||||
# from djangorestframework.parsers import MultiPartParser
|
||||
# from djangorestframework.views import View
|
||||
# from StringIO import StringIO
|
||||
#
|
||||
# def encode_multipart_formdata(fields, files):
|
||||
# """For testing multipart parser.
|
||||
# fields is a sequence of (name, value) elements for regular form fields.
|
||||
# files is a sequence of (name, filename, value) elements for data to be uploaded as files
|
||||
# Return (content_type, body)."""
|
||||
# BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
|
||||
# CRLF = '\r\n'
|
||||
# L = []
|
||||
# for (key, value) in fields:
|
||||
# L.append('--' + BOUNDARY)
|
||||
# L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||
# L.append('')
|
||||
# L.append(value)
|
||||
# for (key, filename, value) in files:
|
||||
# L.append('--' + BOUNDARY)
|
||||
# L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||
# L.append('Content-Type: %s' % get_content_type(filename))
|
||||
# L.append('')
|
||||
# L.append(value)
|
||||
# L.append('--' + BOUNDARY + '--')
|
||||
# L.append('')
|
||||
# body = CRLF.join(L)
|
||||
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||
# return content_type, body
|
||||
#
|
||||
# def get_content_type(filename):
|
||||
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
#
|
||||
#class TestMultiPartParser(TestCase):
|
||||
# def setUp(self):
|
||||
# self.req = RequestFactory()
|
||||
# self.content_type, self.body = encode_multipart_formdata([('key1', 'val1'), ('key1', 'val2')],
|
||||
# [('file1', 'pic.jpg', 'blablabla'), ('file1', 't.txt', 'blobloblo')])
|
||||
#
|
||||
# def test_multipartparser(self):
|
||||
# """Ensure that MultiPartParser can parse multipart/form-data that contains a mix of several files and parameters."""
|
||||
# post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
|
||||
# view = View()
|
||||
# view.request = post_req
|
||||
# (data, files) = MultiPartParser(view).parse(StringIO(self.body))
|
||||
# self.assertEqual(data['key1'], 'val1')
|
||||
# self.assertEqual(files['file1'].read(), 'blablabla')
|
||||
|
||||
|
|
108
djangorestframework/tests/renderers.py
Normal file
108
djangorestframework/tests/renderers.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django import http
|
||||
from django.test import TestCase
|
||||
from djangorestframework.compat import View as DjangoView
|
||||
from djangorestframework.renderers import BaseRenderer, JSONRenderer
|
||||
from djangorestframework.mixins import ResponseMixin
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework.utils.mediatypes import add_media_type_param
|
||||
|
||||
DUMMYSTATUS = 200
|
||||
DUMMYCONTENT = 'dummycontent'
|
||||
|
||||
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
||||
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
|
||||
|
||||
class RendererA(BaseRenderer):
|
||||
media_type = 'mock/renderera'
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
return RENDERER_A_SERIALIZER(obj)
|
||||
|
||||
class RendererB(BaseRenderer):
|
||||
media_type = 'mock/rendererb'
|
||||
|
||||
def render(self, obj=None, media_type=None):
|
||||
return RENDERER_B_SERIALIZER(obj)
|
||||
|
||||
class MockView(ResponseMixin, DjangoView):
|
||||
renderers = (RendererA, RendererB)
|
||||
|
||||
def get(self, request):
|
||||
response = Response(DUMMYSTATUS, DUMMYCONTENT)
|
||||
return self.render(response)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||
|
||||
_indented_repr = """{
|
||||
"foo": [
|
||||
"bar",
|
||||
"baz"
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
class JSONRendererTests(TestCase):
|
||||
"""
|
||||
Tests specific to the JSON Renderer
|
||||
"""
|
||||
def test_without_content_type_args(self):
|
||||
obj = {'foo':['bar','baz']}
|
||||
renderer = JSONRenderer(None)
|
||||
content = renderer.render(obj, 'application/json')
|
||||
self.assertEquals(content, _flat_repr)
|
||||
|
||||
def test_with_content_type_args(self):
|
||||
obj = {'foo':['bar','baz']}
|
||||
renderer = JSONRenderer(None)
|
||||
content = renderer.render(obj, 'application/json; indent=2')
|
||||
self.assertEquals(content, _indented_repr)
|
31
djangorestframework/tests/resources.py
Normal file
31
djangorestframework/tests/resources.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
"""Tests for the resource module"""
|
||||
from django.test import TestCase
|
||||
from djangorestframework.resources import _object_to_data
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
class TestObjectToData(TestCase):
|
||||
"""Tests for the _object_to_data function"""
|
||||
|
||||
def test_decimal(self):
|
||||
"""Decimals need to be converted to a string representation."""
|
||||
self.assertEquals(_object_to_data(decimal.Decimal('1.5')), '1.5')
|
||||
|
||||
def test_function(self):
|
||||
"""Functions with no arguments should be called."""
|
||||
def foo():
|
||||
return 1
|
||||
self.assertEquals(_object_to_data(foo), 1)
|
||||
|
||||
def test_method(self):
|
||||
"""Methods with only a ``self`` argument should be called."""
|
||||
class Foo(object):
|
||||
def foo(self):
|
||||
return 1
|
||||
self.assertEquals(_object_to_data(Foo().foo), 1)
|
||||
|
||||
def test_datetime(self):
|
||||
"""datetime objects are left as-is."""
|
||||
now = datetime.datetime.now()
|
||||
self.assertEquals(_object_to_data(now), now)
|
|
@ -3,19 +3,19 @@ from django.core.urlresolvers import reverse
|
|||
from django.test import TestCase
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
|
||||
|
||||
class MockResource(Resource):
|
||||
class MockView(View):
|
||||
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
|
||||
anon_allowed_methods = ('GET',)
|
||||
permissions = ()
|
||||
|
||||
def get(self, request, auth):
|
||||
def get(self, request):
|
||||
return reverse('another')
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', MockResource.as_view()),
|
||||
url(r'^another$', MockResource.as_view(), name='another'),
|
||||
url(r'^$', MockView.as_view()),
|
||||
url(r'^another$', MockView.as_view(), name='another'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -24,5 +24,9 @@ class ReverseTests(TestCase):
|
|||
urls = 'djangorestframework.tests.reverse'
|
||||
|
||||
def test_reversed_urls_are_fully_qualified(self):
|
||||
response = self.client.get('/')
|
||||
try:
|
||||
response = self.client.get('/')
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.assertEqual(json.loads(response.content), 'http://testserver/another')
|
||||
|
|
38
djangorestframework/tests/throttling.py
Normal file
38
djangorestframework/tests/throttling.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.test import TestCase
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.permissions import PerUserThrottling
|
||||
|
||||
|
||||
class MockView(View):
|
||||
permissions = ( PerUserThrottling, )
|
||||
throttle = (3, 1) # 3 requests per second
|
||||
|
||||
def get(self, request):
|
||||
return 'foo'
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', MockView.as_view()),
|
||||
)
|
||||
|
||||
|
||||
#class ThrottlingTests(TestCase):
|
||||
# """Basic authentication"""
|
||||
# urls = 'djangorestframework.tests.throttling'
|
||||
#
|
||||
# def test_requests_are_throttled(self):
|
||||
# """Ensure request rate is limited"""
|
||||
# for dummy in range(3):
|
||||
# response = self.client.get('/')
|
||||
# response = self.client.get('/')
|
||||
#
|
||||
# def test_request_throttling_is_per_user(self):
|
||||
# """Ensure request rate is only limited per user, not globally"""
|
||||
# pass
|
||||
#
|
||||
# def test_request_throttling_expires(self):
|
||||
# """Ensure request rate is limited for a limited duration only"""
|
||||
# pass
|
|
@ -2,68 +2,62 @@ from django import forms
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.validators import ValidatorMixin, FormValidatorMixin, ModelFormValidatorMixin
|
||||
from djangorestframework.response import ResponseException
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
from djangorestframework.response import ErrorResponse
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.resources import Resource
|
||||
|
||||
|
||||
class TestValidatorMixinInterfaces(TestCase):
|
||||
"""Basic tests to ensure that the ValidatorMixin classes expose the expected interfaces"""
|
||||
|
||||
def test_validator_mixin_interface(self):
|
||||
"""Ensure the ValidatorMixin base class interface is as expected."""
|
||||
self.assertRaises(NotImplementedError, ValidatorMixin().validate, None)
|
||||
|
||||
def test_form_validator_mixin_interface(self):
|
||||
"""Ensure the FormValidatorMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(FormValidatorMixin, ValidatorMixin))
|
||||
getattr(FormValidatorMixin, 'form')
|
||||
getattr(FormValidatorMixin, 'validate')
|
||||
|
||||
def test_model_form_validator_mixin_interface(self):
|
||||
"""Ensure the ModelFormValidatorMixin interface is as expected."""
|
||||
self.assertTrue(issubclass(ModelFormValidatorMixin, FormValidatorMixin))
|
||||
getattr(ModelFormValidatorMixin, 'model')
|
||||
getattr(ModelFormValidatorMixin, 'form')
|
||||
getattr(ModelFormValidatorMixin, 'fields')
|
||||
getattr(ModelFormValidatorMixin, 'exclude_fields')
|
||||
getattr(ModelFormValidatorMixin, 'validate')
|
||||
|
||||
|
||||
class TestDisabledValidations(TestCase):
|
||||
"""Tests on Validator Mixins with validation disabled by setting form to None"""
|
||||
"""Tests on FormValidator with validation disabled by setting form to None"""
|
||||
|
||||
def test_disabled_form_validator_returns_content_unchanged(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
|
||||
class DisabledFormValidator(FormValidatorMixin):
|
||||
"""If the view's form attribute is None then FormValidator(view).validate_request(content, None)
|
||||
should just return the content unmodified."""
|
||||
class DisabledFormResource(FormResource):
|
||||
form = None
|
||||
|
||||
class MockView(View):
|
||||
resource = DisabledFormResource
|
||||
|
||||
view = MockView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledFormValidator().validate(content), content)
|
||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||
|
||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||
class DisabledFormValidator(FormValidatorMixin):
|
||||
form = None
|
||||
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledFormValidator().get_bound_form(content), None)
|
||||
|
||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then validate(content) should just return the content unmodified."""
|
||||
class DisabledModelFormValidator(ModelFormValidatorMixin):
|
||||
"""If the view's form attribute is None on then
|
||||
FormValidator(view).get_bound_form(content) should just return None."""
|
||||
class DisabledFormResource(FormResource):
|
||||
form = None
|
||||
|
||||
class MockView(View):
|
||||
resource = DisabledFormResource
|
||||
|
||||
view = MockView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledModelFormValidator().validate(content), content)
|
||||
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
||||
|
||||
|
||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||
"""If the view's form is None and does not have a Resource with a model set then
|
||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||
|
||||
class DisabledModelFormView(View):
|
||||
resource = ModelResource
|
||||
|
||||
view = DisabledModelFormView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
|
||||
|
||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||
class DisabledModelFormValidator(ModelFormValidatorMixin):
|
||||
form = None
|
||||
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(DisabledModelFormValidator().get_bound_form(content), None)
|
||||
|
||||
class DisabledModelFormView(View):
|
||||
resource = ModelResource
|
||||
|
||||
view = DisabledModelFormView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||
|
||||
class TestNonFieldErrors(TestCase):
|
||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||
|
@ -80,69 +74,81 @@ class TestNonFieldErrors(TestCase):
|
|||
raise forms.ValidationError(self.ERROR_TEXT)
|
||||
return self.cleaned_data #pragma: no cover
|
||||
|
||||
class MockValidator(FormValidatorMixin):
|
||||
class MockResource(FormResource):
|
||||
form = MockForm
|
||||
|
||||
class MockView(View):
|
||||
pass
|
||||
|
||||
view = MockView()
|
||||
content = {'field1': 'example1', 'field2': 'example2'}
|
||||
try:
|
||||
MockValidator().validate(content)
|
||||
except ResponseException, exc:
|
||||
MockResource(view).validate_request(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
self.fail('ErrorResponse was not raised') #pragma: no cover
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
"""Tests which check basic form validation.
|
||||
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
||||
(ModelFormValidatorMixin should behave as FormValidatorMixin if form is set rather than relying on the default ModelForm)"""
|
||||
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
||||
def setUp(self):
|
||||
class MockForm(forms.Form):
|
||||
qwerty = forms.CharField(required=True)
|
||||
|
||||
class MockFormValidator(FormValidatorMixin):
|
||||
form = MockForm
|
||||
|
||||
class MockModelFormValidator(ModelFormValidatorMixin):
|
||||
class MockFormResource(FormResource):
|
||||
form = MockForm
|
||||
|
||||
self.MockFormValidator = MockFormValidator
|
||||
self.MockModelFormValidator = MockModelFormValidator
|
||||
class MockModelResource(ModelResource):
|
||||
form = MockForm
|
||||
|
||||
class MockFormView(View):
|
||||
resource = MockFormResource
|
||||
|
||||
class MockModelFormView(View):
|
||||
resource = MockModelResource
|
||||
|
||||
self.MockFormResource = MockFormResource
|
||||
self.MockModelResource = MockModelResource
|
||||
self.MockFormView = MockFormView
|
||||
self.MockModelFormView = MockModelFormView
|
||||
|
||||
|
||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(validator.validate(content), content)
|
||||
self.assertEqual(validator.validate_request(content, None), content)
|
||||
|
||||
def validation_failure_raises_response_exception(self, validator):
|
||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||
content = {}
|
||||
self.assertRaises(ResponseException, validator.validate, content)
|
||||
content = {}
|
||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||
|
||||
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, validator.validate, content)
|
||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||
|
||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
validator._validate(content, allowed_extra_fields=('extra',))
|
||||
validator._validate(content, None, allowed_extra_fields=('extra',))
|
||||
|
||||
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
||||
content = {'qwerty': 'uiop'}
|
||||
self.assertEqual(validator._validate(content, allowed_extra_fields=('extra',)), content)
|
||||
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
||||
|
||||
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||
content = {}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
validator.validate_request(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
@ -151,8 +157,8 @@ class TestFormValidation(TestCase):
|
|||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||
content = {'qwerty': ''}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
validator.validate_request(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
@ -161,8 +167,8 @@ class TestFormValidation(TestCase):
|
|||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
validator.validate_request(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
@ -171,70 +177,88 @@ class TestFormValidation(TestCase):
|
|||
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||
content = {'qwerty': '', 'extra': 'extra'}
|
||||
try:
|
||||
validator.validate(content)
|
||||
except ResponseException, exc:
|
||||
validator.validate_request(content, None)
|
||||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
|
||||
'extra': ['This field does not exist.']}})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
|
||||
# Tests on FormValidtionMixin
|
||||
# Tests on FormResource
|
||||
|
||||
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
||||
|
||||
def test_form_validation_failure_raises_response_exception(self):
|
||||
self.validation_failure_raises_response_exception(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_failure_raises_response_exception(validator)
|
||||
|
||||
def test_validation_does_not_allow_extra_fields_by_default(self):
|
||||
self.validation_does_not_allow_extra_fields_by_default(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_does_not_allow_extra_fields_by_default(validator)
|
||||
|
||||
def test_validation_allows_extra_fields_if_explicitly_set(self):
|
||||
self.validation_allows_extra_fields_if_explicitly_set(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
||||
|
||||
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
||||
|
||||
def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
||||
|
||||
def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
||||
|
||||
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
||||
|
||||
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockFormValidator())
|
||||
validator = self.MockFormResource(self.MockFormView())
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
||||
|
||||
# Same tests on ModelFormValidtionMixin
|
||||
# Same tests on ModelResource
|
||||
|
||||
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
||||
|
||||
def test_modelform_validation_failure_raises_response_exception(self):
|
||||
self.validation_failure_raises_response_exception(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_failure_raises_response_exception(validator)
|
||||
|
||||
def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
|
||||
self.validation_does_not_allow_extra_fields_by_default(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_does_not_allow_extra_fields_by_default(validator)
|
||||
|
||||
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
|
||||
self.validation_allows_extra_fields_if_explicitly_set(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
||||
|
||||
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
||||
|
||||
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
||||
|
||||
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
||||
|
||||
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
||||
|
||||
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(self.MockModelFormValidator())
|
||||
validator = self.MockModelResource(self.MockModelFormView())
|
||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
||||
|
||||
|
||||
class TestModelFormValidator(TestCase):
|
||||
|
@ -249,43 +273,46 @@ class TestModelFormValidator(TestCase):
|
|||
@property
|
||||
def readonly(self):
|
||||
return 'read only'
|
||||
|
||||
class MockValidator(ModelFormValidatorMixin):
|
||||
|
||||
class MockResource(ModelResource):
|
||||
model = MockModel
|
||||
|
||||
class MockView(View):
|
||||
resource = MockResource
|
||||
|
||||
self.MockValidator = MockValidator
|
||||
self.validator = MockResource(MockView)
|
||||
|
||||
|
||||
def test_property_fields_are_allowed_on_model_forms(self):
|
||||
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||
|
||||
def test_property_fields_are_not_required_on_model_forms(self):
|
||||
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
||||
content = {'qwerty':'example', 'uiop': 'example'}
|
||||
self.assertEqual(self.MockValidator().validate(content), content)
|
||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||
|
||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||
|
||||
def test_validate_requires_fields_on_model_forms(self):
|
||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||
content = {'readonly': 'read only'}
|
||||
self.assertRaises(ResponseException, self.MockValidator().validate, content)
|
||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||
|
||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
||||
self.MockValidator().validate(content)
|
||||
self.validator.validate_request(content, None)
|
||||
|
||||
def test_model_form_validator_uses_model_forms(self):
|
||||
self.assertTrue(isinstance(self.MockValidator().get_bound_form(), forms.ModelForm))
|
||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
|||
from django.test import Client
|
||||
|
||||
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||
url(r'^robots.txt$', 'deny_robots'),
|
||||
url(r'^favicon.ico$', 'favicon'),
|
||||
url(r'^accounts/login$', 'api_login'),
|
||||
|
|
16
djangorestframework/urls.py
Normal file
16
djangorestframework/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
from django.conf import settings
|
||||
|
||||
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||
(r'robots.txt', 'deny_robots'),
|
||||
(r'^accounts/login/$', 'api_login'),
|
||||
(r'^accounts/logout/$', 'api_logout'),
|
||||
)
|
||||
|
||||
# Only serve favicon in production because otherwise chrome users will pretty much
|
||||
# permanantly have the django-rest-framework favicon whenever they navigate to
|
||||
# 127.0.0.1:8000 or whatever, which gets annoying
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += patterns('djangorestframework.utils.staticviews',
|
||||
(r'favicon.ico', 'favicon'),
|
||||
)
|
|
@ -1,22 +1,33 @@
|
|||
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):
|
||||
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
|
||||
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
|
||||
|
||||
from mediatypes import media_type_matches, is_form_media_type
|
||||
from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence
|
||||
|
||||
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"""
|
||||
"""
|
||||
Given an object which may be a list/tuple, another object, or None,
|
||||
return that object in list form.
|
||||
|
||||
IE:
|
||||
If the object is already a list/tuple just return it.
|
||||
If the object is not None, return it in a list with a single element.
|
||||
If the object is None return an empty list.
|
||||
"""
|
||||
if obj is None:
|
||||
return ()
|
||||
elif isinstance(obj, list):
|
||||
|
@ -27,7 +38,9 @@ def as_tuple(obj):
|
|||
|
||||
|
||||
def url_resolves(url):
|
||||
"""Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
|
||||
"""
|
||||
Return True if the given URL is mapped to a view in the urlconf, False otherwise.
|
||||
"""
|
||||
try:
|
||||
resolve(url)
|
||||
except:
|
||||
|
@ -124,7 +137,7 @@ def xml2dict(input):
|
|||
|
||||
|
||||
# Piston:
|
||||
class XMLEmitter():
|
||||
class XMLRenderer():
|
||||
def _to_xml(self, xml, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
|
@ -155,4 +168,4 @@ class XMLEmitter():
|
|||
return stream.getvalue()
|
||||
|
||||
def dict2xml(input):
|
||||
return XMLEmitter().dict2xml(input)
|
||||
return XMLRenderer().dict2xml(input)
|
|
@ -1,19 +1,21 @@
|
|||
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)."""
|
||||
|
||||
from djangorestframework.views import View
|
||||
|
||||
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):
|
||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||
if isinstance(getattr(view, 'cls_instance', None), View):
|
||||
breadcrumbs_list.insert(0, (get_name(view), url))
|
||||
|
||||
if url == '':
|
91
djangorestframework/utils/description.py
Normal file
91
djangorestframework/utils/description.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
"""
|
||||
Get a descriptive name and description for a view.
|
||||
"""
|
||||
import re
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
|
||||
|
||||
# These a a bit Grungy, but they do the job.
|
||||
|
||||
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 we're looking up the name of a view callable, as found by reverse,
|
||||
# grok the class instance that we stored when as_view was called.
|
||||
if getattr(view, 'cls_instance', None):
|
||||
view = view.cls_instance
|
||||
|
||||
# If this view has a resource that's been overridden, then use that resource for the name
|
||||
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
||||
name = view.resource.__name__
|
||||
|
||||
# Chomp of any non-descriptive trailing part of the resource class name
|
||||
if name.endswith('Resource') and name != 'Resource':
|
||||
name = name[:-len('Resource')]
|
||||
|
||||
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
|
||||
if getattr(view, '_suffix', None):
|
||||
name += view._suffix
|
||||
|
||||
# Otherwise if it's a function view use the function's name
|
||||
elif getattr(view, '__name__', None) is not None:
|
||||
name = view.__name__
|
||||
|
||||
# If it's a view class with no resource then grok the name from the class name
|
||||
elif getattr(view, '__class__', None) is not None:
|
||||
name = view.__class__.__name__
|
||||
|
||||
# Chomp of any non-descriptive trailing part of the view class name
|
||||
if name.endswith('View') and name != 'View':
|
||||
name = name[:-len('View')]
|
||||
|
||||
# I ain't got nuthin fo' ya
|
||||
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 we're looking up the name of a view callable, as found by reverse,
|
||||
# grok the class instance that we stored when as_view was called.
|
||||
if getattr(view, 'cls_instance', None):
|
||||
view = view.cls_instance
|
||||
|
||||
|
||||
# If this view has a resource that's been overridden, then use the resource's doctring
|
||||
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
||||
doc = view.resource.__doc__
|
||||
|
||||
# Otherwise use the view doctring
|
||||
elif getattr(view, '__doc__', None):
|
||||
doc = view.__doc__
|
||||
|
||||
# I ain't got nuthin fo' ya
|
||||
else:
|
||||
return ''
|
||||
|
||||
if not doc:
|
||||
return ''
|
||||
|
||||
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
|
||||
|
||||
# unindent the docstring if needed
|
||||
if whitespace_counts:
|
||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
|
||||
|
||||
# otherwise return it as-is
|
||||
return doc
|
||||
|
137
djangorestframework/utils/mediatypes.py
Normal file
137
djangorestframework/utils/mediatypes.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
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
|
||||
|
||||
|
||||
def media_type_matches(lhs, rhs):
|
||||
"""
|
||||
Returns ``True`` if the media type in the first argument <= the
|
||||
media type in the second argument. The media types are strings
|
||||
as described by the HTTP spec.
|
||||
|
||||
Valid media type strings include:
|
||||
|
||||
'application/json; indent=4'
|
||||
'application/json'
|
||||
'text/*'
|
||||
'*/*'
|
||||
"""
|
||||
lhs = _MediaType(lhs)
|
||||
rhs = _MediaType(rhs)
|
||||
return lhs.match(rhs)
|
||||
|
||||
|
||||
def is_form_media_type(media_type):
|
||||
"""
|
||||
Return True if the media type 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)
|
||||
"""
|
||||
media_type = _MediaType(media_type)
|
||||
return media_type.full_type == 'application/x-www-form-urlencoded' or \
|
||||
media_type.full_type == 'multipart/form-data'
|
||||
|
||||
|
||||
def add_media_type_param(media_type, key, val):
|
||||
"""
|
||||
Add a key, value parameter to a media type string, and return the new media type string.
|
||||
"""
|
||||
media_type = _MediaType(media_type)
|
||||
media_type.params[key] = val
|
||||
return str(media_type)
|
||||
|
||||
|
||||
def get_media_type_params(media_type):
|
||||
"""
|
||||
Return a dictionary of the parameters on the given media type.
|
||||
"""
|
||||
return _MediaType(media_type).params
|
||||
|
||||
|
||||
def order_by_precedence(media_type_lst):
|
||||
"""
|
||||
Returns a list of lists of media type strings, ordered by precedence.
|
||||
Precedence is determined by how specific a media type is:
|
||||
|
||||
3. 'type/subtype; param=val'
|
||||
2. 'type/subtype'
|
||||
1. 'type/*'
|
||||
0. '*/*'
|
||||
"""
|
||||
ret = [[],[],[],[]]
|
||||
for media_type in media_type_lst:
|
||||
precedence = _MediaType(media_type).precedence
|
||||
ret[3-precedence].append(media_type)
|
||||
return ret
|
||||
|
||||
|
||||
class _MediaType(object):
|
||||
def __init__(self, media_type_str):
|
||||
if media_type_str is None:
|
||||
media_type_str = ''
|
||||
self.orig = media_type_str
|
||||
self.full_type, self.params = parse_header(media_type_str)
|
||||
self.main_type, sep, self.sub_type = self.full_type.partition('/')
|
||||
|
||||
def match(self, other):
|
||||
"""Return true if this MediaType satisfies the given MediaType."""
|
||||
for key in self.params.keys():
|
||||
if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
|
||||
return False
|
||||
|
||||
if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
|
||||
return False
|
||||
|
||||
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def precedence(self):
|
||||
"""
|
||||
Return a precedence level from 0-3 for the media type given how specific it is.
|
||||
"""
|
||||
if self.main_type == '*':
|
||||
return 0
|
||||
elif self.sub_type == '*':
|
||||
return 1
|
||||
elif not self.params or self.params.keys() == ['q']:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
#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 as_tuple(self):
|
||||
# return (self.main_type, self.sub_type, self.params)
|
||||
|
||||
#def __repr__(self):
|
||||
# return "<MediaType %s>" % (self.as_tuple(),)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __unicode__(self):
|
||||
ret = "%s/%s" % (self.main_type, self.sub_type)
|
||||
for key, val in self.params.items():
|
||||
ret += "; %s=%s" % (key, val)
|
||||
return ret
|
||||
|
65
djangorestframework/utils/staticviews.py
Normal file
65
djangorestframework/utils/staticviews.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from django.contrib.auth.views import *
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
import base64
|
||||
|
||||
def deny_robots(request):
|
||||
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
|
||||
|
||||
def favicon(request):
|
||||
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
|
||||
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
# BLERGH
|
||||
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
|
||||
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
|
||||
# be making settings changes in order to accomodate django-rest-framework
|
||||
@csrf_protect
|
||||
@never_cache
|
||||
def api_login(request, template_name='api_login.html',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
authentication_form=AuthenticationForm):
|
||||
"""Displays the login form and handles the login action."""
|
||||
|
||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
||||
|
||||
if request.method == "POST":
|
||||
form = authentication_form(data=request.POST)
|
||||
if form.is_valid():
|
||||
# Light security check -- make sure redirect_to isn't garbage.
|
||||
if not redirect_to or ' ' in redirect_to:
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Heavier security check -- redirects to http://example.com should
|
||||
# not be allowed, but things like /view/?param=http://example.com
|
||||
# should be allowed. This regex checks if there is a '//' *before* a
|
||||
# question mark.
|
||||
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Okay, security checks complete. Log the user in.
|
||||
auth_login(request, form.get_user())
|
||||
|
||||
if request.session.test_cookie_worked():
|
||||
request.session.delete_test_cookie()
|
||||
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
else:
|
||||
form = authentication_form(request)
|
||||
|
||||
request.session.set_test_cookie()
|
||||
|
||||
#current_site = get_current_site(request)
|
||||
|
||||
return render_to_response(template_name, {
|
||||
'form': form,
|
||||
redirect_field_name: redirect_to,
|
||||
#'site': current_site,
|
||||
#'site_name': current_site.name,
|
||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
||||
return logout(request, next_page, template_name, redirect_field_name)
|
|
@ -1,199 +0,0 @@
|
|||
"""Mixin classes that provide a validate(content) function to validate and cleanup request content"""
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from djangorestframework.response import ResponseException
|
||||
from djangorestframework.utils import as_tuple
|
||||
|
||||
class ValidatorMixin(object):
|
||||
"""Base class for all ValidatorMixin classes, which simply defines the interface they provide."""
|
||||
|
||||
def validate(self, content):
|
||||
"""Given some content as input return some cleaned, validated content.
|
||||
Raises a ResponseException with status code 400 (Bad Request) on failure.
|
||||
|
||||
Must be overridden to be implemented."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class FormValidatorMixin(ValidatorMixin):
|
||||
"""Validator Mixin that uses forms for validation.
|
||||
Extends the ValidatorMixin interface to also provide a get_bound_form() method.
|
||||
(Which may be used by some emitters.)"""
|
||||
|
||||
"""The form class that should be used for validation, or None to turn off form validation."""
|
||||
form = None
|
||||
bound_form_instance = None
|
||||
|
||||
def validate(self, content):
|
||||
"""Given some content as input return some cleaned, validated content.
|
||||
Raises a ResponseException with status code 400 (Bad Request) on failure.
|
||||
|
||||
Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
|
||||
|
||||
On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
|
||||
If the 'errors' key exists it is a list of strings of non-field errors.
|
||||
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
|
||||
return self._validate(content)
|
||||
|
||||
def _validate(self, content, allowed_extra_fields=()):
|
||||
"""Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
|
||||
extra_fields is a list of fields which are not defined by the form, but which we still
|
||||
expect to see on the input."""
|
||||
bound_form = self.get_bound_form(content)
|
||||
|
||||
if bound_form is None:
|
||||
return content
|
||||
|
||||
self.bound_form_instance = bound_form
|
||||
|
||||
seen_fields_set = set(content.keys())
|
||||
form_fields_set = set(bound_form.fields.keys())
|
||||
allowed_extra_fields_set = set(allowed_extra_fields)
|
||||
|
||||
# In addition to regular validation we also ensure no additional fields are being passed in...
|
||||
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||
|
||||
# Check using both regular validation, and our stricter no additional fields rule
|
||||
if bound_form.is_valid() and not unknown_fields:
|
||||
# Validation succeeded...
|
||||
cleaned_data = bound_form.cleaned_data
|
||||
|
||||
cleaned_data.update(bound_form.files)
|
||||
|
||||
# Add in any extra fields to the cleaned content...
|
||||
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
|
||||
cleaned_data[key] = content[key]
|
||||
|
||||
return cleaned_data
|
||||
|
||||
# Validation failed...
|
||||
detail = {}
|
||||
|
||||
if not bound_form.errors and not unknown_fields:
|
||||
detail = {u'errors': [u'No content was supplied.']}
|
||||
|
||||
else:
|
||||
# Add any non-field errors
|
||||
if bound_form.non_field_errors():
|
||||
detail[u'errors'] = bound_form.non_field_errors()
|
||||
|
||||
# Add standard field errors
|
||||
field_errors = dict((key, map(unicode, val)) for (key, val) in bound_form.errors.iteritems() if not key.startswith('__'))
|
||||
|
||||
# Add any unknown field errors
|
||||
for key in unknown_fields:
|
||||
field_errors[key] = [u'This field does not exist.']
|
||||
|
||||
if field_errors:
|
||||
detail[u'field-errors'] = field_errors
|
||||
|
||||
# Return HTTP 400 response (BAD REQUEST)
|
||||
raise ResponseException(400, detail)
|
||||
|
||||
|
||||
def get_bound_form(self, content=None):
|
||||
"""Given some content return a Django form bound to that content.
|
||||
If form validation is turned off (form class attribute is None) then returns None."""
|
||||
if not self.form:
|
||||
return None
|
||||
|
||||
if not content is None:
|
||||
if hasattr(content, 'FILES'):
|
||||
return self.form(content, content.FILES)
|
||||
return self.form(content)
|
||||
return self.form()
|
||||
|
||||
|
||||
class ModelFormValidatorMixin(FormValidatorMixin):
|
||||
"""Validator Mixin that uses forms for validation and falls back to a model form if no form is set.
|
||||
Extends the ValidatorMixin interface to also provide a get_bound_form() method.
|
||||
(Which may be used by some emitters.)"""
|
||||
|
||||
"""The form class that should be used for validation, or None to use model form validation."""
|
||||
form = None
|
||||
|
||||
"""The model class from which the model form should be constructed if no form is set."""
|
||||
model = None
|
||||
|
||||
"""The list of fields we expect to receive as input. Fields in this list will may be received with
|
||||
raising non-existent field errors, even if they do not exist as fields on the ModelForm.
|
||||
|
||||
Setting the fields class attribute causes the exclude_fields class attribute to be disregarded."""
|
||||
fields = None
|
||||
|
||||
"""The list of fields to exclude from the Model. This is only used if the fields class attribute is not set."""
|
||||
exclude_fields = ('id', 'pk')
|
||||
|
||||
|
||||
# TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
|
||||
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
|
||||
def validate(self, content):
|
||||
"""Given some content as input return some cleaned, validated content.
|
||||
Raises a ResponseException with status code 400 (Bad Request) on failure.
|
||||
|
||||
Validation is standard form or model form validation,
|
||||
with an additional constraint that no extra unknown fields may be supplied,
|
||||
and that all fields specified by the fields class attribute must be supplied,
|
||||
even if they are not validated by the form/model form.
|
||||
|
||||
On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys.
|
||||
If the 'errors' key exists it is a list of strings of non-field errors.
|
||||
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
|
||||
return self._validate(content, allowed_extra_fields=self._property_fields_set)
|
||||
|
||||
|
||||
def get_bound_form(self, content=None):
|
||||
"""Given some content return a Django form bound to that content.
|
||||
|
||||
If the form class attribute has been explicitly set then use that class to create a Form,
|
||||
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
|
||||
|
||||
if self.form:
|
||||
# Use explict Form
|
||||
return super(ModelFormValidatorMixin, self).get_bound_form(content)
|
||||
|
||||
elif self.model:
|
||||
# Fall back to ModelForm which we create on the fly
|
||||
class OnTheFlyModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
#fields = tuple(self._model_fields_set)
|
||||
|
||||
# Instantiate the ModelForm as appropriate
|
||||
if content and isinstance(content, models.Model):
|
||||
# Bound to an existing model instance
|
||||
return OnTheFlyModelForm(instance=content)
|
||||
elif not content is None:
|
||||
if hasattr(content, 'FILES'):
|
||||
return OnTheFlyModelForm(content, content.FILES)
|
||||
return OnTheFlyModelForm(content)
|
||||
return OnTheFlyModelForm()
|
||||
|
||||
# Both form and model not set? Okay bruv, whatevs...
|
||||
return None
|
||||
|
||||
|
||||
@property
|
||||
def _model_fields_set(self):
|
||||
"""Return a set containing the names of validated fields on the model."""
|
||||
model_fields = set(field.name for field in self.model._meta.fields)
|
||||
|
||||
if self.fields:
|
||||
return model_fields & set(as_tuple(self.fields))
|
||||
|
||||
return model_fields - set(as_tuple(self.exclude_fields))
|
||||
|
||||
@property
|
||||
def _property_fields_set(self):
|
||||
"""Returns a set containing the names of validated properties on the model."""
|
||||
property_fields = set(attr for attr in dir(self.model) if
|
||||
isinstance(getattr(self.model, attr, None), property)
|
||||
and not attr.startswith('_'))
|
||||
|
||||
if self.fields:
|
||||
return property_fields & set(as_tuple(self.fields))
|
||||
|
||||
return property_fields - set(as_tuple(self.exclude_fields))
|
||||
|
||||
|
||||
|
|
@ -1,66 +1,170 @@
|
|||
from django.contrib.auth.views import *
|
||||
#from django.contrib.sites.models import get_current_site
|
||||
from django.conf import settings
|
||||
"""
|
||||
The :mod:`views` module provides the Views you will most probably
|
||||
be subclassing in your implementation.
|
||||
|
||||
By setting or modifying class attributes on your view, you change it's predefined behaviour.
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.http import HttpResponse
|
||||
import base64
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
def deny_robots(request):
|
||||
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
|
||||
|
||||
def favicon(request):
|
||||
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
|
||||
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
# BLERGH
|
||||
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
|
||||
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
|
||||
# be making settings changes in order to accomodate django-rest-framework
|
||||
@csrf_protect
|
||||
@never_cache
|
||||
def api_login(request, template_name='api_login.html',
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
authentication_form=AuthenticationForm):
|
||||
"""Displays the login form and handles the login action."""
|
||||
|
||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
||||
|
||||
if request.method == "POST":
|
||||
form = authentication_form(data=request.POST)
|
||||
if form.is_valid():
|
||||
# Light security check -- make sure redirect_to isn't garbage.
|
||||
if not redirect_to or ' ' in redirect_to:
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Heavier security check -- redirects to http://example.com should
|
||||
# not be allowed, but things like /view/?param=http://example.com
|
||||
# should be allowed. This regex checks if there is a '//' *before* a
|
||||
# question mark.
|
||||
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
# Okay, security checks complete. Log the user in.
|
||||
auth_login(request, form.get_user())
|
||||
|
||||
if request.session.test_cookie_worked():
|
||||
request.session.delete_test_cookie()
|
||||
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
else:
|
||||
form = authentication_form(request)
|
||||
|
||||
request.session.set_test_cookie()
|
||||
|
||||
#current_site = get_current_site(request)
|
||||
|
||||
return render_to_response(template_name, {
|
||||
'form': form,
|
||||
redirect_field_name: redirect_to,
|
||||
#'site': current_site,
|
||||
#'site_name': current_site.name,
|
||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
||||
}, context_instance=RequestContext(request))
|
||||
from djangorestframework.compat import View as DjangoView
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.mixins import *
|
||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
||||
|
||||
|
||||
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
||||
return logout(request, next_page, template_name, redirect_field_name)
|
||||
__all__ = (
|
||||
'View',
|
||||
'ModelView',
|
||||
'InstanceModelView',
|
||||
'ListModelView',
|
||||
'ListOrCreateModelView'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||
"""
|
||||
Handles incoming requests and maps them to REST operations.
|
||||
Performs request deserialization, response serialization, authentication and input validation.
|
||||
"""
|
||||
|
||||
"""
|
||||
The resource to use when validating requests and filtering responses,
|
||||
or `None` to use default behaviour.
|
||||
"""
|
||||
resource = None
|
||||
|
||||
"""
|
||||
List of renderers the resource can serialize the response with, ordered by preference.
|
||||
"""
|
||||
renderers = ( renderers.JSONRenderer,
|
||||
renderers.DocumentingHTMLRenderer,
|
||||
renderers.DocumentingXHTMLRenderer,
|
||||
renderers.DocumentingPlainTextRenderer,
|
||||
renderers.XMLRenderer )
|
||||
|
||||
"""
|
||||
List of parsers the resource can parse the request with.
|
||||
"""
|
||||
parsers = ( parsers.JSONParser,
|
||||
parsers.FormParser,
|
||||
parsers.MultiPartParser )
|
||||
|
||||
"""
|
||||
List of all authenticating methods to attempt.
|
||||
"""
|
||||
authentication = ( authentication.UserLoggedInAuthenticaton,
|
||||
authentication.BasicAuthenticaton )
|
||||
|
||||
"""
|
||||
List of all permissions that must be checked.
|
||||
"""
|
||||
permissions = ( permissions.FullAnonAccess, )
|
||||
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
"""
|
||||
Override the default :meth:`as_view` to store an instance of the view
|
||||
as an attribute on the callable function. This allows us to discover
|
||||
information about the view when we do URL reverse lookups.
|
||||
"""
|
||||
view = super(View, cls).as_view(**initkwargs)
|
||||
view.cls_instance = cls(**initkwargs)
|
||||
return view
|
||||
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
"""
|
||||
Return the list of allowed HTTP methods, uppercased.
|
||||
"""
|
||||
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
||||
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
||||
"""
|
||||
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||
|
||||
|
||||
def initial(self, request, *args, **kargs):
|
||||
"""
|
||||
Hook for any code that needs to run prior to anything else.
|
||||
Required if you want to do things like set `request.upload_handlers` before
|
||||
the authentication and dispatch handling is run.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
# all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||
set_script_prefix(prefix)
|
||||
|
||||
try:
|
||||
self.initial(request, *args, **kwargs)
|
||||
|
||||
# Authenticate and check request has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
||||
# Get the appropriate handler method
|
||||
if self.method.lower() in self.http_method_names:
|
||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
||||
else:
|
||||
handler = self.http_method_not_allowed
|
||||
|
||||
response_obj = handler(request, *args, **kwargs)
|
||||
|
||||
# Allow return value to be either HttpResponse, Response, or an object, or None
|
||||
if isinstance(response_obj, HttpResponse):
|
||||
return response_obj
|
||||
elif isinstance(response_obj, Response):
|
||||
response = response_obj
|
||||
elif response_obj is not None:
|
||||
response = Response(status.HTTP_200_OK, response_obj)
|
||||
else:
|
||||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.filter_response(response.raw_content)
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
# TODO - this isn't actually the correct way to set the vary header,
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
response.headers['Vary'] = 'Authenticate, Accept'
|
||||
|
||||
return self.render(response)
|
||||
|
||||
|
||||
class ModelView(View):
|
||||
"""A RESTful view that maps to a model in the database."""
|
||||
resource = resources.ModelResource
|
||||
|
||||
class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
|
||||
"""A view which provides default operations for read/update/delete against a model instance."""
|
||||
_suffix = 'Instance'
|
||||
|
||||
class ListModelView(ListModelMixin, ModelView):
|
||||
"""A view which provides default operations for list, against a model in the database."""
|
||||
_suffix = 'List'
|
||||
|
||||
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
||||
"""A view which provides default operations for list and create, against a model in the database."""
|
||||
_suffix = 'List'
|
||||
|
|
|
@ -60,6 +60,7 @@ version = '0.1'
|
|||
release = '0.1'
|
||||
|
||||
autodoc_member_order='bysource'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
|
|
@ -27,7 +27,7 @@ Creating the resources
|
|||
Once we have some existing models there's very little we need to do to create the corresponding resources. We simply create a base resource and an instance resource for each model we're working with.
|
||||
django-rest-framework will provide the default operations on the resources all the usual input validation that Django's models can give us for free.
|
||||
|
||||
``views.py``
|
||||
#``views.py``
|
||||
|
||||
.. include:: ../../examples/blogpost/views.py
|
||||
:literal:
|
||||
#.. include:: ../../examples/blogpost/views.py
|
||||
# :literal:
|
|
@ -31,12 +31,12 @@ We'll need two resources:
|
|||
Form validation
|
||||
---------------
|
||||
|
||||
We'll now add a form to specify what input fields are required when creating a new highlighed code snippet. This will include:
|
||||
We'll now add a form to specify what input fields are required when creating a new highlighted code snippet. This will include:
|
||||
|
||||
* The code text itself.
|
||||
* An optional title for the code.
|
||||
* A flag to determine if line numbers should be included.
|
||||
* Which programming langauge to interpret the code snippet as.
|
||||
* Which programming language to interpret the code snippet as.
|
||||
* Which output style to use for the highlighting.
|
||||
|
||||
``forms.py``
|
||||
|
|
|
@ -18,7 +18,7 @@ Features:
|
|||
* Automatically provides an awesome Django admin style `browse-able self-documenting API <http://api.django-rest-framework.org>`_.
|
||||
* Clean, simple, views for Resources, using Django's new `class based views <http://docs.djangoproject.com/en/dev/topics/class-based-views/>`_.
|
||||
* Support for ModelResources with out-of-the-box default implementations and input validation.
|
||||
* Pluggable :mod:`.emitters`, :mod:`parsers`, :mod:`validators` and :mod:`authenticators` - Easy to customise.
|
||||
* Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise.
|
||||
* Content type negotiation using HTTP Accept headers.
|
||||
* Optional support for forms as input validation.
|
||||
* Modular architecture - MixIn classes can be used without requiring the :class:`.Resource` or :class:`.ModelResource` classes.
|
||||
|
@ -36,7 +36,8 @@ Resources
|
|||
|
||||
Any and all questions, thoughts, bug reports and contributions are *hugely appreciated*.
|
||||
|
||||
We'd like for this to be a real community driven effort, so come say hi, get involved, and get forking! (See: `Bitbucket <http://confluence.atlassian.com/display/BITBUCKET/Forking+a+Bitbucket+Repository>`_, `GitHub <http://help.github.com/fork-a-repo/>`_)
|
||||
We'd like for this to be a real community driven effort, so come say hi, get involved, and get forking! (See: `Forking a Bitbucket Repository
|
||||
<http://confluence.atlassian.com/display/BITBUCKET/Forking+a+Bitbucket+Repository>`_, or `Fork A GitHub Repo <http://help.github.com/fork-a-repo/>`_)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
@ -139,14 +140,16 @@ Library Reference
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
library/resource
|
||||
library/modelresource
|
||||
library/emitters
|
||||
library/authentication
|
||||
library/compat
|
||||
library/mixins
|
||||
library/parsers
|
||||
library/authenticators
|
||||
library/validators
|
||||
library/permissions
|
||||
library/renderers
|
||||
library/resource
|
||||
library/response
|
||||
library/status
|
||||
library/views
|
||||
|
||||
Examples Reference
|
||||
------------------
|
||||
|
|
5
docs/library/authentication.rst
Normal file
5
docs/library/authentication.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
:mod:`authentication`
|
||||
=====================
|
||||
|
||||
.. automodule:: authentication
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`authenticators`
|
||||
=====================
|
||||
|
||||
.. automodule:: authenticators
|
||||
:members:
|
5
docs/library/compat.rst
Normal file
5
docs/library/compat.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
:mod:`compat`
|
||||
=====================
|
||||
|
||||
.. automodule:: compat
|
||||
:members:
|
|
@ -1,7 +0,0 @@
|
|||
:mod:`emitters`
|
||||
===============
|
||||
|
||||
The emitters module provides a set of emitters that can be plugged in to a :class:`.Resource`. An emitter is responsible for taking the output of a and serializing it to a given media type. A :class:`.Resource` can have a number of emitters, allow the same content to be serialized in a number of different formats depending on the requesting client's preferences, as specified in the HTTP Request's Accept header.
|
||||
|
||||
.. automodule:: emitters
|
||||
:members:
|
5
docs/library/mixins.rst
Normal file
5
docs/library/mixins.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
:mod:`mixins`
|
||||
=====================
|
||||
|
||||
.. automodule:: mixins
|
||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||
:mod:`modelresource`
|
||||
====================
|
||||
|
||||
.. note::
|
||||
|
||||
TODO - document this module properly
|
||||
|
||||
.. automodule:: modelresource
|
||||
:members:
|
5
docs/library/permissions.rst
Normal file
5
docs/library/permissions.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
:mod:`permissions`
|
||||
=====================
|
||||
|
||||
.. automodule:: permissions
|
||||
:members:
|
10
docs/library/renderers.rst
Normal file
10
docs/library/renderers.rst
Normal file
|
@ -0,0 +1,10 @@
|
|||
:mod:`renderers`
|
||||
================
|
||||
|
||||
The renderers module provides a set of renderers that can be plugged in to a :class:`.Resource`.
|
||||
A renderer is responsible for taking the output of a View and serializing it to a given media type.
|
||||
A :class:`.Resource` can have a number of renderers, allow the same content to be serialized in a number
|
||||
of different formats depending on the requesting client's preferences, as specified in the HTTP Request's Accept header.
|
||||
|
||||
.. automodule:: renderers
|
||||
:members:
|
|
@ -1,136 +1,5 @@
|
|||
:mod:`resource`
|
||||
===============
|
||||
|
||||
.. module:: resource
|
||||
|
||||
The :mod:`resource` module is the core of Django REST framework. It provides the :class:`Resource` base class which handles incoming HTTP requests and maps them to method calls, performing authentication, input deserialization, input validation and output serialization.
|
||||
|
||||
Resources are created by sublassing :class:`Resource`, setting a number of class attributes, and overriding one or more methods.
|
||||
|
||||
.. class:: Resource
|
||||
|
||||
:class:`Resource` class attributes
|
||||
----------------------------------
|
||||
|
||||
The following class attributes determine the behavior of the Resource and are intended to be overridden.
|
||||
|
||||
.. attribute:: Resource.allowed_methods
|
||||
|
||||
A list of the HTTP methods that the Resource supports.
|
||||
HTTP requests to the resource that do not map to an allowed operation will result in a 405 Method Not Allowed response.
|
||||
|
||||
Default: ``('GET',)``
|
||||
|
||||
.. attribute:: Resource.anon_allowed_methods
|
||||
|
||||
A list of the HTTP methods that the Resource supports for unauthenticated users.
|
||||
Unauthenticated HTTP requests to the resource that do not map to an allowed operation will result in a 405 Method Not Allowed response.
|
||||
|
||||
Default: ``()``
|
||||
|
||||
.. attribute:: Resource.emitters
|
||||
|
||||
The list of emitters that the Resource supports. This determines which media types the resource can serialize it's output to. Clients can specify which media types they accept using standard HTTP content negotiation via the Accept header. (See `RFC 2616 - Sec 14.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html>`_) Clients can also override this standard content negotiation by specifying a `_format` ...
|
||||
|
||||
The :mod:`emitters` module provides the :class:`BaseEmitter` class and a set of default emitters, including emitters for JSON and XML, as well as emitters for HTML and Plain Text which provide for a self documenting API.
|
||||
|
||||
The ordering of the Emitters is important as it determines an order of preference.
|
||||
|
||||
Default: ``(emitters.JSONEmitter, emitters.DocumentingHTMLEmitter, emitters.DocumentingXHTMLEmitter, emitters.DocumentingPlainTextEmitter, emitters.XMLEmitter)``
|
||||
|
||||
.. attribute:: Resource.parsers
|
||||
|
||||
The list of parsers that the Resource supports. This determines which media types the resource can accept as input for incoming HTTP requests. (Typically PUT and POST requests).
|
||||
|
||||
The ordering of the Parsers may be considered informative of preference but is not used ...
|
||||
|
||||
Default: ``(parsers.JSONParser, parsers.XMLParser, parsers.FormParser)``
|
||||
|
||||
.. attribute:: Resource.authenticators
|
||||
|
||||
The list of authenticators that the Resource supports. This determines which authentication methods (eg Basic, Digest, OAuth) are used to authenticate requests.
|
||||
|
||||
Default: ``(authenticators.UserLoggedInAuthenticator, authenticators.BasicAuthenticator)``
|
||||
|
||||
.. attribute:: Resource.form
|
||||
|
||||
If not None, this attribute should be a Django form which will be used to validate any request data.
|
||||
This attribute is typically only used for POST or PUT requests to the resource.
|
||||
|
||||
Deafult: ``None``
|
||||
|
||||
.. attribute:: Resource.callmap
|
||||
|
||||
Maps HTTP methods to function calls on the :class:`Resource`. It may be overridden in order to add support for other HTTP methods such as HEAD, OPTIONS and PATCH, or in order to map methods to different function names, for example to use a more `CRUD <http://en.wikipedia.org/wiki/Create,_read,_update_and_delete>`_ like style.
|
||||
|
||||
Default: ``{ 'GET': 'get', 'POST': 'post', 'PUT': 'put', 'DELETE': 'delete' }``
|
||||
|
||||
|
||||
:class:`Resource` methods
|
||||
-------------------------
|
||||
|
||||
.. method:: Resource.get
|
||||
.. method:: Resource.post
|
||||
.. method:: Resource.put
|
||||
.. method:: Resource.delete
|
||||
.. method:: Resource.authenticate
|
||||
.. method:: Resource.reverse
|
||||
|
||||
:class:`Resource` properties
|
||||
----------------------------
|
||||
|
||||
.. method:: Resource.name
|
||||
.. method:: Resource.description
|
||||
.. method:: Resource.default_emitter
|
||||
.. method:: Resource.default_parser
|
||||
.. method:: Resource.emitted_media_types
|
||||
.. method:: Resource.parsed_media_types
|
||||
|
||||
:class:`Resource` reserved form and query parameters
|
||||
----------------------------------------------------
|
||||
|
||||
.. attribute:: Resource.ACCEPT_QUERY_PARAM
|
||||
|
||||
If set, allows the default `Accept:` header content negotiation to be bypassed by setting the requested media type in a query parameter on the URL. This can be useful if it is necessary to be able to hyperlink to a given format on the Resource using standard HTML.
|
||||
|
||||
Set to None to disable, or to another string value to use another name for the reserved URL query parameter.
|
||||
|
||||
Default: ``"_accept"``
|
||||
|
||||
.. attribute:: Resource.METHOD_PARAM
|
||||
|
||||
If set, allows for PUT and DELETE requests to be tunneled on form POST operations, by setting a (typically hidden) form field with the method name. This allows standard HTML forms to perform method requests which would otherwise `not be supported <http://dev.w3.org/html5/spec/Overview.html#attr-fs-method>`_
|
||||
|
||||
Set to None to disable, or to another string value to use another name for the reserved form field.
|
||||
|
||||
Default: ``"_method"``
|
||||
|
||||
.. attribute:: Resource.CONTENTTYPE_PARAM
|
||||
|
||||
Used together with :attr:`CONTENT_PARAM`.
|
||||
|
||||
If set, allows for arbitrary content types to be tunneled on form POST operations, by setting a form field with the content type. This allows standard HTML forms to perform requests with content types other those `supported by default <http://dev.w3.org/html5/spec/Overview.html#attr-fs-enctype>`_ (ie. `application/x-www-form-urlencoded`, `multipart/form-data`, and `text-plain`)
|
||||
|
||||
Set to None to disable, or to another string value to use another name for the reserved form field.
|
||||
|
||||
Default: ``"_contenttype"``
|
||||
|
||||
.. attribute:: Resource.CONTENT_PARAM
|
||||
|
||||
Used together with :attr:`CONTENTTYPE_PARAM`.
|
||||
|
||||
Set to None to disable, or to another string value to use another name for the reserved form field.
|
||||
|
||||
Default: ``"_content"``
|
||||
|
||||
.. attribute:: Resource.CSRF_PARAM
|
||||
|
||||
The name used in Django's (typically hidden) form field for `CSRF Protection <http://docs.djangoproject.com/en/dev/ref/contrib/csrf/>`_.
|
||||
|
||||
Setting to None does not disable Django's CSRF middleware, but it does mean that the field name will not be treated as reserved by FlyWheel, so for example the default :class:`FormParser` will return fields with this as part of the request content, rather than ignoring them.
|
||||
|
||||
Default:: ``"csrfmiddlewaretoken"``
|
||||
|
||||
reserved params
|
||||
internal methods
|
||||
|
||||
.. automodule:: resources
|
||||
:members:
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`validators`
|
||||
=================
|
||||
|
||||
.. automodule:: validators
|
||||
:members:
|
5
docs/library/views.rst
Normal file
5
docs/library/views.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
:mod:`views`
|
||||
=====================
|
||||
|
||||
.. automodule:: views
|
||||
:members:
|
|
@ -12,6 +12,8 @@ RATING_CHOICES = ((0, 'Awful'),
|
|||
(3, 'Good'),
|
||||
(4, 'Excellent'))
|
||||
|
||||
MAX_POSTS = 10
|
||||
|
||||
class BlogPost(models.Model):
|
||||
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
||||
title = models.CharField(max_length=128)
|
||||
|
@ -19,28 +21,13 @@ class BlogPost(models.Model):
|
|||
created = models.DateTimeField(auto_now_add=True)
|
||||
slug = models.SlugField(editable=False, default='')
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('blog-post', (), {'key': self.key})
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def comments_url(self):
|
||||
"""Link to a resource which lists all comments for this blog post."""
|
||||
return ('comments', (), {'blogpost': self.key})
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
super(self.__class__, self).save(*args, **kwargs)
|
||||
for obj in self.__class__.objects.order_by('-pk')[10:]:
|
||||
for obj in self.__class__.objects.order_by('-created')[MAX_POSTS:]:
|
||||
obj.delete()
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
|
||||
username = models.CharField(max_length=128)
|
||||
|
@ -48,16 +35,3 @@ class Comment(models.Model):
|
|||
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('comment', (), {'blogpost': self.blogpost.key, 'id': self.id})
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def blogpost_url(self):
|
||||
"""Link to the blog post resource which this comment corresponds to."""
|
||||
return ('blog-post', (), {'key': self.blogpost.key})
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Test a range of REST API usage of the example application.
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import simplejson as json
|
||||
|
|
|
@ -1,9 +1,36 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from blogpost.views import BlogPosts, BlogPostInstance, Comments, CommentInstance
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||
from djangorestframework.resources import ModelResource
|
||||
|
||||
from blogpost.models import BlogPost, Comment
|
||||
|
||||
|
||||
class BlogPostResource(ModelResource):
|
||||
"""
|
||||
A Blog Post has a *title* and *content*, and can be associated with zero or more comments.
|
||||
"""
|
||||
model = BlogPost
|
||||
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
|
||||
ordering = ('-created',)
|
||||
|
||||
def comments(self, instance):
|
||||
return reverse('comments', kwargs={'blogpost': instance.key})
|
||||
|
||||
|
||||
class CommentResource(ModelResource):
|
||||
"""
|
||||
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
|
||||
"""
|
||||
model = Comment
|
||||
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
|
||||
ordering = ('-created',)
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', BlogPosts.as_view(), name='blog-posts'),
|
||||
url(r'^(?P<key>[^/]+)/$', BlogPostInstance.as_view(), name='blog-post'),
|
||||
url(r'^(?P<blogpost>[^/]+)/comments/$', Comments.as_view(), name='comments'),
|
||||
url(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', CommentInstance.as_view(), name='comment'),
|
||||
url(r'^$', ListOrCreateModelView.as_view(resource=BlogPostResource), name='blog-posts-root'),
|
||||
url(r'^(?P<key>[^/]+)/$', InstanceModelView.as_view(resource=BlogPostResource)),
|
||||
url(r'^(?P<blogpost>[^/]+)/comments/$', ListOrCreateModelView.as_view(resource=CommentResource), name='comments'),
|
||||
url(r'^(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', InstanceModelView.as_view(resource=CommentResource)),
|
||||
)
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
from djangorestframework.modelresource import ModelResource, RootModelResource
|
||||
|
||||
from blogpost import models
|
||||
|
||||
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
|
||||
COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
|
||||
MAX_POSTS = 10
|
||||
|
||||
class BlogPosts(RootModelResource):
|
||||
"""A resource with which lists all existing blog posts and creates new blog posts."""
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'POST',)
|
||||
model = models.BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class BlogPostInstance(ModelResource):
|
||||
"""A resource which represents a single blog post."""
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = models.BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class Comments(RootModelResource):
|
||||
"""A resource which lists all existing comments for a given blog post, and creates new blog comments for a given blog post."""
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'POST',)
|
||||
model = models.Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
class CommentInstance(ModelResource):
|
||||
"""A resource which represents a single comment."""
|
||||
anon_allowed_methods = allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = models.Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
from djangorestframework.compat import View # Use Django 1.3's django.views.generic.View, or fall back to a clone of that if Django < 1.3
|
||||
from djangorestframework.emitters import EmitterMixin, DEFAULT_EMITTERS
|
||||
from djangorestframework.mixins import ResponseMixin
|
||||
from djangorestframework.renderers import DEFAULT_RENDERERS
|
||||
from djangorestframework.response import Response
|
||||
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class ExampleView(EmitterMixin, View):
|
||||
class ExampleView(ResponseMixin, View):
|
||||
"""An example view using Django 1.3's class based views.
|
||||
Uses djangorestframework's EmitterMixin to provide support for multiple output formats."""
|
||||
emitters = DEFAULT_EMITTERS
|
||||
Uses djangorestframework's RendererMixin to provide support for multiple output formats."""
|
||||
renderers = DEFAULT_RENDERERS
|
||||
|
||||
def get(self, request):
|
||||
response = Response(200, {'description': 'Some example content',
|
||||
'url': reverse('mixin-view')})
|
||||
return self.emit(response)
|
||||
return self.render(response)
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
|
|
@ -7,17 +7,13 @@ class MyModel(models.Model):
|
|||
bar = models.IntegerField(help_text='Must be an integer.')
|
||||
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""For the purposes of the sandbox, limit the maximum number of stored models."""
|
||||
"""
|
||||
For the purposes of the sandbox limit the maximum number of stored models.
|
||||
"""
|
||||
super(MyModel, self).save(*args, **kwargs)
|
||||
while MyModel.objects.all().count() > MAX_INSTANCES:
|
||||
MyModel.objects.all()[0].delete()
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('my-model-resource', (self.pk,))
|
||||
MyModel.objects.all().order_by('-created')[0].delete()
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from modelresourceexample.views import MyModelRootResource, MyModelResource
|
||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||
from djangorestframework.resources import ModelResource
|
||||
from modelresourceexample.models import MyModel
|
||||
|
||||
urlpatterns = patterns('modelresourceexample.views',
|
||||
url(r'^$', MyModelRootResource.as_view(), name='my-model-root-resource'),
|
||||
url(r'^([0-9]+)/$', MyModelResource.as_view(), name='my-model-resource'),
|
||||
class MyModelResource(ModelResource):
|
||||
model = MyModel
|
||||
fields = ('foo', 'bar', 'baz', 'url')
|
||||
ordering = ('created',)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'),
|
||||
url(r'^([0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)),
|
||||
)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
from djangorestframework.modelresource import ModelResource, RootModelResource
|
||||
from modelresourceexample.models import MyModel
|
||||
|
||||
FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
|
||||
|
||||
class MyModelRootResource(RootModelResource):
|
||||
"""A create/list resource for MyModel.
|
||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||
model = MyModel
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'POST')
|
||||
fields = FIELDS
|
||||
|
||||
class MyModelResource(ModelResource):
|
||||
"""A read/update/delete resource for MyModel.
|
||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||
model = MyModel
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
fields = FIELDS
|
|
@ -1,7 +1,7 @@
|
|||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework import status
|
||||
|
||||
|
@ -15,55 +15,69 @@ MAX_FILES = 10
|
|||
|
||||
|
||||
def remove_oldest_files(dir, max_files):
|
||||
"""Remove the oldest files in a directory 'dir', leaving at most 'max_files' remaining.
|
||||
We use this to limit the number of resources in the sandbox."""
|
||||
"""
|
||||
Remove the oldest files in a directory 'dir', leaving at most 'max_files' remaining.
|
||||
We use this to limit the number of resources in the sandbox.
|
||||
"""
|
||||
filepaths = [os.path.join(dir, file) for file in os.listdir(dir) if not file.startswith('.')]
|
||||
ctime_sorted_paths = [item[0] for item in sorted([(path, os.path.getctime(path)) for path in filepaths],
|
||||
key=operator.itemgetter(1), reverse=True)]
|
||||
[os.remove(path) for path in ctime_sorted_paths[max_files:]]
|
||||
|
||||
|
||||
class ObjectStoreRoot(Resource):
|
||||
"""Root of the Object Store API.
|
||||
Allows the client to get a complete list of all the stored objects, or to create a new stored object."""
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'POST')
|
||||
class ObjectStoreRoot(View):
|
||||
"""
|
||||
Root of the Object Store API.
|
||||
Allows the client to get a complete list of all the stored objects, or to create a new stored object.
|
||||
"""
|
||||
|
||||
def get(self, request, auth):
|
||||
"""Return a list of all the stored object URLs. (Ordered by creation time, newest first)"""
|
||||
def get(self, request):
|
||||
"""
|
||||
Return a list of all the stored object URLs. (Ordered by creation time, newest first)
|
||||
"""
|
||||
filepaths = [os.path.join(OBJECT_STORE_DIR, file) for file in os.listdir(OBJECT_STORE_DIR) if not file.startswith('.')]
|
||||
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
|
||||
key=operator.itemgetter(1), reverse=True)]
|
||||
return [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]
|
||||
|
||||
def post(self, request, auth, content):
|
||||
"""Create a new stored object, with a unique key."""
|
||||
def post(self, request):
|
||||
"""
|
||||
Create a new stored object, with a unique key.
|
||||
"""
|
||||
key = str(uuid.uuid1())
|
||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||
pickle.dump(content, open(pathname, 'wb'))
|
||||
pickle.dump(self.CONTENT, open(pathname, 'wb'))
|
||||
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
|
||||
return Response(status.HTTP_201_CREATED, content, {'Location': reverse('stored-object', kwargs={'key':key})})
|
||||
return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': reverse('stored-object', kwargs={'key':key})})
|
||||
|
||||
|
||||
class StoredObject(Resource):
|
||||
"""Represents a stored object.
|
||||
The object may be any picklable content."""
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
class StoredObject(View):
|
||||
"""
|
||||
Represents a stored object.
|
||||
The object may be any picklable content.
|
||||
"""
|
||||
|
||||
def get(self, request, auth, key):
|
||||
"""Return a stored object, by unpickling the contents of a locally stored file."""
|
||||
def get(self, request, key):
|
||||
"""
|
||||
Return a stored object, by unpickling the contents of a locally stored file.
|
||||
"""
|
||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||
if not os.path.exists(pathname):
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
return pickle.load(open(pathname, 'rb'))
|
||||
|
||||
def put(self, request, auth, content, key):
|
||||
"""Update/create a stored object, by pickling the request content to a locally stored file."""
|
||||
def put(self, request, key):
|
||||
"""
|
||||
Update/create a stored object, by pickling the request content to a locally stored file.
|
||||
"""
|
||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||
pickle.dump(content, open(pathname, 'wb'))
|
||||
return content
|
||||
pickle.dump(self.CONTENT, open(pathname, 'wb'))
|
||||
return self.CONTENT
|
||||
|
||||
def delete(self, request, auth, key):
|
||||
"""Delete a stored object, by removing it's pickled file."""
|
||||
def delete(self, request):
|
||||
"""
|
||||
Delete a stored object, by removing it's pickled file.
|
||||
"""
|
||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
||||
if not os.path.exists(pathname):
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
from django.test import TestCase
|
||||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.compat import RequestFactory
|
||||
|
||||
from pygments_api import views
|
||||
import tempfile, shutil
|
||||
|
||||
|
||||
|
||||
class TestPygmentsExample(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -2,9 +2,10 @@ from __future__ import with_statement # for python 2.5
|
|||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.resources import FormResource
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework.emitters import BaseEmitter
|
||||
from djangorestframework.renderers import BaseRenderer
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework import status
|
||||
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
@ -17,73 +18,98 @@ import os
|
|||
import uuid
|
||||
import operator
|
||||
|
||||
# We need somewhere to store the code that we highlight
|
||||
# We need somewhere to store the code snippets that we highlight
|
||||
HIGHLIGHTED_CODE_DIR = os.path.join(settings.MEDIA_ROOT, 'pygments')
|
||||
MAX_FILES = 10
|
||||
|
||||
|
||||
def list_dir_sorted_by_ctime(dir):
|
||||
"""Return a list of files sorted by creation time"""
|
||||
"""
|
||||
Return a list of files sorted by creation time
|
||||
"""
|
||||
filepaths = [os.path.join(dir, file) for file in os.listdir(dir) if not file.startswith('.')]
|
||||
return [item[0] for item in sorted([(path, os.path.getctime(path)) for path in filepaths],
|
||||
key=operator.itemgetter(1), reverse=False)]
|
||||
return [item[0] for item in sorted( [(path, os.path.getctime(path)) for path in filepaths],
|
||||
key=operator.itemgetter(1), reverse=False) ]
|
||||
|
||||
def remove_oldest_files(dir, max_files):
|
||||
"""Remove the oldest files in a directory 'dir', leaving at most 'max_files' remaining.
|
||||
We use this to limit the number of resources in the sandbox."""
|
||||
"""
|
||||
Remove the oldest files in a directory 'dir', leaving at most 'max_files' remaining.
|
||||
We use this to limit the number of resources in the sandbox.
|
||||
"""
|
||||
[os.remove(path) for path in list_dir_sorted_by_ctime(dir)[max_files:]]
|
||||
|
||||
|
||||
class HTMLEmitter(BaseEmitter):
|
||||
"""Basic emitter which just returns the content without any further serialization."""
|
||||
class HTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Basic renderer which just returns the content without any further serialization.
|
||||
"""
|
||||
media_type = 'text/html'
|
||||
|
||||
|
||||
class PygmentsRoot(Resource):
|
||||
"""This example demonstrates a simple RESTful Web API aound the awesome pygments library.
|
||||
This top level resource is used to create highlighted code snippets, and to list all the existing code snippets."""
|
||||
form = PygmentsForm
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'POST',)
|
||||
|
||||
def get(self, request, auth):
|
||||
"""Return a list of all currently existing snippets."""
|
||||
class PygmentsFormResource(FormResource):
|
||||
"""
|
||||
"""
|
||||
form = PygmentsForm
|
||||
|
||||
|
||||
class PygmentsRoot(View):
|
||||
"""
|
||||
This example demonstrates a simple RESTful Web API aound the awesome pygments library.
|
||||
This top level resource is used to create highlighted code snippets, and to list all the existing code snippets.
|
||||
"""
|
||||
resource = PygmentsFormResource
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Return a list of all currently existing snippets.
|
||||
"""
|
||||
unique_ids = [os.path.split(f)[1] for f in list_dir_sorted_by_ctime(HIGHLIGHTED_CODE_DIR)]
|
||||
return [reverse('pygments-instance', args=[unique_id]) for unique_id in unique_ids]
|
||||
|
||||
def post(self, request, auth, content):
|
||||
"""Create a new highlighed snippet and return it's location.
|
||||
For the purposes of the sandbox example, also ensure we delete the oldest snippets if we have > MAX_FILES."""
|
||||
def post(self, request):
|
||||
"""
|
||||
Create a new highlighed snippet and return it's location.
|
||||
For the purposes of the sandbox example, also ensure we delete the oldest snippets if we have > MAX_FILES.
|
||||
"""
|
||||
unique_id = str(uuid.uuid1())
|
||||
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
||||
|
||||
lexer = get_lexer_by_name(content['lexer'])
|
||||
linenos = 'table' if content['linenos'] else False
|
||||
options = {'title': content['title']} if content['title'] else {}
|
||||
formatter = HtmlFormatter(style=content['style'], linenos=linenos, full=True, **options)
|
||||
lexer = get_lexer_by_name(self.CONTENT['lexer'])
|
||||
linenos = 'table' if self.CONTENT['linenos'] else False
|
||||
options = {'title': self.CONTENT['title']} if self.CONTENT['title'] else {}
|
||||
formatter = HtmlFormatter(style=self.CONTENT['style'], linenos=linenos, full=True, **options)
|
||||
|
||||
with open(pathname, 'w') as outfile:
|
||||
highlight(content['code'], lexer, formatter, outfile)
|
||||
highlight(self.CONTENT['code'], lexer, formatter, outfile)
|
||||
|
||||
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
||||
|
||||
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', args=[unique_id])})
|
||||
|
||||
|
||||
class PygmentsInstance(Resource):
|
||||
"""Simply return the stored highlighted HTML file with the correct mime type.
|
||||
This Resource only emits HTML and uses a standard HTML emitter rather than the emitters.DocumentingHTMLEmitter class."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
emitters = (HTMLEmitter,)
|
||||
class PygmentsInstance(View):
|
||||
"""
|
||||
Simply return the stored highlighted HTML file with the correct mime type.
|
||||
This Resource only renders HTML and uses a standard HTML renderer rather than the renderers.DocumentingHTMLRenderer class.
|
||||
"""
|
||||
renderers = (HTMLRenderer,)
|
||||
|
||||
def get(self, request, auth, unique_id):
|
||||
"""Return the highlighted snippet."""
|
||||
def get(self, request, unique_id):
|
||||
"""
|
||||
Return the highlighted snippet.
|
||||
"""
|
||||
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
||||
if not os.path.exists(pathname):
|
||||
return Resource(status.HTTP_404_NOT_FOUND)
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
return open(pathname, 'r').read()
|
||||
|
||||
def delete(self, request, auth, unique_id):
|
||||
"""Delete the highlighted snippet."""
|
||||
def delete(self, request, unique_id):
|
||||
"""
|
||||
Delete the highlighted snippet.
|
||||
"""
|
||||
pathname = os.path.join(HIGHLIGHTED_CODE_DIR, unique_id)
|
||||
if not os.path.exists(pathname):
|
||||
return Resource(status.HTTP_404_NOT_FOUND)
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
return os.remove(pathname)
|
||||
|
||||
|
|
|
@ -1,31 +1,42 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.resources import FormResource
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework import status
|
||||
|
||||
from resourceexample.forms import MyForm
|
||||
|
||||
class ExampleResource(Resource):
|
||||
"""A basic read-only resource that points to 3 other resources."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
class MyFormValidation(FormResource):
|
||||
"""
|
||||
A resource which applies form validation on the input.
|
||||
"""
|
||||
form = MyForm
|
||||
|
||||
def get(self, request, auth):
|
||||
|
||||
class ExampleResource(View):
|
||||
"""
|
||||
A basic read-only resource that points to 3 other resources.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
return {"Some other resources": [reverse('another-example-resource', kwargs={'num':num}) for num in range(3)]}
|
||||
|
||||
class AnotherExampleResource(Resource):
|
||||
"""A basic GET-able/POST-able resource."""
|
||||
allowed_methods = anon_allowed_methods = ('GET', 'POST')
|
||||
form = MyForm # Optional form validation on input (Applies in this case the POST method, but can also apply to PUT)
|
||||
|
||||
def get(self, request, auth, num):
|
||||
class AnotherExampleResource(View):
|
||||
"""
|
||||
A basic GET-able/POST-able resource.
|
||||
"""
|
||||
resource = MyFormValidation
|
||||
|
||||
def get(self, request, num):
|
||||
"""Handle GET requests"""
|
||||
if int(num) > 2:
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
return "GET request to AnotherExampleResource %s" % num
|
||||
|
||||
def post(self, request, auth, content, num):
|
||||
def post(self, request, num):
|
||||
"""Handle POST requests"""
|
||||
if int(num) > 2:
|
||||
return Response(status.HTTP_404_NOT_FOUND)
|
||||
return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(content))
|
||||
return "POST request to AnotherExampleResource %s, with content: %s" % (num, repr(self.CONTENT))
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
"""The root view for the examples provided with Django REST framework"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import View
|
||||
|
||||
|
||||
class Sandbox(Resource):
|
||||
class Sandbox(View):
|
||||
"""This is the sandbox for the examples provided with [Django REST framework](http://django-rest-framework.org).
|
||||
|
||||
These examples are provided to help you get a better idea of the some of the features of RESTful APIs created using the framework.
|
||||
|
||||
All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line...
|
||||
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ # (Use default emitter)
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation emitter)
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer)
|
||||
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
|
||||
|
||||
The examples provided:
|
||||
|
||||
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
|
||||
2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class.
|
||||
3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [EmitterMixin](http://django-rest-framework.org/library/emitters.html).
|
||||
3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [RendererMixin](http://django-rest-framework.org/library/renderers.html).
|
||||
4. A generic object store API.
|
||||
5. A code highlighting API.
|
||||
6. A blog posts and comments API.
|
||||
|
||||
Please feel free to browse, create, edit and delete the resources in these examples."""
|
||||
allowed_methods = anon_allowed_methods = ('GET',)
|
||||
|
||||
def get(self, request, auth):
|
||||
def get(self, request):
|
||||
return [{'name': 'Simple Resource example', 'url': reverse('example-resource')},
|
||||
{'name': 'Simple ModelResource example', 'url': reverse('my-model-root-resource')},
|
||||
{'name': 'Simple ModelResource example', 'url': reverse('model-resource-root')},
|
||||
{'name': 'Simple Mixin-only example', 'url': reverse('mixin-view')},
|
||||
{'name': 'Object store API', 'url': reverse('object-store-root')},
|
||||
{'name': 'Code highlighting API', 'url': reverse('pygments-root')},
|
||||
{'name': 'Blog posts API', 'url': reverse('blog-posts')}]
|
||||
{'name': 'Blog posts API', 'url': reverse('blog-posts-root')}]
|
||||
|
|
|
@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url
|
|||
from django.conf import settings
|
||||
from sandbox.views import Sandbox
|
||||
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
(r'robots.txt', 'deny_robots'),
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', Sandbox.as_view()),
|
||||
|
||||
(r'^resource-example/', include('resourceexample.urls')),
|
||||
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
||||
(r'^mixin/', include('mixin.urls')),
|
||||
|
@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views',
|
|||
(r'^pygments/', include('pygments_api.urls')),
|
||||
(r'^blog-post/', include('blogpost.urls')),
|
||||
|
||||
(r'^accounts/login/$', 'api_login'),
|
||||
(r'^accounts/logout/$', 'api_logout'),
|
||||
(r'^', include('djangorestframework.urls')),
|
||||
)
|
||||
|
||||
# Only serve favicon in production because otherwise chrome users will pretty much
|
||||
# permanantly have the django-rest-framework favicon whenever they navigate to
|
||||
# 127.0.0.1:8000 or whatever, which gets annoying
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += patterns('djangorestframework.views',
|
||||
(r'favicon.ico', 'favicon'),
|
||||
)
|
||||
|
|
3
setup.py
3
setup.py
|
@ -21,7 +21,8 @@ setup(
|
|||
packages = ['djangorestframework',
|
||||
'djangorestframework.templatetags',
|
||||
'djangorestframework.tests',
|
||||
'djangorestframework.runtests'],
|
||||
'djangorestframework.runtests',
|
||||
'djangorestframework.utils'],
|
||||
package_dir={'djangorestframework': 'djangorestframework'},
|
||||
package_data = {'djangorestframework': ['templates/*', 'static/*']},
|
||||
test_suite = 'djangorestframework.runtests.runcoverage.main',
|
||||
|
|
Loading…
Reference in New Issue
Block a user