Commit b5049285 by Tom Christie

pull in -dev as 0.2.0

parents 7ee9adbe fc1640de
......@@ -2,6 +2,7 @@ syntax: glob
*.pyc
*.db
assetplatform.egg-info/*
*~
coverage.xml
env
......
......@@ -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.
......
__version__ = '0.1.1'
__version__ = '0.2.0'
VERSION = __version__ # synonym
"""
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
"""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
"""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)
\ No newline at end of file
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
\ No newline at end of file
"""Get a descriptive name and description for a view,
based on class name and docstring, and override-able by 'name' and 'description' attributes"""
import re
def get_name(view):
"""Return a name for the view.
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
if getattr(view, 'name', None) is not None:
return view.name
if getattr(view, '__name__', None) is not None:
name = view.__name__
elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
name = view.__class__.__name__
else:
return ''
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
def get_description(view):
"""Provide a description for the view.
By default this is the view's docstring with nice unindention applied."""
if getattr(view, 'description', None) is not None:
return getattr(view, 'description')
if getattr(view, '__doc__', None) is not None:
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)
return view.__doc__
return ''
\ No newline at end of file
"""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
\ No newline at end of file
"""
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
"""
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)
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)
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)
from django.core.handlers.wsgi import STATUS_CODE_TEXT
"""
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.
"""
__all__ =['NoContent', 'Response', ]
from django.core.handlers.wsgi import STATUS_CODE_TEXT
__all__ = ('Response', 'ErrorResponse')
# TODO: remove raw_content/cleaned_content and just use content?
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.
class Response(object):
"""
An HttpResponse that may include content that hasn't yet been serialized.
"""
pass
class Response(object):
def __init__(self, status=200, content=NoContent, headers={}):
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)
"""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 = ''
......
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,
......
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)
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'
......
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)
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)
self.assertEquals(get_description(MockView()), DESCRIPTION)
# 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"""
......
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)
\ No newline at end of file
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"}')
......
# 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')
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)
"""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)
\ No newline at end of file
......@@ -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')
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
......@@ -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'),
......
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'),
)
\ No newline at end of file
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)
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 == '':
......
"""
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
"""
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
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)
"""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))
......@@ -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:
\ No newline at end of file
#.. include:: ../../examples/blogpost/views.py
# :literal:
\ No newline at end of file
......@@ -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
------------------
......
:mod:`authenticators`
:mod:`authentication`
=====================
.. automodule:: authenticators
.. automodule:: authentication
:members:
:mod:`compat`
=====================
.. automodule:: compat
:members:
: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:
:mod:`mixins`
=====================
.. automodule:: mixins
:members:
:mod:`modelresource`
====================
.. note::
TODO - document this module properly
.. automodule:: modelresource
:members:
:mod:`permissions`
=====================
.. automodule:: permissions
:members:
: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:
: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:
:mod:`validators`
=================
.. automodule:: validators
:members:
: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})
"""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
......
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)),
)
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
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()
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)),
)
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
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)
......
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."""
class PygmentsFormResource(FormResource):
"""
"""
form = PygmentsForm
allowed_methods = anon_allowed_methods = ('GET', 'POST',)
def get(self, request, auth):
"""Return a list of all currently existing snippets."""
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)
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))
"""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'),
)
......@@ -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',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment