merged master into experimental

This commit is contained in:
Sébastien Piquemal 2012-01-03 11:02:46 +02:00
commit 59e6cd9892
83 changed files with 1264 additions and 546 deletions

View File

@ -19,6 +19,9 @@ Danilo Bargen <gwrtheyrn>
Andrew McCloud <amccloud> Andrew McCloud <amccloud>
Thomas Steinacher <thomasst> Thomas Steinacher <thomasst>
Meurig Freeman <meurig> Meurig Freeman <meurig>
Anthony Nemitz <anemitz>
Ewoud Kohl van Wijngaarden <ekohl>
Michael Ding <yandy>
THANKS TO: THANKS TO:

21
LICENSE
View File

@ -1,9 +1,22 @@
Copyright (c) 2011, Tom Christie Copyright (c) 2011, Tom Christie
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions of source code must retain the above copyright notice, this
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -16,10 +16,12 @@ Full documentation for the project is available at http://django-rest-framework.
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_. Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_. General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework/>`_ which runs our test suite.
Requirements: Requirements:
* Python (2.5, 2.6, 2.7 supported) * Python (2.5, 2.6, 2.7 supported)
* Django (1.2, 1.3 supported) * Django (1.2, 1.3, 1.4-alpha supported)
Installation Notes Installation Notes
@ -33,7 +35,7 @@ To clone the project from GitHub using git::
To install django-rest-framework in a virtualenv environment:: To install django-rest-framework in a virtualenv environment::
cd django-rest-framework cd django-rest-framework
virtualenv --no-site-packages --distribute --python=python2.6 env virtualenv --no-site-packages --distribute env
source env/bin/activate source env/bin/activate
pip install -r requirements.txt # django, coverage pip install -r requirements.txt # django, coverage

View File

@ -1,3 +1,8 @@
0.3.0
* JSONP Support
* Bugfixes, including support for latest markdown release
0.2.4 0.2.4
* Fix broken IsAdminUser permission. * Fix broken IsAdminUser permission.

View File

@ -1,3 +1,3 @@
__version__ = '0.2.4' __version__ = '0.3.1-dev'
VERSION = __version__ # synonym VERSION = __version__ # synonym

View File

@ -11,7 +11,8 @@ set of :class:`authentication` classes.
""" """
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.middleware.csrf import CsrfViewMiddleware from djangorestframework.compat import CsrfViewMiddleware
from djangorestframework.utils import as_tuple
import base64 import base64
__all__ = ( __all__ = (
@ -67,7 +68,7 @@ class BasicAuthentication(BaseAuthentication):
supplied using HTTP Basic authentication. supplied using HTTP Basic authentication.
Otherwise returns :const:`None`. Otherwise returns :const:`None`.
""" """
from django.utils import encoding from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
if 'HTTP_AUTHORIZATION' in request.META: if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split() auth = request.META['HTTP_AUTHORIZATION'].split()
@ -83,10 +84,9 @@ class BasicAuthentication(BaseAuthentication):
except encoding.DjangoUnicodeDecodeError: except encoding.DjangoUnicodeDecodeError:
return None return None
user = self._authenticate_user(username, password) user = authenticate(username=username, password=password)
if user: if user is not None and user.is_active:
return user return user
return None return None
@ -100,18 +100,26 @@ class UserLoggedInAuthentication(BaseAuthentication):
Returns a :obj:`User` if the request session currently has a logged in Returns a :obj:`User` if the request session currently has a logged in
user. Otherwise returns :const:`None`. user. Otherwise returns :const:`None`.
""" """
# TODO: Switch this back to request.POST, and let # TODO: Might be cleaner to switch this back to using request.POST,
# FormParser/MultiPartParser deal with the consequences. # and let FormParser/MultiPartParser deal with the consequences.
if getattr(request, 'user', None) and request.user.is_active: if getattr(request, 'user', None) and request.user.is_active:
# If this is a POST request we enforce CSRF validation. # Enforce CSRF validation for session based authentication.
# Temporarily replace request.POST with .DATA, to use our generic parsing.
# If DATA is not dict-like, use an empty dict.
if request.method.upper() == 'POST': if request.method.upper() == 'POST':
# Temporarily replace request.POST with .DATA, if hasattr(self.view.DATA, 'get'):
# so that we use our more generic request parsing
request._post = self.view.DATA request._post = self.view.DATA
else:
request._post = {}
resp = CsrfViewMiddleware().process_view(request, None, (), {}) resp = CsrfViewMiddleware().process_view(request, None, (), {})
# Replace request.POST
if request.method.upper() == 'POST':
del(request._post) del(request._post)
if resp is not None: # csrf failed
return None if resp is None: # csrf passed
return request.user return request.user
return None return None

View File

@ -1,24 +1,25 @@
""" """
The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. The :mod:`compat` module provides support for backwards compatibility with older versions of django/python.
""" """
import django
# cStringIO only if it's available # cStringIO only if it's available, otherwise StringIO
try: try:
import cStringIO as StringIO import cStringIO as StringIO
except ImportError: except ImportError:
import StringIO import StringIO
# parse_qs # parse_qs from 'urlparse' module unless python 2.5, in which case from 'cgi'
try: try:
# python >= ? # python >= 2.6
from urlparse import parse_qs from urlparse import parse_qs
except ImportError: except ImportError:
# python <= ? # python < 2.6
from cgi import parse_qs from cgi import parse_qs
# django.test.client.RequestFactory (Django >= 1.3) # django.test.client.RequestFactory (Required for Django < 1.3)
try: try:
from django.test.client import RequestFactory from django.test.client import RequestFactory
except ImportError: except ImportError:
@ -156,18 +157,233 @@ except ImportError:
def head(self, request, *args, **kwargs): def head(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware
else:
import hashlib
import re
import random
import logging
import urlparse
from django.conf import settings
from django.core.urlresolvers import get_callable
try:
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
logger = logging.getLogger('django.request')
if not logger.handlers:
logger.addHandler(NullHandler())
def same_origin(url1, url2):
"""
Checks if two URLs are 'same-origin'
"""
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
return p1[0:2] == p2[0:2]
def constant_time_compare(val1, val2):
"""
Returns True if the two strings are equal, False otherwise.
The time taken is independent of the number of characters that match.
"""
if len(val1) != len(val2):
return False
result = 0
for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y)
return result == 0
# Use the system (hardware-based) random number generator if it exists.
if hasattr(random, 'SystemRandom'):
randrange = random.SystemRandom().randrange
else:
randrange = random.randrange
_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63
REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
def _get_failure_view():
"""
Returns the view to be used for CSRF rejections
"""
return get_callable(settings.CSRF_FAILURE_VIEW)
def _get_new_csrf_key():
return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
def get_token(request):
"""
Returns the the CSRF token required for a POST form. The token is an
alphanumeric value.
A side effect of calling this function is to make the the csrf_protect
decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
header to the outgoing response. For this reason, you may need to use this
function lazily, as is done by the csrf context processor.
"""
request.META["CSRF_COOKIE_USED"] = True
return request.META.get("CSRF_COOKIE", None)
def _sanitize_token(token):
# Allow only alphanum, and ensure we return a 'str' for the sake of the post
# processing middleware.
token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
if token == "":
# In case the cookie has been truncated to nothing at some point.
return _get_new_csrf_key()
else:
return token
class CsrfViewMiddleware(object):
"""
Middleware that requires a present and correct csrfmiddlewaretoken
for POST requests that have a CSRF cookie, and sets an outgoing
CSRF cookie.
This middleware should be used in conjunction with the csrf_token template
tag.
"""
# The _accept and _reject methods currently only exist for the sake of the
# requires_csrf_token decorator.
def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
def _reject(self, request, reason):
return _get_failure_view()(request, reason=reason)
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
try:
csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
csrf_token = None
# Generate token and store it in the request, so it's available to the view.
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RC2616 needs protection.
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite. It comes after
# the creation of CSRF cookies, so that everything else continues to
# work exactly the same (e.g. cookies are sent etc), but before the
# any branches that call reject()
return self._accept(request)
if request.is_secure():
# Suppose user visits http://example.com/
# An active network attacker,(man-in-the-middle, MITM) sends a
# POST form which targets https://example.com/detonate-bomb/ and
# submits it via javascript.
#
# The attacker will need to provide a CSRF cookie and token, but
# that is no problem for a MITM and the session independent
# nonce we are using. So the MITM can circumvent the CSRF
# protection. This is true for any HTTP connection, but anyone
# using HTTPS expects better! For this reason, for
# https://example.com/ we need additional protection that treats
# http://example.com/ as completely untrusted. Under HTTPS,
# Barth et al. found that the Referer header is missing for
# same-domain requests in only about 0.2% of cases or less, so
# we can use strict Referer checking.
referer = request.META.get('HTTP_REFERER')
if referer is None:
logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_REFERER)
# Note that request.get_host() includes the port
good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
reason = REASON_BAD_REFERER % (referer, good_referer)
logger.warning('Forbidden (%s): %s' % (reason, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, reason)
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_NO_CSRF_COOKIE)
# check non-cookie token for match
request_csrf_token = ""
if request.method == "POST":
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
if not constant_time_compare(request_csrf_token, csrf_token):
logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
extra={
'status_code': 403,
'request': request,
}
)
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
# Markdown is optional # Markdown is optional
try: try:
import markdown import markdown
import re
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor): class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
""" """
Override `markdown`'s :class:`SetextHeaderProcessor`, so that ==== headers are <h2> and ---- headers are <h3>. Class for markdown < 2.1
Override `markdown`'s :class:`SetextHeaderProcessor`, so that ==== headers are <h2> and ---- heade
We use <h1> for the resource name. We use <h1> for the resource name.
""" """
import re
# Detect Setext-style header. Must be first 2 lines of block. # Detect Setext-style header. Must be first 2 lines of block.
RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE) RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
@ -189,18 +405,22 @@ try:
def apply_markdown(text): def apply_markdown(text):
""" """
Simple wrapper around :func:`markdown.markdown` to apply our :class:`CustomSetextHeaderProcessor`, Simple wrapper around :func:`markdown.markdown` to set the base level
and also set the base level of '#' style headers to <h2>. of '#' style headers to <h2>.
""" """
extensions = ['headerid(level=2)'] extensions = ['headerid(level=2)']
safe_mode = False, safe_mode = False,
if markdown.version_info < (2, 1):
output_format = markdown.DEFAULT_OUTPUT_FORMAT output_format = markdown.DEFAULT_OUTPUT_FORMAT
md = markdown.Markdown(extensions=markdown.load_extensions(extensions), md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
safe_mode=safe_mode, safe_mode=safe_mode,
output_format=output_format) output_format=output_format)
md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser) md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
else:
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text) return md.convert(text)
except ImportError: except ImportError:
@ -211,3 +431,4 @@ try:
import yaml import yaml
except ImportError: except ImportError:
yaml = None yaml = None

View File

@ -26,6 +26,7 @@ __all__ = (
'BaseRenderer', 'BaseRenderer',
'TemplateRenderer', 'TemplateRenderer',
'JSONRenderer', 'JSONRenderer',
'JSONPRenderer',
'DocumentingHTMLRenderer', 'DocumentingHTMLRenderer',
'DocumentingXHTMLRenderer', 'DocumentingXHTMLRenderer',
'DocumentingPlainTextRenderer', 'DocumentingPlainTextRenderer',
@ -113,6 +114,28 @@ class JSONRenderer(BaseRenderer):
return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys) return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys)
class JSONPRenderer(JSONRenderer):
"""
Renderer which serializes to JSONP
"""
media_type = 'application/json-p'
format = 'json-p'
renderer_class = JSONRenderer
callback_parameter = 'callback'
def _get_callback(self):
return self.view.request.GET.get(self.callback_parameter, self.callback_parameter)
def _get_renderer(self):
return self.renderer_class(self.view)
def render(self, obj=None, media_type=None):
callback = self._get_callback()
json = self._get_renderer().render(obj, media_type)
return "%s(%s);" % (callback, json)
class XMLRenderer(BaseRenderer): class XMLRenderer(BaseRenderer):
""" """
Renderer which serializes to XML. Renderer which serializes to XML.
@ -326,7 +349,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
'logout_url': logout_url, 'logout_url': logout_url,
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX 'ADMIN_MEDIA_PREFIX': getattr(settings, 'ADMIN_MEDIA_PREFIX', None),
}) })
ret = template.render(context) ret = template.render(context)
@ -376,6 +399,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
DEFAULT_RENDERERS = ( JSONRenderer, DEFAULT_RENDERERS = ( JSONRenderer,
JSONPRenderer,
DocumentingHTMLRenderer, DocumentingHTMLRenderer,
DocumentingXHTMLRenderer, DocumentingXHTMLRenderer,
DocumentingPlainTextRenderer, DocumentingPlainTextRenderer,

View File

@ -78,6 +78,7 @@ class FormResource(Resource):
def validate_request(self, data, files=None): def validate_request(self, data, files=None):
""" """
Given some content as input return some cleaned, validated content. Given some content as input return some cleaned, validated content.
Raises a :exc:`response.ErrorResponse` with status code 400 Raises a :exc:`response.ErrorResponse` with status code 400
# (Bad Request) on failure. # (Bad Request) on failure.
@ -125,16 +126,15 @@ class FormResource(Resource):
data = data and data or {} data = data and data or {}
files = files and files or {} files = files and files or {}
seen_fields = set(data.keys())
form_fields = set(bound_form.fields.keys())
allowed_extra_fields = set(allowed_extra_fields)
# In addition to regular validation we also ensure no additional fields # In addition to regular validation we also ensure no additional fields
# are being passed in... # are being passed in...
# TODO: Hardcoded ignore_fields here is pretty icky. seen_fields_set = set(data.keys())
ignore_fields = set(('csrfmiddlewaretoken', '_accept', '_method')) form_fields_set = set(bound_form.fields.keys())
allowed_fields = form_fields | allowed_extra_fields | ignore_fields allowed_extra_fields_set = set(allowed_extra_fields)
unknown_fields = seen_fields - allowed_fields
# In addition to regular validation we also ensure no additional fields are being passed in...
unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
# Check using both regular validation, and our stricter no additional fields rule # Check using both regular validation, and our stricter no additional fields rule
if bound_form.is_valid() and not unknown_fields: if bound_form.is_valid() and not unknown_fields:
@ -178,7 +178,7 @@ class FormResource(Resource):
field_errors[key] = [u'This field does not exist.'] field_errors[key] = [u'This field does not exist.']
if field_errors: if field_errors:
detail[u'field-errors'] = field_errors detail[u'field_errors'] = field_errors
# Return HTTP 400 response (BAD REQUEST) # Return HTTP 400 response (BAD REQUEST)
raise ErrorResponse(400, detail) raise ErrorResponse(400, detail)
@ -204,6 +204,7 @@ class FormResource(Resource):
return form return form
def get_bound_form(self, data=None, files=None, method=None): def get_bound_form(self, data=None, files=None, method=None):
""" """
Given some content return a Django form bound to that content. Given some content return a Django form bound to that content.
@ -302,6 +303,7 @@ class ModelResource(FormResource):
def validate_request(self, data, files=None): def validate_request(self, data, files=None):
""" """
Given some content as input return some cleaned, validated content. Given some content as input return some cleaned, validated content.
Raises a :exc:`response.ErrorResponse` with status code 400 Raises a :exc:`response.ErrorResponse` with status code 400
(Bad Request) on failure. (Bad Request) on failure.
@ -410,7 +412,8 @@ class ModelResource(FormResource):
if self.fields: if self.fields:
return model_fields & set(self.fields) return model_fields & set(self.fields)
return model_fields - set(self.exclude) return model_fields - set(as_tuple(self.exclude))
@property @property
def _property_fields_set(self): def _property_fields_set(self):

View File

@ -95,7 +95,6 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'djangorestframework', 'djangorestframework',
'djangorestframework.tests',
) )
# OAuth support is optional, so we only test oauth if it's installed. # OAuth support is optional, so we only test oauth if it's installed.

View File

@ -19,8 +19,11 @@ indented
# hash style header #""" # hash style header #"""
# If markdown is installed we also test it's working (and that our wrapped forces '=' to h2 and '-' to h3) # If markdown is installed we also test it's working
MARKED_DOWN = """<h2>an example docstring</h2> # (and that our wrapped forces '=' to h2 and '-' to h3)
# We support markdown < 2.1 and markdown >= 2.1
MARKED_DOWN_lt_21 = """<h2>an example docstring</h2>
<ul> <ul>
<li>list</li> <li>list</li>
<li>list</li> <li>list</li>
@ -31,6 +34,17 @@ MARKED_DOWN = """<h2>an example docstring</h2>
<p>indented</p> <p>indented</p>
<h2 id="hash_style_header">hash style header</h2>""" <h2 id="hash_style_header">hash style header</h2>"""
MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2>
<ul>
<li>list</li>
<li>list</li>
</ul>
<h3 id="another-header">another header</h3>
<pre><code>code block
</code></pre>
<p>indented</p>
<h2 id="hash-style-header">hash style header</h2>"""
class TestViewNamesAndDescriptions(TestCase): class TestViewNamesAndDescriptions(TestCase):
def test_resource_name_uses_classname_by_default(self): def test_resource_name_uses_classname_by_default(self):
@ -92,4 +106,6 @@ class TestViewNamesAndDescriptions(TestCase):
def test_markdown(self): def test_markdown(self):
"""Ensure markdown to HTML works as expected""" """Ensure markdown to HTML works as expected"""
if apply_markdown: if apply_markdown:
self.assertEquals(apply_markdown(DESCRIPTION), MARKED_DOWN) gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
self.assertTrue(gte_21_match or lt_21_match)

View File

@ -8,13 +8,15 @@ from djangorestframework.mixins import PaginatorMixin, ModelMixin
from djangorestframework.resources import ModelResource from djangorestframework.resources import ModelResource
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.tests.models import CustomUser from djangorestframework.tests.models import CustomUser
from djangorestframework.tests.testcases import TestModelsTestCase
from djangorestframework.views import View from djangorestframework.views import View
class TestModelCreation(TestCase): class TestModelCreation(TestModelsTestCase):
"""Tests on CreateModelMixin""" """Tests on CreateModelMixin"""
def setUp(self): def setUp(self):
super(TestModelsTestCase, self).setUp()
self.req = RequestFactory() self.req = RequestFactory()
def test_creation(self): def test_creation(self):

View File

@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User
from djangorestframework.resources import ModelResource from djangorestframework.resources import ModelResource
from djangorestframework.views import ListOrCreateModelView, InstanceModelView from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from djangorestframework.tests.models import CustomUser from djangorestframework.tests.models import CustomUser
from djangorestframework.tests.testcases import TestModelsTestCase
class GroupResource(ModelResource): class GroupResource(ModelResource):
model = Group model = Group
@ -31,7 +32,7 @@ urlpatterns = patterns('',
) )
class ModelViewTests(TestCase): class ModelViewTests(TestModelsTestCase):
"""Test the model views djangorestframework provides""" """Test the model views djangorestframework provides"""
urls = 'djangorestframework.tests.modelviews' urls = 'djangorestframework.tests.modelviews'

View File

@ -2,9 +2,10 @@ from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
from djangorestframework import status from djangorestframework import status
from djangorestframework.views import View
from djangorestframework.compat import View as DjangoView from djangorestframework.compat import View as DjangoView
from djangorestframework.renderers import BaseRenderer, JSONRenderer, \ from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
YAMLRenderer, XMLRenderer XMLRenderer, JSONPRenderer
from djangorestframework.parsers import JSONParser, YAMLParser from djangorestframework.parsers import JSONParser, YAMLParser
from djangorestframework.mixins import ResponseMixin from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response from djangorestframework.response import Response
@ -44,9 +45,16 @@ class MockView(ResponseMixin, DjangoView):
return self.render(response) return self.render(response)
class MockGETView(View):
def get(self, request, **kwargs):
return {'foo': ['bar', 'baz']}
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
) )
@ -190,6 +198,45 @@ class JSONRendererTests(TestCase):
self.assertEquals(obj, data) self.assertEquals(obj, data)
class JSONPRendererTests(TestCase):
"""
Tests specific to the JSONP Renderer
"""
urls = 'djangorestframework.tests.renderers'
def test_without_callback_with_json_renderer(self):
"""
Test JSONP rendering with View JSON Renderer.
"""
resp = self.client.get('/jsonp/jsonrenderer',
HTTP_ACCEPT='application/json-p')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/json-p')
self.assertEquals(resp.content, 'callback(%s);' % _flat_repr)
def test_without_callback_without_json_renderer(self):
"""
Test JSONP rendering without View JSON Renderer.
"""
resp = self.client.get('/jsonp/nojsonrenderer',
HTTP_ACCEPT='application/json-p')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/json-p')
self.assertEquals(resp.content, 'callback(%s);' % _flat_repr)
def test_with_callback(self):
"""
Test JSONP rendering with callback function name.
"""
callback_func = 'myjsonpcallback'
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
HTTP_ACCEPT='application/json-p')
self.assertEquals(resp.status_code, 200)
self.assertEquals(resp['Content-Type'], 'application/json-p')
self.assertEquals(resp.content, '%s(%s);' % (callback_func, _flat_repr))
if YAMLRenderer: if YAMLRenderer:
_yaml_repr = 'foo: [bar, baz]\n' _yaml_repr = 'foo: [bar, baz]\n'
@ -224,7 +271,6 @@ if YAMLRenderer:
self.assertEquals(obj, data) self.assertEquals(obj, data)
class XMLRendererTestCase(TestCase): class XMLRendererTestCase(TestCase):
""" """
Tests specific to the XML Renderer Tests specific to the XML Renderer
@ -280,7 +326,6 @@ class XMLRendererTestCase(TestCase):
content = renderer.render({'field': None}, 'application/xml') content = renderer.render({'field': None}, 'application/xml')
self.assertXMLContains(content, '<field></field>') self.assertXMLContains(content, '<field></field>')
def assertXMLContains(self, xml, string): def assertXMLContains(self, xml, string):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>')) self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>')) self.assertTrue(xml.endswith('</root>'))

View File

@ -56,8 +56,8 @@ class TestFieldNesting(TestCase):
self.serialize = self.serializer.serialize self.serialize = self.serializer.serialize
class M1(models.Model): class M1(models.Model):
field1 = models.CharField() field1 = models.CharField(max_length=256)
field2 = models.CharField() field2 = models.CharField(max_length=256)
class M2(models.Model): class M2(models.Model):
field = models.OneToOneField(M1) field = models.OneToOneField(M1)

View File

@ -0,0 +1,63 @@
# http://djangosnippets.org/snippets/1011/
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django.test import TestCase
NO_SETTING = ('!', None)
class TestSettingsManager(object):
"""
A class which can modify some Django settings temporarily for a
test and then revert them to their original values later.
Automatically handles resyncing the DB if INSTALLED_APPS is
modified.
"""
def __init__(self):
self._original_settings = {}
def set(self, **kwargs):
for k,v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
NO_SETTING))
setattr(settings, k, v)
if 'INSTALLED_APPS' in kwargs:
self.syncdb()
def syncdb(self):
loading.cache.loaded = False
call_command('syncdb', verbosity=0)
def revert(self):
for k,v in self._original_settings.iteritems():
if v == NO_SETTING:
delattr(settings, k)
else:
setattr(settings, k, v)
if 'INSTALLED_APPS' in self._original_settings:
self.syncdb()
self._original_settings = {}
class SettingsTestCase(TestCase):
"""
A subclass of the Django TestCase with a settings_manager
attribute which is an instance of TestSettingsManager.
Comes with a tearDown() method that calls
self.settings_manager.revert().
"""
def __init__(self, *args, **kwargs):
super(SettingsTestCase, self).__init__(*args, **kwargs)
self.settings_manager = TestSettingsManager()
def tearDown(self):
self.settings_manager.revert()
class TestModelsTestCase(SettingsTestCase):
def setUp(self, *args, **kwargs):
installed_apps = tuple(settings.INSTALLED_APPS) + ('djangorestframework.tests',)
self.settings_manager.set(INSTALLED_APPS=installed_apps)

View File

@ -149,7 +149,7 @@ class TestFormValidation(TestCase):
try: try:
validator.validate_request(content, None) validator.validate_request(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -159,7 +159,7 @@ class TestFormValidation(TestCase):
try: try:
validator.validate_request(content, None) validator.validate_request(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -169,7 +169,7 @@ class TestFormValidation(TestCase):
try: try:
validator.validate_request(content, None) validator.validate_request(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}}) self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -179,7 +179,7 @@ class TestFormValidation(TestCase):
try: try:
validator.validate_request(content, None) validator.validate_request(content, None)
except ErrorResponse, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'], self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}}) 'extra': ['This field does not exist.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover

View File

@ -18,6 +18,24 @@ from mediatypes import add_media_type_param, get_media_type_params, order_by_pre
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') 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 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):
return tuple(obj)
elif isinstance(obj, tuple):
return obj
return (obj,)
def url_resolves(url): def url_resolves(url):
""" """

View File

@ -1,6 +1,8 @@
from django.contrib.auth.views import * from django.contrib.auth.views import *
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
import base64 import base64
def deny_robots(request): def deny_robots(request):

9
docs/check_sphinx.py Normal file
View File

@ -0,0 +1,9 @@
import pytest
import subprocess
def test_build_docs(tmpdir):
doctrees = tmpdir.join("doctrees")
htmldir = "html" #we want to keep the docs
subprocess.check_call([
"sphinx-build", "-q", "-bhtml",
"-d", str(doctrees), ".", str(htmldir)])

View File

@ -14,8 +14,8 @@
import sys, os import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'djangorestframework')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'djangorestframework')) # for documenting the library
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'examples')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'examples')) # for importing settings
import settings import settings
from django.core.management import setup_environ from django.core.management import setup_environ
setup_environ(settings) setup_environ(settings)
@ -55,9 +55,13 @@ copyright = u'2011, Tom Christie'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.1'
import djangorestframework
version = djangorestframework.__version__
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.1' release = version
autodoc_member_order='bysource' autodoc_member_order='bysource'
@ -100,7 +104,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'default' html_theme = 'sphinxdoc'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@ -220,3 +224,5 @@ html_static_path = []
#man_pages = [ #man_pages = [
# () # ()
#] #]
linkcheck_timeout = 120 # seconds, set to extra large value for link_checks

10
docs/contents.rst Normal file
View File

@ -0,0 +1,10 @@
Documentation
=============
.. toctree::
:maxdepth: 2
howto
library
examples

23
docs/examples.rst Normal file
View File

@ -0,0 +1,23 @@
Examples
========
There are a few real world web API examples included with Django REST framework.
#. :doc:`examples/objectstore` - Using :class:`views.View` classes for APIs that do not map to models.
#. :doc:`examples/pygments` - Using :class:`views.View` classes with forms for input validation.
#. :doc:`examples/blogpost` - Using :class:`views.ModelView` classes for APIs that map directly to models.
All the examples are freely available for testing in the sandbox:
http://rest.ep.io
(The :doc:`examples/sandbox` resource is also documented.)
Example Reference
-----------------
.. toctree::
:maxdepth: 1
:glob:
examples/*

View File

@ -1,9 +1,7 @@
.. _blogposts:
Blog Posts API Blog Posts API
============== ==============
* http://api.django-rest-framework.org/blog-post/ * http://rest.ep.io/blog-post/
The models The models
---------- ----------

View File

@ -1,5 +1,3 @@
.. _modelviews:
Getting Started - Model Views Getting Started - Model Views
----------------------------- -----------------------------
@ -7,11 +5,11 @@ Getting Started - Model Views
A live sandbox instance of this API is available: A live sandbox instance of this API is available:
http://api.django-rest-framework.org/model-resource-example/ http://rest.ep.io/model-resource-example/
You can browse the API using a web browser, or from the command line:: You can browse the API using a web browser, or from the command line::
curl -X GET http://api.django-rest-framework.org/resource-example/ -H 'Accept: text/plain' curl -X GET http://rest.ep.io/resource-example/ -H 'Accept: text/plain'
Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways: Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways:
@ -43,16 +41,16 @@ And we're done. We've now got a fully browseable API, which supports multiple i
We can visit the API in our browser: We can visit the API in our browser:
* http://api.django-rest-framework.org/model-resource-example/ * http://rest.ep.io/model-resource-example/
Or access it from the command line using curl: Or access it from the command line using curl:
.. code-block:: bash .. code-block:: bash
# Demonstrates API's input validation using form input # Demonstrates API's input validation using form input
bash: curl -X POST --data 'foo=true' http://api.django-rest-framework.org/model-resource-example/ bash: curl -X POST --data 'foo=true' http://rest.ep.io/model-resource-example/
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
# Demonstrates API's input validation using JSON input # Demonstrates API's input validation using JSON input
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://api.django-rest-framework.org/model-resource-example/ bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://rest.ep.io/model-resource-example/
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}

View File

@ -1,9 +1,7 @@
.. _objectstore:
Object Store API Object Store API
================ ================
* http://api.django-rest-framework.org/object-store/ * http://rest.ep.io/object-store/
This example shows an object store API that can be used to store arbitrary serializable content. This example shows an object store API that can be used to store arbitrary serializable content.

View File

@ -0,0 +1,66 @@
Permissions
===========
This example will show how you can protect your api by using authentication
and how you can limit the amount of requests a user can do to a resource by setting
a throttle to your view.
Authentication
--------------
If you want to protect your api from unauthorized users, Django REST Framework
offers you two default authentication methods:
* Basic Authentication
* Django's session-based authentication
These authentication methods are by default enabled. But they are not used unless
you specifically state that your view requires authentication.
To do this you just need to import the `Isauthenticated` class from the frameworks' `permissions` module.::
from djangorestframework.permissions import IsAuthenticated
Then you enable authentication by setting the right 'permission requirement' to the `permissions` class attribute of your View like
the example View below.:
.. literalinclude:: ../../examples/permissionsexample/views.py
:pyobject: LoggedInExampleView
The `IsAuthenticated` permission will only let a user do a 'GET' if he is authenticated. Try it
yourself on the live sandbox__
__ http://rest.ep.io/permissions-example/loggedin
Throttling
----------
If you want to limit the amount of requests a client is allowed to do on
a resource, then you can set a 'throttle' to achieve this.
For this to work you'll need to import the `PerUserThrottling` class from the `permissions`
module.::
from djangorestframework.permissions import PerUserThrottling
In the example below we have limited the amount of requests one 'client' or 'user'
may do on our view to 10 requests per minute.:
.. literalinclude:: ../../examples/permissionsexample/views.py
:pyobject: ThrottlingExampleView
Try it yourself on the live sandbox__.
__ http://rest.ep.io/permissions-example/throttling
Now if you want a view to require both aurhentication and throttling, you simply declare them
both::
permissions = (PerUserThrottling, Isauthenticated)
To see what other throttles are available, have a look at the :mod:`permissions` module.
If you want to implement your own authentication method, then refer to the :mod:`authentication`
module.

View File

@ -1,5 +1,3 @@
.. _codehighlighting:
Code Highlighting API Code Highlighting API
===================== =====================
@ -8,11 +6,11 @@ We're going to provide a simple wrapper around the awesome `pygments <http://pyg
.. note:: .. note::
A live sandbox instance of this API is available at http://api.django-rest-framework.org/pygments/ A live sandbox instance of this API is available at http://rest.ep.io/pygments/
You can browse the API using a web browser, or from the command line:: You can browse the API using a web browser, or from the command line::
curl -X GET http://api.django-rest-framework.org/pygments/ -H 'Accept: text/plain' curl -X GET http://rest.ep.io/pygments/ -H 'Accept: text/plain'
URL configuration URL configuration
@ -79,13 +77,13 @@ For example if we make a POST request using form input:
.. code-block:: bash .. code-block:: bash
bash: curl -X POST --data 'code=print "hello, world!"' --data 'style=foobar' -H 'X-Requested-With: XMLHttpRequest' http://api.django-rest-framework.org/pygments/ bash: curl -X POST --data 'code=print "hello, world!"' --data 'style=foobar' -H 'X-Requested-With: XMLHttpRequest' http://rest.ep.io/pygments/
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}} {"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}
Or if we make the same request using JSON: Or if we make the same request using JSON:
.. code-block:: bash .. code-block:: bash
bash: curl -X POST --data-binary '{"code":"print \"hello, world!\"", "style":"foobar"}' -H 'Content-Type: application/json' -H 'X-Requested-With: XMLHttpRequest' http://api.django-rest-framework.org/pygments/ bash: curl -X POST --data-binary '{"code":"print \"hello, world!\"", "style":"foobar"}' -H 'Content-Type: application/json' -H 'X-Requested-With: XMLHttpRequest' http://rest.ep.io/pygments/
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}} {"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}

View File

@ -1,5 +1,3 @@
.. _sandbox:
Sandbox Root API Sandbox Root API
================ ================

View File

@ -1,5 +1,3 @@
.. _views:
Getting Started - Views Getting Started - Views
----------------------- -----------------------
@ -7,11 +5,11 @@ Getting Started - Views
A live sandbox instance of this API is available: A live sandbox instance of this API is available:
http://api.django-rest-framework.org/resource-example/ http://rest.ep.io/resource-example/
You can browse the API using a web browser, or from the command line:: You can browse the API using a web browser, or from the command line::
curl -X GET http://api.django-rest-framework.org/resource-example/ -H 'Accept: text/plain' curl -X GET http://rest.ep.io/resource-example/ -H 'Accept: text/plain'
We're going to start off with a simple example, that demonstrates a few things: We're going to start off with a simple example, that demonstrates a few things:
@ -43,16 +41,16 @@ Now we'll write our views. The first is a read only view that links to three in
That's us done. Our API now provides both programmatic access using JSON and XML, as well a nice browseable HTML view, so we can now access it both from the browser: That's us done. Our API now provides both programmatic access using JSON and XML, as well a nice browseable HTML view, so we can now access it both from the browser:
* http://api.django-rest-framework.org/resource-example/ * http://rest.ep.io/resource-example/
And from the command line: And from the command line:
.. code-block:: bash .. code-block:: bash
# Demonstrates API's input validation using form input # Demonstrates API's input validation using form input
bash: curl -X POST --data 'foo=true' http://api.django-rest-framework.org/resource-example/1/ bash: curl -X POST --data 'foo=true' http://rest.ep.io/resource-example/1/
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
# Demonstrates API's input validation using JSON input # Demonstrates API's input validation using JSON input
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://api.django-rest-framework.org/resource-example/1/ bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://rest.ep.io/resource-example/1/
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}

8
docs/howto.rst Normal file
View File

@ -0,0 +1,8 @@
How Tos, FAQs & Notes
=====================
.. toctree::
:maxdepth: 1
:glob:
howto/*

View File

@ -1,19 +1,18 @@
Using Django REST framework Mixin classes Using Django REST framework Mixin classes
========================================= =========================================
This example demonstrates creating a REST API **without** using Django REST framework's :class:`.Resource` or :class:`.ModelResource`, This example demonstrates creating a REST API **without** using Django REST framework's :class:`.Resource` or :class:`.ModelResource`, but instead using Django's :class:`View` class, and adding the :class:`ResponseMixin` class to provide full HTTP Accept header content negotiation,
but instead using Django :class:`View` class, and adding the :class:`EmitterMixin` class to provide full HTTP Accept header content negotiation,
a browseable Web API, and much of the other goodness that Django REST framework gives you for free. a browseable Web API, and much of the other goodness that Django REST framework gives you for free.
.. note:: .. note::
A live sandbox instance of this API is available for testing: A live sandbox instance of this API is available for testing:
* http://api.django-rest-framework.org/mixin/ * http://rest.ep.io/mixin/
You can browse the API using a web browser, or from the command line:: You can browse the API using a web browser, or from the command line::
curl -X GET http://api.django-rest-framework.org/mixin/ curl -X GET http://rest.ep.io/mixin/
URL configuration URL configuration
@ -26,5 +25,6 @@ Everything we need for this example can go straight into the URL conf...
.. include:: ../../examples/mixin/urls.py .. include:: ../../examples/mixin/urls.py
:literal: :literal:
That's it. Auto-magically our API now supports multiple output formats, specified either by using standard HTTP Accept header content negotiation, or by using the `&_accept=application/json` style parameter overrides. That's it. Auto-magically our API now supports multiple output formats, specified either by using
standard HTTP Accept header content negotiation, or by using the `&_accept=application/json` style parameter overrides.
We even get a nice HTML view which can be used to self-document our API. We even get a nice HTML view which can be used to self-document our API.

View File

@ -13,7 +13,7 @@ If you need to manually install Django REST framework to your ``site-packages``
Template Loaders Template Loaders
---------------- ----------------
Django REST framework uses a few templates for the HTML and plain text documenting emitters. Django REST framework uses a few templates for the HTML and plain text documenting renderers.
* Ensure ``TEMPLATE_LOADERS`` setting contains ``'django.template.loaders.app_directories.Loader'``. * Ensure ``TEMPLATE_LOADERS`` setting contains ``'django.template.loaders.app_directories.Loader'``.
@ -22,16 +22,20 @@ This will be the case by default so you shouldn't normally need to do anything h
Admin Styling Admin Styling
------------- -------------
Django REST framework uses the admin media for styling. When running using Django's testserver this is automatically served for you, but once you move onto a production server, you'll want to make sure you serve the admin media separately, exactly as you would do `if using the Django admin <http://docs.djangoproject.com/en/dev/howto/deployment/modwsgi/#serving-the-admin-files>`_. Django REST framework uses the admin media for styling. When running using Django's testserver this is automatically served for you,
but once you move onto a production server, you'll want to make sure you serve the admin media separately, exactly as you would do
`if using the Django admin <https://docs.djangoproject.com/en/dev/howto/deployment/modpython/#serving-the-admin-files>`_.
* Ensure that the ``ADMIN_MEDIA_PREFIX`` is set appropriately and that you are serving the admin media. (Django's testserver will automatically serve the admin media for you) * Ensure that the ``ADMIN_MEDIA_PREFIX`` is set appropriately and that you are serving the admin media.
(Django's testserver will automatically serve the admin media for you)
Markdown Markdown
-------- --------
The Python `markdown library <http://www.freewisdom.org/projects/python-markdown/>`_ is not required but comes recommended. The Python `markdown library <http://www.freewisdom.org/projects/python-markdown/>`_ is not required but comes recommended.
If markdown is installed your :class:`.Resource` descriptions can include `markdown style formatting <http://daringfireball.net/projects/markdown/syntax>`_ which will be rendered by the HTML documenting emitter. If markdown is installed your :class:`.Resource` descriptions can include `markdown style formatting
<http://daringfireball.net/projects/markdown/syntax>`_ which will be rendered by the HTML documenting renderer.
robots.txt, favicon, login/logout robots.txt, favicon, login/logout
--------------------------------- ---------------------------------

View File

@ -0,0 +1,39 @@
Using urllib2 with Django REST Framework
========================================
Python's standard library comes with some nice modules
you can use to test your api or even write a full client.
Using the 'GET' method
----------------------
Here's an example which does a 'GET' on the `model-resource` example
in the sandbox.::
>>> import urllib2
>>> r = urllib2.urlopen('htpp://rest.ep.io/model-resource-example')
>>> r.getcode() # Check if the response was ok
200
>>> print r.read() # Examin the response itself
[{"url": "http://rest.ep.io/model-resource-example/1/", "baz": "sdf", "foo": true, "bar": 123}]
Using the 'POST' method
-----------------------
And here's an example which does a 'POST' to create a new instance. First let's encode
the data we want to POST. We'll use `urllib` for encoding and the `time` module
to send the current time as as a string value for our POST.::
>>> import urllib, time
>>> d = urllib.urlencode((('bar', 123), ('baz', time.asctime())))
Now use the `Request` class and specify the 'Content-type'::
>>> req = urllib2.Request('http://rest.ep.io/model-resource-example/', data=d, headers={'Content-Type':'application/x-www-form-urlencoded'})
>>> resp = urllib2.urlopen(req)
>>> resp.getcode()
201
>>> resp.read()
'{"url": "http://rest.ep.io/model-resource-example/4/", "baz": "Fri Dec 30 18:22:52 2011", "foo": false, "bar": 123}'
That should get you started to write a client for your own api.

View File

@ -11,11 +11,12 @@ Introduction
Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs. Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs.
**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_ **Browse example APIs created with Django REST framework:** `The Sandbox <http://rest.ep.io/>`_
Features: Features:
---------
* Automatically provides an awesome Django admin style `browse-able self-documenting API <http://api.django-rest-framework.org>`_. * Automatically provides an awesome Django admin style `browse-able self-documenting API <http://rest.ep.io>`_.
* Clean, simple, views for Resources, using Django's new `class based views <http://docs.djangoproject.com/en/dev/topics/class-based-views/>`_. * 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. * Support for ModelResources with out-of-the-box default implementations and input validation.
* Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise. * Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise.
@ -39,18 +40,17 @@ Requirements
------------ ------------
* Python (2.5, 2.6, 2.7 supported) * Python (2.5, 2.6, 2.7 supported)
* Django (1.2, 1.3 supported) * Django (1.2, 1.3, 1.4-alpha supported)
Installation Installation
------------ ------------
You can install Django REST framework using ``pip`` or ``easy_install``:: You can install Django REST framework using ``pip`` or ``easy_install``::
pip install djangorestframework pip install djangorestframework
Or get the latest development version using mercurial or git:: Or get the latest development version using git::
git clone git@github.com:tomchristie/django-rest-framework.git git clone git@github.com:tomchristie/django-rest-framework.git
@ -71,6 +71,13 @@ Getting Started
Using Django REST framework can be as simple as adding a few lines to your urlconf. Using Django REST framework can be as simple as adding a few lines to your urlconf.
The following example exposes your `MyModel` model through an api. It will provide two views:
* A view which lists your model instances and simultaniously allows creation of instances
from that view.
* Another view which lets you view, update or delete your model instances individually.
``urls.py``:: ``urls.py``::
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
@ -86,70 +93,17 @@ Using Django REST framework can be as simple as adding a few lines to your urlco
url(r'^(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MyResource)), url(r'^(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MyResource)),
) )
Django REST framework comes with two "getting started" examples. .. include:: howto.rst
#. :ref:`views` .. include:: library.rst
#. :ref:`modelviews`
Examples
--------
There are a few real world web API examples included with Django REST framework.
#. :ref:`objectstore` - Using :class:`views.View` classes for APIs that do not map to models.
#. :ref:`codehighlighting` - Using :class:`views.View` classes with forms for input validation.
#. :ref:`blogposts` - Using :class:`views.ModelView` classes for APIs that map directly to models.
All the examples are freely available for testing in the sandbox:
http://api.django-rest-framework.org
(The :ref:`sandbox` resource is also documented.)
.. include:: examples.rst
How Tos, FAQs & Notes
---------------------
.. toctree:: .. toctree::
:maxdepth: 1 :hidden:
howto/setup contents
howto/usingcurl
howto/alternativeframeworks
howto/mixin
Library Reference
-----------------
.. toctree::
:maxdepth: 1
library/authentication
library/compat
library/mixins
library/parsers
library/permissions
library/renderers
library/resource
library/response
library/serializer
library/status
library/views
Examples Reference
------------------
.. toctree::
:maxdepth: 1
examples/views
examples/modelviews
examples/objectstore
examples/pygments
examples/blogpost
examples/sandbox
howto/mixin
Indices and tables Indices and tables
------------------ ------------------

8
docs/library.rst Normal file
View File

@ -0,0 +1,8 @@
Library
=======
.. toctree::
:maxdepth: 1
:glob:
library/*

View File

@ -24,3 +24,5 @@
</script> </script>
{% endblock %} {% endblock %}
{% block footer %}
<div class="footer"> <p> Documentation version {{ version }} {% endblock %}</p></div>

1
examples/.epio-app Normal file
View File

@ -0,0 +1 @@
rest

62
examples/epio.ini Normal file
View File

@ -0,0 +1,62 @@
# This is an example epio.ini file.
# We suggest you edit it to fit your application's needs.
# Documentation for the options is available at www.ep.io/docs/epioini/
[wsgi]
# Location of your requirements file
requirements = requirements-epio.txt
[static]
# Serve the static directory directly as /static
/static/admin = ../shortcuts/django-admin-media/
[services]
# Uncomment to enable the PostgreSQL service.
postgres = true
# Uncomment to enable the Redis service
# redis = true
[checkout]
# By default your code is put in a directory called 'app'.
# You can change that here.
# directory_name = my_project
[env]
# Set any additional environment variables here. For example:
# IN_PRODUCTION = true
[symlinks]
# Any symlinks you'd like to add. As an example, link the symlink 'config.py'
# to the real file 'configs/epio.py':
# config.py = configs/epio.py
media/ = %(data_directory)s/
# #### If you're using Django, you'll want to uncomment some or all of these lines ####
# [django]
# # Path to your project root, relative to this directory.
# base = .
#
# [static]
# Serve the admin media
# # Django 1.3
# /static/admin = ../shortcuts/django-admin-media/
# # Django 1.2 and below
# /media = ../shortcuts/django-admin-media/
#
# [env]
# # Use a different settings module for ep.io (i.e. with DEBUG=False)
# DJANGO_SETTINGS_MODULE = production_settings

View File

@ -13,6 +13,9 @@ import operator
OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore') OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
MAX_FILES = 10 MAX_FILES = 10
if not os.path.exists(OBJECT_STORE_DIR):
os.makedirs(OBJECT_STORE_DIR)
def remove_oldest_files(dir, max_files): def remove_oldest_files(dir, max_files):
""" """

View File

@ -31,8 +31,11 @@ class ThrottlingExampleView(View):
class LoggedInExampleView(View): class LoggedInExampleView(View):
""" """
You can login with **'test', 'test'.** You can login with **'test', 'test'.** or use curl:
`curl -X GET -H 'Accept: application/json' -u test:test http://localhost:8000/permissions-example`
""" """
permissions = (IsAuthenticated, ) permissions = (IsAuthenticated, )
def get(self, request): def get(self, request):
return 'Logged in or not?' return 'Logged in or not?'

View File

@ -22,6 +22,9 @@ import operator
HIGHLIGHTED_CODE_DIR = os.path.join(settings.MEDIA_ROOT, 'pygments') HIGHLIGHTED_CODE_DIR = os.path.join(settings.MEDIA_ROOT, 'pygments')
MAX_FILES = 10 MAX_FILES = 10
if not os.path.exists(HIGHLIGHTED_CODE_DIR):
os.makedirs(HIGHLIGHTED_CODE_DIR)
def list_dir_sorted_by_ctime(dir): def list_dir_sorted_by_ctime(dir):
""" """

View File

@ -0,0 +1,3 @@
Pygments==1.4
Markdown==2.0.3
djangorestframework

View File

@ -1,4 +1,5 @@
# Settings for djangorestframework examples project # Settings for djangorestframework examples project
import os
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
@ -46,7 +47,7 @@ USE_L10N = True
# Absolute filesystem path to the directory that will hold user-uploaded files. # Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/" # Example: "/home/media/media.lawrence.com/"
# NOTE: Some of the djangorestframework examples use MEDIA_ROOT to store content. # NOTE: Some of the djangorestframework examples use MEDIA_ROOT to store content.
MEDIA_ROOT = 'media/' MEDIA_ROOT = os.path.join(os.getenv('EPIO_DATA_DIRECTORY', '.'), 'media')
# URL that handles the media served from MEDIA_ROOT. Make sure to use a # URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases). # trailing slash if there is a path component (optional in other cases).
@ -61,7 +62,7 @@ MEDIA_URL = ''
# but it does require the admin media be served. Django's test server will do # but it does require the admin media be served. Django's test server will do
# this for you automatically, but in production you'll want to make sure you # this for you automatically, but in production you'll want to make sure you
# serve the admin media from somewhere. # serve the admin media from somewhere.
ADMIN_MEDIA_PREFIX = '/admin-media/' ADMIN_MEDIA_PREFIX = '/static/admin'
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu' SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'

View File

@ -1,6 +1,5 @@
# We need Django. Duh. # We need Django. Duh.
# coverage isn't strictly a requirement, but it's useful. # coverage isn't strictly a requirement, but it's useful.
Django==1.2.4 Django>=1.2
wsgiref==0.1.2 coverage>=3.4
coverage==3.4

127
tox.ini
View File

@ -9,13 +9,18 @@ envlist=
py25-django13, py25-django13,
py26-django13, py26-django13,
py27-django13, py27-django13,
py25-django14a1,
py25-django12e, py26-django14a1,
py26-django12e, py27-django14a1,
py27-django12e, py25-django12-examples,
py25-django13e, py26-django12-examples,
py26-django13e, py27-django12-examples,
py27-django13e py25-django13-examples,
py26-django13-examples,
py27-django13-examples,
py25-django14a1-examples,
py26-django14a1-examples,
py27-django14a1-examples
########################################### CORE TESTS ############################################ ########################################### CORE TESTS ############################################
@ -30,6 +35,8 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py26-django12] [testenv:py26-django12]
basepython=python2.6 basepython=python2.6
@ -38,6 +45,8 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py27-django12] [testenv:py27-django12]
basepython=python2.7 basepython=python2.7
@ -46,6 +55,8 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py25-django13] [testenv:py25-django13]
basepython=python2.5 basepython=python2.5
@ -54,6 +65,8 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py26-django13] [testenv:py26-django13]
basepython=python2.6 basepython=python2.6
@ -62,6 +75,8 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py27-django13] [testenv:py27-django13]
basepython=python2.7 basepython=python2.7
@ -70,10 +85,42 @@ deps=
coverage==3.4 coverage==3.4
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
# Optional packages:
markdown
[testenv:py25-django14a1]
basepython=python2.5
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
unittest-xml-reporting==1.2
Pyyaml==3.10
# Optional packages:
markdown
[testenv:py26-django14a1]
basepython=python2.6
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
unittest-xml-reporting==1.2
Pyyaml==3.10
# Optional packages:
markdown
[testenv:py27-django14a1]
basepython=python2.7
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
unittest-xml-reporting==1.2
Pyyaml==3.10
# Optional packages:
markdown
####################################### EXAMPLES ################################################ ####################################### EXAMPLES ################################################
[testenv:py25-django12e] [testenv:py25-django12-examples]
basepython=python2.5 basepython=python2.5
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -87,7 +134,7 @@ deps=
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py26-django12e] [testenv:py26-django12-examples]
basepython=python2.6 basepython=python2.6
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -101,7 +148,7 @@ deps=
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py27-django12e] [testenv:py27-django12-examples]
basepython=python2.7 basepython=python2.7
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -115,7 +162,7 @@ deps=
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py25-django13e] [testenv:py25-django13-examples]
basepython=python2.5 basepython=python2.5
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -129,7 +176,7 @@ deps=
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py26-django13e] [testenv:py26-django13-examples]
basepython=python2.6 basepython=python2.6
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -143,7 +190,7 @@ deps=
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py27-django13e] [testenv:py27-django13-examples]
basepython=python2.7 basepython=python2.7
commands= commands=
python examples/runtests.py python examples/runtests.py
@ -156,3 +203,57 @@ deps=
Markdown==2.0.3 Markdown==2.0.3
unittest-xml-reporting==1.2 unittest-xml-reporting==1.2
Pyyaml==3.10 Pyyaml==3.10
[testenv:py25-django14a1-examples]
basepython=python2.5
commands=
python examples/runtests.py
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
wsgiref==0.1.2
Pygments==1.4
httplib2==0.6.0
Markdown==2.0.3
unittest-xml-reporting==1.2
Pyyaml==3.10
[testenv:py26-django14a1-examples]
basepython=python2.6
commands=
python examples/runtests.py
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
wsgiref==0.1.2
Pygments==1.4
httplib2==0.6.0
Markdown==2.0.3
unittest-xml-reporting==1.2
Pyyaml==3.10
[testenv:py27-django14a1-examples]
basepython=python2.7
commands=
python examples/runtests.py
deps=
http://www.djangoproject.com/download/1.4-alpha-1/tarball/
coverage==3.4
wsgiref==0.1.2
Pygments==1.4
httplib2==0.6.0
Markdown==2.0.3
unittest-xml-reporting==1.2
Pyyaml==3.10
##########################################DOCS#################################################
[testenv:docs]
basepython=python
changedir=docs
deps=
sphinx
pytest
django==1.3
commands=
py.test --tb=line -v --junitxml=junit-{envname}.xml check_sphinx.py