mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-28 04:24:00 +03:00
merged master into experimental
This commit is contained in:
commit
59e6cd9892
3
AUTHORS
3
AUTHORS
|
@ -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
21
LICENSE
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
5
RELEASES
5
RELEASES
|
@ -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.
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '0.2.4'
|
__version__ = '0.3.1-dev'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -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,19 +100,27 @@ 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 hasattr(self.view.DATA, 'get'):
|
||||||
|
request._post = self.view.DATA
|
||||||
|
else:
|
||||||
|
request._post = {}
|
||||||
|
|
||||||
|
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||||
|
|
||||||
|
# Replace request.POST
|
||||||
if request.method.upper() == 'POST':
|
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)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,49 @@
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
# From: http://djangosnippets.org/snippets/963/
|
# From: http://djangosnippets.org/snippets/963/
|
||||||
# Lovely stuff
|
# Lovely stuff
|
||||||
class RequestFactory(Client):
|
class RequestFactory(Client):
|
||||||
"""
|
"""
|
||||||
Class that lets you create mock :obj:`Request` objects for use in testing.
|
Class that lets you create mock :obj:`Request` objects for use in testing.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
get_request = rf.get('/hello/')
|
get_request = rf.get('/hello/')
|
||||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||||
|
|
||||||
This class re-uses the :class:`django.test.client.Client` interface. Of which
|
This class re-uses the :class:`django.test.client.Client` interface. Of which
|
||||||
you can find the docs here__.
|
you can find the docs here__.
|
||||||
|
|
||||||
__ http://www.djangoproject.com/documentation/testing/#the-test-client
|
__ http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||||
|
|
||||||
Once you have a `request` object you can pass it to any :func:`view` function,
|
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.
|
just as if that :func:`view` had been hooked up using a URLconf.
|
||||||
"""
|
"""
|
||||||
def request(self, **request):
|
def request(self, **request):
|
||||||
|
@ -68,30 +69,30 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
if not hasattr(View, 'head'):
|
if not hasattr(View, 'head'):
|
||||||
# First implementation of Django class-based views did not include head method
|
# First implementation of Django class-based views did not include head method
|
||||||
# in base View class - https://code.djangoproject.com/ticket/15668
|
# in base View class - https://code.djangoproject.com/ticket/15668
|
||||||
class ViewPlusHead(View):
|
class ViewPlusHead(View):
|
||||||
def head(self, request, *args, **kwargs):
|
def head(self, request, *args, **kwargs):
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
View = ViewPlusHead
|
View = ViewPlusHead
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from django import http
|
from django import http
|
||||||
from django.utils.functional import update_wrapper
|
from django.utils.functional import update_wrapper
|
||||||
# from django.utils.log import getLogger
|
# from django.utils.log import getLogger
|
||||||
# from django.utils.decorators import classonlymethod
|
# from django.utils.decorators import classonlymethod
|
||||||
|
|
||||||
# logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
|
# logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
|
||||||
# Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely
|
# Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely
|
||||||
|
|
||||||
class View(object):
|
class View(object):
|
||||||
"""
|
"""
|
||||||
Intentionally simple parent class for all views. Only implements
|
Intentionally simple parent class for all views. Only implements
|
||||||
dispatch-by-method and simple sanity checking.
|
dispatch-by-method and simple sanity checking.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
|
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Constructor. Called in the URLconf; can contain helpful extra
|
Constructor. Called in the URLconf; can contain helpful extra
|
||||||
|
@ -101,7 +102,7 @@ except ImportError:
|
||||||
# instance, or raise an error.
|
# instance, or raise an error.
|
||||||
for key, value in kwargs.iteritems():
|
for key, value in kwargs.iteritems():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
|
# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
|
@ -117,19 +118,19 @@ except ImportError:
|
||||||
if not hasattr(cls, key):
|
if not hasattr(cls, key):
|
||||||
raise TypeError(u"%s() received an invalid keyword %r" % (
|
raise TypeError(u"%s() received an invalid keyword %r" % (
|
||||||
cls.__name__, key))
|
cls.__name__, key))
|
||||||
|
|
||||||
def view(request, *args, **kwargs):
|
def view(request, *args, **kwargs):
|
||||||
self = cls(**initkwargs)
|
self = cls(**initkwargs)
|
||||||
return self.dispatch(request, *args, **kwargs)
|
return self.dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
# take name and docstring from class
|
# take name and docstring from class
|
||||||
update_wrapper(view, cls, updated=())
|
update_wrapper(view, cls, updated=())
|
||||||
|
|
||||||
# and possible attributes set by decorators
|
# and possible attributes set by decorators
|
||||||
# like csrf_exempt from dispatch
|
# like csrf_exempt from dispatch
|
||||||
update_wrapper(view, cls.dispatch, assigned=())
|
update_wrapper(view, cls.dispatch, assigned=())
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# Try to dispatch to the right method; if a method doesn't exist,
|
# Try to dispatch to the right method; if a method doesn't exist,
|
||||||
# defer to the error handler. Also defer to the error handler if the
|
# defer to the error handler. Also defer to the error handler if the
|
||||||
|
@ -142,7 +143,7 @@ except ImportError:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
|
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
|
||||||
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
|
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
|
||||||
|
@ -156,24 +157,239 @@ 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)
|
||||||
|
|
||||||
def test(self, parent, block):
|
def test(self, parent, block):
|
||||||
return bool(self.RE.match(block))
|
return bool(self.RE.match(block))
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
def run(self, parent, blocks):
|
||||||
lines = blocks.pop(0).split('\n')
|
lines = blocks.pop(0).split('\n')
|
||||||
# Determine level. ``=`` is 1 and ``-`` is 2.
|
# Determine level. ``=`` is 1 and ``-`` is 2.
|
||||||
|
@ -186,21 +402,25 @@ try:
|
||||||
if len(lines) > 2:
|
if len(lines) > 2:
|
||||||
# Block contains additional lines. Add to master blocks for later.
|
# Block contains additional lines. Add to master blocks for later.
|
||||||
blocks.insert(0, '\n'.join(lines[2:]))
|
blocks.insert(0, '\n'.join(lines[2:]))
|
||||||
|
|
||||||
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,
|
||||||
output_format = markdown.DEFAULT_OUTPUT_FORMAT
|
|
||||||
|
|
||||||
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
|
if markdown.version_info < (2, 1):
|
||||||
safe_mode=safe_mode,
|
output_format = markdown.DEFAULT_OUTPUT_FORMAT
|
||||||
|
|
||||||
|
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -49,15 +49,15 @@ class BaseParser(object):
|
||||||
in case the parser needs to access any metadata on the :obj:`View` object.
|
in case the parser needs to access any metadata on the :obj:`View` object.
|
||||||
"""
|
"""
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def can_handle_request(self, content_type):
|
def can_handle_request(self, content_type):
|
||||||
"""
|
"""
|
||||||
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
||||||
|
|
||||||
The default implementation for this function is to check the *content_type*
|
The default implementation for this function is to check the *content_type*
|
||||||
argument against the :attr:`media_type` attribute set on the class to see if
|
argument against the :attr:`media_type` attribute set on the class to see if
|
||||||
they match.
|
they match.
|
||||||
|
|
||||||
This may be overridden to provide for other behavior, but typically you'll
|
This may be overridden to provide for other behavior, but typically you'll
|
||||||
instead want to just set the :attr:`media_type` attribute on the class.
|
instead want to just set the :attr:`media_type` attribute on the class.
|
||||||
"""
|
"""
|
||||||
|
@ -97,13 +97,13 @@ if yaml:
|
||||||
"""
|
"""
|
||||||
Parses YAML-serialized data.
|
Parses YAML-serialized data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/yaml'
|
media_type = 'application/yaml'
|
||||||
|
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be an object which is the parsed content of the response.
|
`data` will be an object which is the parsed content of the response.
|
||||||
`files` will always be `None`.
|
`files` will always be `None`.
|
||||||
"""
|
"""
|
||||||
|
@ -125,7 +125,7 @@ class PlainTextParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will simply be a string representing the body of the request.
|
`data` will simply be a string representing the body of the request.
|
||||||
`files` will always be `None`.
|
`files` will always be `None`.
|
||||||
"""
|
"""
|
||||||
|
@ -142,7 +142,7 @@ class FormParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||||
`files` will always be :const:`None`.
|
`files` will always be :const:`None`.
|
||||||
"""
|
"""
|
||||||
|
@ -160,7 +160,7 @@ class MultiPartParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||||
`files` will be a :class:`QueryDict` containing all the form files.
|
`files` will be a :class:`QueryDict` containing all the form files.
|
||||||
"""
|
"""
|
||||||
|
@ -194,9 +194,9 @@ class XMLParser(BaseParser):
|
||||||
|
|
||||||
return (data, None)
|
return (data, None)
|
||||||
|
|
||||||
def _type_convert(self, value):
|
def _type_convert(self, value):
|
||||||
"""
|
"""
|
||||||
Converts the value returned by the XMl parse into the equivalent
|
Converts the value returned by the XMl parse into the equivalent
|
||||||
Python type
|
Python type
|
||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -227,4 +227,4 @@ DEFAULT_PARSERS = ( JSONParser,
|
||||||
)
|
)
|
||||||
|
|
||||||
if YAMLParser:
|
if YAMLParser:
|
||||||
DEFAULT_PARSERS += ( YAMLParser, )
|
DEFAULT_PARSERS += ( YAMLParser, )
|
||||||
|
|
|
@ -3,7 +3,7 @@ Renderers are used to serialize a View's output into specific media types.
|
||||||
|
|
||||||
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
|
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
|
||||||
by serializing the output along with documentation regarding the View, output status and headers,
|
by serializing the output along with documentation regarding the View, output status and headers,
|
||||||
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||||
"""
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -26,6 +26,7 @@ __all__ = (
|
||||||
'BaseRenderer',
|
'BaseRenderer',
|
||||||
'TemplateRenderer',
|
'TemplateRenderer',
|
||||||
'JSONRenderer',
|
'JSONRenderer',
|
||||||
|
'JSONPRenderer',
|
||||||
'DocumentingHTMLRenderer',
|
'DocumentingHTMLRenderer',
|
||||||
'DocumentingXHTMLRenderer',
|
'DocumentingXHTMLRenderer',
|
||||||
'DocumentingPlainTextRenderer',
|
'DocumentingPlainTextRenderer',
|
||||||
|
@ -39,7 +40,7 @@ class BaseRenderer(object):
|
||||||
All renderers must extend this class, set the :attr:`media_type` attribute,
|
All renderers must extend this class, set the :attr:`media_type` attribute,
|
||||||
and override the :meth:`render` method.
|
and override the :meth:`render` method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_FORMAT_QUERY_PARAM = 'format'
|
_FORMAT_QUERY_PARAM = 'format'
|
||||||
|
|
||||||
media_type = None
|
media_type = None
|
||||||
|
@ -81,7 +82,7 @@ class BaseRenderer(object):
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -135,10 +158,10 @@ if yaml:
|
||||||
"""
|
"""
|
||||||
Renderer which serializes to YAML.
|
Renderer which serializes to YAML.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_type = 'application/yaml'
|
media_type = 'application/yaml'
|
||||||
format = 'yaml'
|
format = 'yaml'
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
Renders *obj* into serialized YAML.
|
Renders *obj* into serialized YAML.
|
||||||
|
@ -200,7 +223,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
content = renderers[0](view).render(obj, media_type)
|
content = renderers[0](view).render(obj, media_type)
|
||||||
if not all(char in string.printable for char in content):
|
if not all(char in string.printable for char in content):
|
||||||
return '[%d bytes of binary content]'
|
return '[%d bytes of binary content]'
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
@ -236,7 +259,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
|
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
|
||||||
if not form_instance:
|
if not form_instance:
|
||||||
form_instance = self._get_generic_content_form(view)
|
form_instance = self._get_generic_content_form(view)
|
||||||
|
|
||||||
return form_instance
|
return form_instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -326,9 +349,9 @@ 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)
|
||||||
|
|
||||||
# Munge DELETE Response code to allow us to return content
|
# Munge DELETE Response code to allow us to return content
|
||||||
|
@ -376,6 +399,7 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_RENDERERS = ( JSONRenderer,
|
DEFAULT_RENDERERS = ( JSONRenderer,
|
||||||
|
JSONPRenderer,
|
||||||
DocumentingHTMLRenderer,
|
DocumentingHTMLRenderer,
|
||||||
DocumentingXHTMLRenderer,
|
DocumentingXHTMLRenderer,
|
||||||
DocumentingPlainTextRenderer,
|
DocumentingPlainTextRenderer,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`response` module provides Response classes you can use in your
|
The :mod:`response` module provides Response classes you can use in your
|
||||||
views to return a certain HTTP response. Typically a response is *rendered*
|
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
|
into a HTTP response depending on what renderers are set on your view and
|
||||||
als depending on the accept header of the request.
|
als depending on the accept header of the request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
@ -23,7 +23,7 @@ class Response(object):
|
||||||
self.raw_content = content # content prior to filtering
|
self.raw_content = content # content prior to filtering
|
||||||
self.cleaned_content = content # content after filtering
|
self.cleaned_content = content # content after filtering
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -51,10 +51,10 @@ def main():
|
||||||
|
|
||||||
# Drop the compat module from coverage, since we're not interested in the coverage
|
# Drop the compat module from coverage, since we're not interested in the coverage
|
||||||
# of a module which is specifically for resolving environment dependant imports.
|
# of a module which is specifically for resolving environment dependant imports.
|
||||||
# (Because we'll end up getting different coverage reports for it for each environment)
|
# (Because we'll end up getting different coverage reports for it for each environment)
|
||||||
if 'compat.py' in files:
|
if 'compat.py' in files:
|
||||||
files.remove('compat.py')
|
files.remove('compat.py')
|
||||||
|
|
||||||
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
||||||
|
|
||||||
cov.report(cov_files)
|
cov.report(cov_files)
|
||||||
|
|
|
@ -16,12 +16,12 @@ from django.test.utils import get_runner
|
||||||
def usage():
|
def usage():
|
||||||
return """
|
return """
|
||||||
Usage: python runtests.py [UnitTestClass].[method]
|
Usage: python runtests.py [UnitTestClass].[method]
|
||||||
|
|
||||||
You can pass the Class name of the `UnitTestClass` you want to test.
|
You can pass the Class name of the `UnitTestClass` you want to test.
|
||||||
|
|
||||||
Append a method name if you only want to test a specific method of that class.
|
Append a method name if you only want to test a specific method of that class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
TestRunner = get_runner(settings)
|
TestRunner = get_runner(settings)
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ MEDIA_URL = ''
|
||||||
|
|
||||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||||
# trailing slash.
|
# trailing slash.
|
||||||
# Examples: "http://foo.com/media/", "/media/".
|
# Examples: "http://foo.com/media/", "/media/".
|
||||||
ADMIN_MEDIA_PREFIX = '/media/'
|
ADMIN_MEDIA_PREFIX = '/media/'
|
||||||
|
|
||||||
# Make this unique, and don't share it with anybody.
|
# Make this unique, and don't share it with anybody.
|
||||||
|
@ -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.
|
||||||
|
|
|
@ -4,4 +4,4 @@ Blank URLConf just to keep runtests.py happy.
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,7 @@ _serializers = {}
|
||||||
|
|
||||||
def _field_to_tuple(field):
|
def _field_to_tuple(field):
|
||||||
"""
|
"""
|
||||||
Convert an item in the `fields` attribute into a 2-tuple.
|
Convert an item in the `fields` attribute into a 2-tuple.
|
||||||
"""
|
"""
|
||||||
if isinstance(field, (tuple, list)):
|
if isinstance(field, (tuple, list)):
|
||||||
return (field[0], field[1])
|
return (field[0], field[1])
|
||||||
|
@ -52,7 +52,7 @@ class _RegisterSerializer(type):
|
||||||
"""
|
"""
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
# Build the class and register it.
|
# Build the class and register it.
|
||||||
ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
|
ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
|
||||||
_serializers[name] = ret
|
_serializers[name] = ret
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -61,19 +61,19 @@ class Serializer(object):
|
||||||
"""
|
"""
|
||||||
Converts python objects into plain old native types suitable for
|
Converts python objects into plain old native types suitable for
|
||||||
serialization. In particular it handles models and querysets.
|
serialization. In particular it handles models and querysets.
|
||||||
|
|
||||||
The output format is specified by setting a number of attributes
|
The output format is specified by setting a number of attributes
|
||||||
on the class.
|
on the class.
|
||||||
|
|
||||||
You may also override any of the serialization methods, to provide
|
You may also override any of the serialization methods, to provide
|
||||||
for more flexible behavior.
|
for more flexible behavior.
|
||||||
|
|
||||||
Valid output types include anything that may be directly rendered into
|
Valid output types include anything that may be directly rendered into
|
||||||
json, xml etc...
|
json, xml etc...
|
||||||
"""
|
"""
|
||||||
__metaclass__ = _RegisterSerializer
|
__metaclass__ = _RegisterSerializer
|
||||||
|
|
||||||
fields = ()
|
fields = ()
|
||||||
"""
|
"""
|
||||||
Specify the fields to be serialized on a model or dict.
|
Specify the fields to be serialized on a model or dict.
|
||||||
Overrides `include` and `exclude`.
|
Overrides `include` and `exclude`.
|
||||||
|
@ -109,7 +109,7 @@ class Serializer(object):
|
||||||
if depth is not None:
|
if depth is not None:
|
||||||
self.depth = depth
|
self.depth = depth
|
||||||
self.stack = stack
|
self.stack = stack
|
||||||
|
|
||||||
|
|
||||||
def get_fields(self, obj):
|
def get_fields(self, obj):
|
||||||
"""
|
"""
|
||||||
|
@ -168,7 +168,7 @@ class Serializer(object):
|
||||||
# Similar to what Django does for cyclically related models.
|
# Similar to what Django does for cyclically related models.
|
||||||
elif isinstance(info, str) and info in _serializers:
|
elif isinstance(info, str) and info in _serializers:
|
||||||
return _serializers[info]
|
return _serializers[info]
|
||||||
|
|
||||||
# Otherwise use `related_serializer` or fall back to `Serializer`
|
# Otherwise use `related_serializer` or fall back to `Serializer`
|
||||||
return getattr(self, 'related_serializer') or Serializer
|
return getattr(self, 'related_serializer') or Serializer
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ class Serializer(object):
|
||||||
Convert a model field or dict value into a serializable representation.
|
Convert a model field or dict value into a serializable representation.
|
||||||
"""
|
"""
|
||||||
related_serializer = self.get_related_serializer(key)
|
related_serializer = self.get_related_serializer(key)
|
||||||
|
|
||||||
if self.depth is None:
|
if self.depth is None:
|
||||||
depth = None
|
depth = None
|
||||||
elif self.depth <= 0:
|
elif self.depth <= 0:
|
||||||
|
@ -227,7 +227,7 @@ class Serializer(object):
|
||||||
|
|
||||||
fields = self.get_fields(instance)
|
fields = self.get_fields(instance)
|
||||||
|
|
||||||
# serialize each required field
|
# serialize each required field
|
||||||
for fname in fields:
|
for fname in fields:
|
||||||
try:
|
try:
|
||||||
if hasattr(self, smart_str(fname)):
|
if hasattr(self, smart_str(fname)):
|
||||||
|
@ -279,13 +279,13 @@ class Serializer(object):
|
||||||
Convert any unhandled object into a serializable representation.
|
Convert any unhandled object into a serializable representation.
|
||||||
"""
|
"""
|
||||||
return smart_unicode(obj, strings_only=True)
|
return smart_unicode(obj, strings_only=True)
|
||||||
|
|
||||||
|
|
||||||
def serialize(self, obj):
|
def serialize(self, obj):
|
||||||
"""
|
"""
|
||||||
Convert any object into a serializable representation.
|
Convert any object into a serializable representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(obj, (dict, models.Model)):
|
if isinstance(obj, (dict, models.Model)):
|
||||||
# Model instances & dictionaries
|
# Model instances & dictionaries
|
||||||
return self.serialize_model(obj)
|
return self.serialize_model(obj)
|
||||||
|
|
|
@ -89,4 +89,4 @@ NOT_IMPLEMENTED = 501
|
||||||
BAD_GATEWAY = 502
|
BAD_GATEWAY = 502
|
||||||
SERVICE_UNAVAILABLE = 503
|
SERVICE_UNAVAILABLE = 503
|
||||||
GATEWAY_TIMEOUT = 504
|
GATEWAY_TIMEOUT = 504
|
||||||
HTTP_VERSION_NOT_SUPPORTED = 505
|
HTTP_VERSION_NOT_SUPPORTED = 505
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Adds the custom filter 'urlize_quoted_links'
|
"""Adds the custom filter 'urlize_quoted_links'
|
||||||
|
|
||||||
This is identical to the built-in filter 'urlize' with the exception that
|
This is identical to the built-in filter 'urlize' with the exception that
|
||||||
single and double quotes are permitted as leading or trailing punctuation.
|
single and double quotes are permitted as leading or trailing punctuation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class UserAgentMungingTest(TestCase):
|
||||||
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||||
resp = view(req)
|
resp = view(req)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
def test_dont_munge_nice_browsers_accept_header(self):
|
def test_dont_munge_nice_browsers_accept_header(self):
|
||||||
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
|
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
|
||||||
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
|
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
|
||||||
|
|
|
@ -31,7 +31,7 @@ class BasicAuthTests(TestCase):
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
def test_post_form_passing_basic_auth(self):
|
def test_post_form_passing_basic_auth(self):
|
||||||
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
|
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
|
||||||
|
@ -66,7 +66,7 @@ class SessionAuthTests(TestCase):
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.csrf_client.logout()
|
self.csrf_client.logout()
|
||||||
|
|
|
@ -64,4 +64,4 @@ class BreadcrumbTests(TestCase):
|
||||||
|
|
||||||
def test_broken_url_breadcrumbs_handled_gracefully(self):
|
def test_broken_url_breadcrumbs_handled_gracefully(self):
|
||||||
url = '/foobar'
|
url = '/foobar'
|
||||||
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
|
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -55,16 +69,16 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
* list
|
* list
|
||||||
* list
|
* list
|
||||||
|
|
||||||
another header
|
another header
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
code block
|
code block
|
||||||
|
|
||||||
indented
|
indented
|
||||||
|
|
||||||
# hash style header #"""
|
# hash style header #"""
|
||||||
|
|
||||||
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
||||||
|
|
||||||
# This has been turned off now
|
# This has been turned off now
|
||||||
|
@ -75,7 +89,7 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
# """docstring"""
|
# """docstring"""
|
||||||
# description = example
|
# description = example
|
||||||
# self.assertEquals(get_description(MockView()), example)
|
# self.assertEquals(get_description(MockView()), example)
|
||||||
|
|
||||||
#def test_resource_description_does_not_require_docstring(self):
|
#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."""
|
# """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'
|
# example = 'Some other description'
|
||||||
|
@ -88,8 +102,10 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_description(MockView()), '')
|
self.assertEquals(get_description(MockView()), '')
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -22,7 +22,7 @@ class UploadFilesTests(TestCase):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return {'FILE_NAME': self.CONTENT['file'].name,
|
return {'FILE_NAME': self.CONTENT['file'].name,
|
||||||
'FILE_CONTENT': self.CONTENT['file'].read()}
|
'FILE_CONTENT': self.CONTENT['file'].read()}
|
||||||
|
|
||||||
file = StringIO.StringIO('stuff')
|
file = StringIO.StringIO('stuff')
|
||||||
file.name = 'stuff.txt'
|
file.name = 'stuff.txt'
|
||||||
request = self.factory.post('/', {'file': file})
|
request = self.factory.post('/', {'file': file})
|
||||||
|
|
|
@ -3,7 +3,7 @@ from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.mixins import RequestMixin
|
from djangorestframework.mixins import RequestMixin
|
||||||
|
|
||||||
|
|
||||||
class TestMethodOverloading(TestCase):
|
class TestMethodOverloading(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class TestMethodOverloading(TestCase):
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.request = self.req.post('/')
|
view.request = self.req.post('/')
|
||||||
self.assertEqual(view.method, 'POST')
|
self.assertEqual(view.method, 'POST')
|
||||||
|
|
||||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||||
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -25,4 +25,4 @@ class UserGroupMap(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('user_group_map', (), {
|
return ('user_group_map', (), {
|
||||||
'pk': self.id
|
'pk': self.id
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
@ -17,9 +18,9 @@ class UserForm(ModelForm):
|
||||||
class UserResource(ModelResource):
|
class UserResource(ModelResource):
|
||||||
model = User
|
model = User
|
||||||
form = UserForm
|
form = UserForm
|
||||||
|
|
||||||
class CustomUserResource(ModelResource):
|
class CustomUserResource(ModelResource):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
||||||
|
@ -31,9 +32,9 @@ 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'
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
"""Ensure that a model object can be created"""
|
"""Ensure that a model object can be created"""
|
||||||
|
@ -52,18 +53,18 @@ class ModelViewTests(TestCase):
|
||||||
self.assertEqual(0, User.objects.count())
|
self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, User.objects.count())
|
self.assertEqual(1, User.objects.count())
|
||||||
|
|
||||||
user = User.objects.all()[0]
|
user = User.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
self.assertEqual('bar', user.username)
|
||||||
self.assertEqual('baz', user.password)
|
self.assertEqual('baz', user.password)
|
||||||
self.assertEqual(1, user.groups.count())
|
self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
self.assertEqual('foo', group.name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
"""
|
||||||
Ensure that a model object with a m2m relation can be created where that
|
Ensure that a model object with a m2m relation can be created where that
|
||||||
|
@ -74,13 +75,13 @@ class ModelViewTests(TestCase):
|
||||||
self.assertEqual(0, User.objects.count())
|
self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, CustomUser.objects.count())
|
self.assertEqual(1, CustomUser.objects.count())
|
||||||
|
|
||||||
user = CustomUser.objects.all()[0]
|
user = CustomUser.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
self.assertEqual('bar', user.username)
|
||||||
self.assertEqual(1, user.groups.count())
|
self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
self.assertEqual('foo', group.name)
|
||||||
|
|
|
@ -23,14 +23,14 @@ else:
|
||||||
class ClientView(View):
|
class ClientView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return {'resource': 'Protected!'}
|
return {'resource': 'Protected!'}
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', oauth_required(ClientView.as_view())),
|
url(r'^$', oauth_required(ClientView.as_view())),
|
||||||
url(r'^oauth/', include('oauth_provider.urls')),
|
url(r'^oauth/', include('oauth_provider.urls')),
|
||||||
url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'),
|
url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OAuthTests(TestCase):
|
class OAuthTests(TestCase):
|
||||||
"""
|
"""
|
||||||
OAuth authentication:
|
OAuth authentication:
|
||||||
|
@ -42,23 +42,23 @@ else:
|
||||||
* the third-party website is able to retrieve data from the API
|
* the third-party website is able to retrieve data from the API
|
||||||
"""
|
"""
|
||||||
urls = 'djangorestframework.tests.oauthentication'
|
urls = 'djangorestframework.tests.oauthentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
# OAuth requirements
|
# OAuth requirements
|
||||||
self.resource = Resource(name='data', url='/')
|
self.resource = Resource(name='data', url='/')
|
||||||
self.resource.save()
|
self.resource.save()
|
||||||
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
|
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
|
||||||
self.CONSUMER_SECRET = 'kd94hf93k423kf44'
|
self.CONSUMER_SECRET = 'kd94hf93k423kf44'
|
||||||
self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
|
self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
|
||||||
name='api.example.com', user=self.user)
|
name='api.example.com', user=self.user)
|
||||||
self.consumer.save()
|
self.consumer.save()
|
||||||
|
|
||||||
def test_oauth_invalid_and_anonymous_access(self):
|
def test_oauth_invalid_and_anonymous_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that the resource is protected and the OAuth authorization view
|
Verify that the resource is protected and the OAuth authorization view
|
||||||
|
@ -69,16 +69,16 @@ else:
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
response = self.client.get('/oauth/authorize/', follow=True)
|
response = self.client.get('/oauth/authorize/', follow=True)
|
||||||
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
|
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
|
||||||
|
|
||||||
def test_oauth_authorize_access(self):
|
def test_oauth_authorize_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that once logged in, the user can access the authorization page
|
Verify that once logged in, the user can access the authorization page
|
||||||
but can't display the page because the request token is not specified.
|
but can't display the page because the request token is not specified.
|
||||||
"""
|
"""
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
response = self.client.get('/oauth/authorize/', follow=True)
|
response = self.client.get('/oauth/authorize/', follow=True)
|
||||||
self.assertEqual(response.content, 'No request token specified.')
|
self.assertEqual(response.content, 'No request token specified.')
|
||||||
|
|
||||||
def _create_request_token_parameters(self):
|
def _create_request_token_parameters(self):
|
||||||
"""
|
"""
|
||||||
A shortcut to create request's token parameters.
|
A shortcut to create request's token parameters.
|
||||||
|
@ -93,28 +93,28 @@ else:
|
||||||
'oauth_callback': 'http://api.example.com/request_token_ready',
|
'oauth_callback': 'http://api.example.com/request_token_ready',
|
||||||
'scope': 'data',
|
'scope': 'data',
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_oauth_request_token_retrieval(self):
|
def test_oauth_request_token_retrieval(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.failIf(token.key not in response.content)
|
self.failIf(token.key not in response.content)
|
||||||
self.failIf(token.secret not in response.content)
|
self.failIf(token.secret not in response.content)
|
||||||
|
|
||||||
def test_oauth_user_request_authorization(self):
|
def test_oauth_user_request_authorization(self):
|
||||||
"""
|
"""
|
||||||
Verify that the user can access the authorization page once logged in
|
Verify that the user can access the authorization page once logged in
|
||||||
and the request token has been retrieved.
|
and the request token has been retrieved.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
parameters = {'oauth_token': token.key,}
|
parameters = {'oauth_token': token.key,}
|
||||||
|
@ -129,7 +129,7 @@ else:
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
self.failIf(token.key not in response['Location'])
|
self.failIf(token.key not in response['Location'])
|
||||||
self.assertEqual(token.is_approved, 1)
|
self.assertEqual(token.is_approved, 1)
|
||||||
|
|
||||||
def _create_access_token_parameters(self, token):
|
def _create_access_token_parameters(self, token):
|
||||||
"""
|
"""
|
||||||
A shortcut to create access' token parameters.
|
A shortcut to create access' token parameters.
|
||||||
|
@ -145,13 +145,13 @@ else:
|
||||||
'oauth_verifier': token.verifier,
|
'oauth_verifier': token.verifier,
|
||||||
'scope': 'data',
|
'scope': 'data',
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_oauth_access_token_retrieval(self):
|
def test_oauth_access_token_retrieval(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
@ -160,7 +160,7 @@ else:
|
||||||
parameters['authorize_access'] = 1 # fake authorization by the user
|
parameters['authorize_access'] = 1 # fake authorization by the user
|
||||||
response = self.client.post("/oauth/authorize/", parameters)
|
response = self.client.post("/oauth/authorize/", parameters)
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -169,7 +169,7 @@ else:
|
||||||
self.failIf(access_token.key not in response.content)
|
self.failIf(access_token.key not in response.content)
|
||||||
self.failIf(access_token.secret not in response.content)
|
self.failIf(access_token.secret not in response.content)
|
||||||
self.assertEqual(access_token.user.username, 'john')
|
self.assertEqual(access_token.user.username, 'john')
|
||||||
|
|
||||||
def _create_access_parameters(self, access_token):
|
def _create_access_parameters(self, access_token):
|
||||||
"""
|
"""
|
||||||
A shortcut to create access' parameters.
|
A shortcut to create access' parameters.
|
||||||
|
@ -188,13 +188,13 @@ else:
|
||||||
signature = signature_method.sign(oauth_request, self.consumer, access_token)
|
signature = signature_method.sign(oauth_request, self.consumer, access_token)
|
||||||
parameters['oauth_signature'] = signature
|
parameters['oauth_signature'] = signature
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
def test_oauth_protected_resource_access(self):
|
def test_oauth_protected_resource_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
@ -205,7 +205,7 @@ else:
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
||||||
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
|
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
response = self.client.get("/", self._create_access_token_parameters(access_token))
|
response = self.client.get("/", self._create_access_token_parameters(access_token))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import djangorestframework
|
import djangorestframework
|
||||||
|
|
||||||
class TestVersion(TestCase):
|
class TestVersion(TestCase):
|
||||||
"""Simple sanity test to check the VERSION exists"""
|
"""Simple sanity test to check the VERSION exists"""
|
||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
|
|
|
@ -8,76 +8,76 @@
|
||||||
# >>> req = RequestFactory().get('/')
|
# >>> req = RequestFactory().get('/')
|
||||||
# >>> some_view = View()
|
# >>> some_view = View()
|
||||||
# >>> some_view.request = req # Make as if this request had been dispatched
|
# >>> some_view.request = req # Make as if this request had been dispatched
|
||||||
#
|
#
|
||||||
# FormParser
|
# FormParser
|
||||||
# ============
|
# ============
|
||||||
#
|
#
|
||||||
# Data flatening
|
# Data flatening
|
||||||
# ----------------
|
# ----------------
|
||||||
#
|
#
|
||||||
# Here is some example data, which would eventually be sent along with a post request :
|
# Here is some example data, which would eventually be sent along with a post request :
|
||||||
#
|
#
|
||||||
# >>> inpt = urlencode([
|
# >>> inpt = urlencode([
|
||||||
# ... ('key1', 'bla1'),
|
# ... ('key1', 'bla1'),
|
||||||
# ... ('key2', 'blo1'), ('key2', 'blo2'),
|
# ... ('key2', 'blo1'), ('key2', 'blo2'),
|
||||||
# ... ])
|
# ... ])
|
||||||
#
|
#
|
||||||
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||||
#
|
#
|
||||||
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||||
#
|
#
|
||||||
# >>> class MyFormParser(FormParser):
|
# >>> class MyFormParser(FormParser):
|
||||||
# ...
|
# ...
|
||||||
# ... def is_a_list(self, key, val_list):
|
# ... def is_a_list(self, key, val_list):
|
||||||
# ... return len(val_list) > 1
|
# ... return len(val_list) > 1
|
||||||
#
|
#
|
||||||
# This new parser only flattens the lists of parameters that contain a single value.
|
# This new parser only flattens the lists of parameters that contain a single value.
|
||||||
#
|
#
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
||||||
#
|
#
|
||||||
# Submitting an empty list
|
# Submitting an empty list
|
||||||
# --------------------------
|
# --------------------------
|
||||||
#
|
#
|
||||||
# When submitting an empty select multiple, like this one ::
|
# When submitting an empty select multiple, like this one ::
|
||||||
#
|
#
|
||||||
# <select multiple="multiple" name="key2"></select>
|
# <select multiple="multiple" name="key2"></select>
|
||||||
#
|
#
|
||||||
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
||||||
#
|
#
|
||||||
# <select multiple="multiple" name="key2"><option value="_empty"></select>
|
# <select multiple="multiple" name="key2"><option value="_empty"></select>
|
||||||
#
|
#
|
||||||
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
||||||
#
|
#
|
||||||
# >>> inpt = urlencode([
|
# >>> inpt = urlencode([
|
||||||
# ... ('key1', 'blo1'), ('key1', '_empty'),
|
# ... ('key1', 'blo1'), ('key1', '_empty'),
|
||||||
# ... ('key2', '_empty'),
|
# ... ('key2', '_empty'),
|
||||||
# ... ])
|
# ... ])
|
||||||
#
|
#
|
||||||
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||||
#
|
#
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'blo1'}
|
# >>> data == {'key1': 'blo1'}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||||
#
|
#
|
||||||
# >>> class MyFormParser(FormParser):
|
# >>> class MyFormParser(FormParser):
|
||||||
# ...
|
# ...
|
||||||
# ... def is_a_list(self, key, val_list):
|
# ... def is_a_list(self, key, val_list):
|
||||||
# ... return key == 'key2'
|
# ... return key == 'key2'
|
||||||
# ...
|
# ...
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'blo1', 'key2': []}
|
# >>> data == {'key1': 'blo1', 'key2': []}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||||
# """
|
# """
|
||||||
# import httplib, mimetypes
|
# import httplib, mimetypes
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
# from djangorestframework.parsers import MultiPartParser
|
# from djangorestframework.parsers import MultiPartParser
|
||||||
# from djangorestframework.views import View
|
# from djangorestframework.views import View
|
||||||
# from StringIO import StringIO
|
# from StringIO import StringIO
|
||||||
#
|
#
|
||||||
# def encode_multipart_formdata(fields, files):
|
# def encode_multipart_formdata(fields, files):
|
||||||
# """For testing multipart parser.
|
# """For testing multipart parser.
|
||||||
# fields is a sequence of (name, value) elements for regular form fields.
|
# fields is a sequence of (name, value) elements for regular form fields.
|
||||||
|
@ -112,10 +112,10 @@
|
||||||
# body = CRLF.join(L)
|
# body = CRLF.join(L)
|
||||||
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||||
# return content_type, body
|
# return content_type, body
|
||||||
#
|
#
|
||||||
# def get_content_type(filename):
|
# def get_content_type(filename):
|
||||||
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
#
|
#
|
||||||
#class TestMultiPartParser(TestCase):
|
#class TestMultiPartParser(TestCase):
|
||||||
# def setUp(self):
|
# def setUp(self):
|
||||||
# self.req = RequestFactory()
|
# self.req = RequestFactory()
|
||||||
|
@ -145,12 +145,12 @@ class Form(forms.Form):
|
||||||
|
|
||||||
class TestFormParser(TestCase):
|
class TestFormParser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.string = "field1=abc&field2=defghijk"
|
self.string = "field1=abc&field2=defghijk"
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
""" Make sure the `QueryDict` works OK """
|
""" Make sure the `QueryDict` works OK """
|
||||||
parser = FormParser(None)
|
parser = FormParser(None)
|
||||||
|
|
||||||
stream = StringIO(self.string)
|
stream = StringIO(self.string)
|
||||||
(data, files) = parser.parse(stream)
|
(data, files) = parser.parse(stream)
|
||||||
|
|
||||||
|
@ -179,4 +179,4 @@ class TestXMLParser(TestCase):
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
parser = XMLParser(None)
|
parser = XMLParser(None)
|
||||||
(data, files) = parser.parse(self.input)
|
(data, files) = parser.parse(self.input)
|
||||||
self.assertEqual(data, self.data)
|
self.assertEqual(data, self.data)
|
||||||
|
|
|
@ -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])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,7 +157,7 @@ class RendererIntegrationTests(TestCase):
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||||
|
|
||||||
_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}'
|
_indented_repr = '{\n "foo": [\n "bar", \n "baz"\n ]\n}'
|
||||||
|
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -203,7 +250,7 @@ if YAMLRenderer:
|
||||||
"""
|
"""
|
||||||
Test basic YAML rendering.
|
Test basic YAML rendering.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = YAMLRenderer(None)
|
renderer = YAMLRenderer(None)
|
||||||
content = renderer.render(obj, 'application/yaml')
|
content = renderer.render(obj, 'application/yaml')
|
||||||
self.assertEquals(content, _yaml_repr)
|
self.assertEquals(content, _yaml_repr)
|
||||||
|
@ -214,7 +261,7 @@ if YAMLRenderer:
|
||||||
Test rendering and then parsing returns the original object.
|
Test rendering and then parsing returns the original object.
|
||||||
IE obj -> render -> parse -> obj.
|
IE obj -> render -> parse -> obj.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
|
|
||||||
renderer = YAMLRenderer(None)
|
renderer = YAMLRenderer(None)
|
||||||
parser = YAMLParser(None)
|
parser = YAMLParser(None)
|
||||||
|
@ -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>'))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
#from djangorestframework.response import Response
|
#from djangorestframework.response import Response
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#class TestResponse(TestCase):
|
#class TestResponse(TestCase):
|
||||||
#
|
#
|
||||||
# # Interface tests
|
# # Interface tests
|
||||||
#
|
#
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.db import models
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
class TestObjectToData(TestCase):
|
class TestObjectToData(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for the Serializer class.
|
Tests for the Serializer class.
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
|
|
||||||
|
|
||||||
class TestStatus(TestCase):
|
class TestStatus(TestCase):
|
||||||
"""Simple sanity test to check the status module"""
|
"""Simple sanity test to check the status module"""
|
||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
|
|
63
djangorestframework/tests/testcases.py
Normal file
63
djangorestframework/tests/testcases.py
Normal 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)
|
|
@ -21,25 +21,25 @@ class MockView(View):
|
||||||
class MockView_PerViewThrottling(MockView):
|
class MockView_PerViewThrottling(MockView):
|
||||||
permissions = ( PerViewThrottling, )
|
permissions = ( PerViewThrottling, )
|
||||||
|
|
||||||
class MockView_PerResourceThrottling(MockView):
|
class MockView_PerResourceThrottling(MockView):
|
||||||
permissions = ( PerResourceThrottling, )
|
permissions = ( PerResourceThrottling, )
|
||||||
resource = FormResource
|
resource = FormResource
|
||||||
|
|
||||||
class MockView_MinuteThrottling(MockView):
|
class MockView_MinuteThrottling(MockView):
|
||||||
throttle = '3/min'
|
throttle = '3/min'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingTests(TestCase):
|
class ThrottlingTests(TestCase):
|
||||||
urls = 'djangorestframework.tests.throttling'
|
urls = 'djangorestframework.tests.throttling'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Reset the cache so that no throttles will be active
|
Reset the cache so that no throttles will be active
|
||||||
"""
|
"""
|
||||||
cache.clear()
|
cache.clear()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_requests_are_throttled(self):
|
def test_requests_are_throttled(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited
|
Ensure request rate is limited
|
||||||
|
@ -48,7 +48,7 @@ class ThrottlingTests(TestCase):
|
||||||
for dummy in range(4):
|
for dummy in range(4):
|
||||||
response = MockView.as_view()(request)
|
response = MockView.as_view()(request)
|
||||||
self.assertEqual(503, response.status_code)
|
self.assertEqual(503, response.status_code)
|
||||||
|
|
||||||
def set_throttle_timer(self, view, value):
|
def set_throttle_timer(self, view, value):
|
||||||
"""
|
"""
|
||||||
Explicitly set the timer, overriding time.time()
|
Explicitly set the timer, overriding time.time()
|
||||||
|
@ -71,7 +71,7 @@ class ThrottlingTests(TestCase):
|
||||||
|
|
||||||
response = MockView.as_view()(request)
|
response = MockView.as_view()(request)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
def ensure_is_throttled(self, view, expect):
|
def ensure_is_throttled(self, view, expect):
|
||||||
request = self.factory.get('/')
|
request = self.factory.get('/')
|
||||||
request.user = User.objects.create(username='a')
|
request.user = User.objects.create(username='a')
|
||||||
|
@ -80,27 +80,27 @@ class ThrottlingTests(TestCase):
|
||||||
request.user = User.objects.create(username='b')
|
request.user = User.objects.create(username='b')
|
||||||
response = view.as_view()(request)
|
response = view.as_view()(request)
|
||||||
self.assertEqual(expect, response.status_code)
|
self.assertEqual(expect, response.status_code)
|
||||||
|
|
||||||
def test_request_throttling_is_per_user(self):
|
def test_request_throttling_is_per_user(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is only limited per user, not globally for
|
Ensure request rate is only limited per user, not globally for
|
||||||
PerUserThrottles
|
PerUserThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView, 200)
|
self.ensure_is_throttled(MockView, 200)
|
||||||
|
|
||||||
def test_request_throttling_is_per_view(self):
|
def test_request_throttling_is_per_view(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited globally per View for PerViewThrottles
|
Ensure request rate is limited globally per View for PerViewThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
||||||
|
|
||||||
def test_request_throttling_is_per_resource(self):
|
def test_request_throttling_is_per_resource(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited globally per Resource for PerResourceThrottles
|
Ensure request rate is limited globally per Resource for PerResourceThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
|
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
|
||||||
|
|
||||||
|
|
||||||
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
||||||
"""
|
"""
|
||||||
Ensure the response returns an X-Throttle field with status and next attributes
|
Ensure the response returns an X-Throttle field with status and next attributes
|
||||||
|
@ -111,7 +111,7 @@ class ThrottlingTests(TestCase):
|
||||||
self.set_throttle_timer(view, timer)
|
self.set_throttle_timer(view, timer)
|
||||||
response = view.as_view()(request)
|
response = view.as_view()(request)
|
||||||
self.assertEquals(response['X-Throttle'], expect)
|
self.assertEquals(response['X-Throttle'], expect)
|
||||||
|
|
||||||
def test_seconds_fields(self):
|
def test_seconds_fields(self):
|
||||||
"""
|
"""
|
||||||
Ensure for second based throttles.
|
Ensure for second based throttles.
|
||||||
|
@ -122,7 +122,7 @@ class ThrottlingTests(TestCase):
|
||||||
(0, 'status=SUCCESS; next=1.00 sec'),
|
(0, 'status=SUCCESS; next=1.00 sec'),
|
||||||
(0, 'status=FAILURE; next=1.00 sec')
|
(0, 'status=FAILURE; next=1.00 sec')
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_minutes_fields(self):
|
def test_minutes_fields(self):
|
||||||
"""
|
"""
|
||||||
Ensure for minute based throttles.
|
Ensure for minute based throttles.
|
||||||
|
@ -133,7 +133,7 @@ class ThrottlingTests(TestCase):
|
||||||
(0, 'status=SUCCESS; next=60.00 sec'),
|
(0, 'status=SUCCESS; next=60.00 sec'),
|
||||||
(0, 'status=FAILURE; next=60.00 sec')
|
(0, 'status=FAILURE; next=60.00 sec')
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_next_rate_remains_constant_if_followed(self):
|
def test_next_rate_remains_constant_if_followed(self):
|
||||||
"""
|
"""
|
||||||
If a client follows the recommended next request rate,
|
If a client follows the recommended next request rate,
|
||||||
|
|
|
@ -22,7 +22,7 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||||
|
|
||||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||||
|
@ -33,12 +33,12 @@ class TestDisabledValidations(TestCase):
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||||
"""If the view's form is None and does not have a Resource with a model set then
|
"""If the view's form is None and does not have a Resource with a model set then
|
||||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||||
|
@ -47,17 +47,17 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
|
||||||
|
|
||||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||||
class DisabledModelFormView(View):
|
class DisabledModelFormView(View):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
class TestNonFieldErrors(TestCase):
|
class TestNonFieldErrors(TestCase):
|
||||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||||
|
@ -68,15 +68,15 @@ class TestNonFieldErrors(TestCase):
|
||||||
field1 = forms.CharField(required=False)
|
field1 = forms.CharField(required=False)
|
||||||
field2 = forms.CharField(required=False)
|
field2 = forms.CharField(required=False)
|
||||||
ERROR_TEXT = 'You may not supply both field1 and field2'
|
ERROR_TEXT = 'You may not supply both field1 and field2'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
||||||
raise forms.ValidationError(self.ERROR_TEXT)
|
raise forms.ValidationError(self.ERROR_TEXT)
|
||||||
return self.cleaned_data #pragma: no cover
|
return self.cleaned_data #pragma: no cover
|
||||||
|
|
||||||
class MockResource(FormResource):
|
class MockResource(FormResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase):
|
||||||
content = {'field1': 'example1', 'field2': 'example2'}
|
content = {'field1': 'example1', 'field2': 'example2'}
|
||||||
try:
|
try:
|
||||||
MockResource(view).validate_request(content, None)
|
MockResource(view).validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
else:
|
else:
|
||||||
self.fail('ErrorResponse was not raised') #pragma: no cover
|
self.fail('ErrorResponse was not raised') #pragma: no cover
|
||||||
|
@ -94,26 +94,26 @@ class TestFormValidation(TestCase):
|
||||||
"""Tests which check basic form validation.
|
"""Tests which check basic form validation.
|
||||||
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
||||||
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class MockForm(forms.Form):
|
class MockForm(forms.Form):
|
||||||
qwerty = forms.CharField(required=True)
|
qwerty = forms.CharField(required=True)
|
||||||
|
|
||||||
class MockFormResource(FormResource):
|
class MockFormResource(FormResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockModelResource(ModelResource):
|
class MockModelResource(ModelResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockFormView(View):
|
class MockFormView(View):
|
||||||
resource = MockFormResource
|
resource = MockFormResource
|
||||||
|
|
||||||
class MockModelFormView(View):
|
class MockModelFormView(View):
|
||||||
resource = MockModelResource
|
resource = MockModelResource
|
||||||
|
|
||||||
self.MockFormResource = MockFormResource
|
self.MockFormResource = MockFormResource
|
||||||
self.MockModelResource = MockModelResource
|
self.MockModelResource = MockModelResource
|
||||||
self.MockFormView = MockFormView
|
self.MockFormView = MockFormView
|
||||||
self.MockModelFormView = MockModelFormView
|
self.MockModelFormView = MockModelFormView
|
||||||
|
|
||||||
|
|
||||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||||
|
@ -130,26 +130,26 @@ class TestFormValidation(TestCase):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
validator._validate(content, None, allowed_extra_fields=('extra',))
|
validator._validate(content, None, allowed_extra_fields=('extra',))
|
||||||
|
|
||||||
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
||||||
content = {'qwerty': 'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
||||||
|
|
||||||
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
||||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||||
content = {}
|
content = {}
|
||||||
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
|
||||||
|
|
||||||
|
@ -158,8 +158,8 @@ class TestFormValidation(TestCase):
|
||||||
content = {'qwerty': ''}
|
content = {'qwerty': ''}
|
||||||
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
|
||||||
|
|
||||||
|
@ -168,18 +168,18 @@ class TestFormValidation(TestCase):
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
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
|
||||||
|
|
||||||
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
||||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||||
content = {'qwerty': '', 'extra': 'extra'}
|
content = {'qwerty': '', 'extra': 'extra'}
|
||||||
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
|
||||||
|
@ -263,23 +263,23 @@ class TestFormValidation(TestCase):
|
||||||
|
|
||||||
class TestModelFormValidator(TestCase):
|
class TestModelFormValidator(TestCase):
|
||||||
"""Tests specific to ModelFormValidatorMixin"""
|
"""Tests specific to ModelFormValidatorMixin"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create a validator for a model with two fields and a property."""
|
"""Create a validator for a model with two fields and a property."""
|
||||||
class MockModel(models.Model):
|
class MockModel(models.Model):
|
||||||
qwerty = models.CharField(max_length=256)
|
qwerty = models.CharField(max_length=256)
|
||||||
uiop = models.CharField(max_length=256, blank=True)
|
uiop = models.CharField(max_length=256, blank=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def readonly(self):
|
def readonly(self):
|
||||||
return 'read only'
|
return 'read only'
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
class MockResource(ModelResource):
|
||||||
model = MockModel
|
model = MockModel
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
resource = MockResource
|
resource = MockResource
|
||||||
|
|
||||||
self.validator = MockResource(MockView)
|
self.validator = MockResource(MockView)
|
||||||
|
|
||||||
|
|
||||||
|
@ -299,19 +299,19 @@ class TestModelFormValidator(TestCase):
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_requires_fields_on_model_forms(self):
|
def test_validate_requires_fields_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'readonly': 'read only'}
|
content = {'readonly': 'read only'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
content = {'qwerty':'example', 'readonly': 'read only'}
|
||||||
self.validator.validate_request(content, None)
|
self.validator.validate_request(content, None)
|
||||||
|
|
||||||
def test_model_form_validator_uses_model_forms(self):
|
def test_model_form_validator_uses_model_forms(self):
|
||||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,17 @@ class ResourceMockView(View):
|
||||||
foo = forms.BooleanField(required=False)
|
foo = forms.BooleanField(required=False)
|
||||||
bar = forms.IntegerField(help_text='Must be an integer.')
|
bar = forms.IntegerField(help_text='Must be an integer.')
|
||||||
baz = forms.CharField(max_length=32)
|
baz = forms.CharField(max_length=32)
|
||||||
|
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
class MockResource(ModelResource):
|
||||||
"""This is a mock model-based resource"""
|
"""This is a mock model-based resource"""
|
||||||
|
|
||||||
class MockResourceModel(models.Model):
|
class MockResourceModel(models.Model):
|
||||||
foo = models.BooleanField()
|
foo = models.BooleanField()
|
||||||
bar = models.IntegerField(help_text='Must be an integer.')
|
bar = models.IntegerField(help_text='Must be an integer.')
|
||||||
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
||||||
|
|
||||||
model = MockResourceModel
|
model = MockResourceModel
|
||||||
fields = ('foo', 'bar', 'baz')
|
fields = ('foo', 'bar', 'baz')
|
||||||
|
|
||||||
|
@ -50,62 +50,62 @@ urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||||
|
|
||||||
class BaseViewTests(TestCase):
|
class BaseViewTests(TestCase):
|
||||||
"""Test the base view class of djangorestframework"""
|
"""Test the base view class of djangorestframework"""
|
||||||
urls = 'djangorestframework.tests.views'
|
urls = 'djangorestframework.tests.views'
|
||||||
|
|
||||||
def test_options_method_simple_view(self):
|
def test_options_method_simple_view(self):
|
||||||
response = self.client.options('/mock/')
|
response = self.client.options('/mock/')
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Mock',
|
name='Mock',
|
||||||
description='This is a basic mock view')
|
description='This is a basic mock view')
|
||||||
|
|
||||||
def test_options_method_resource_view(self):
|
def test_options_method_resource_view(self):
|
||||||
response = self.client.options('/resourcemock/')
|
response = self.client.options('/resourcemock/')
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Resource Mock',
|
name='Resource Mock',
|
||||||
description='This is a resource-based mock view',
|
description='This is a resource-based mock view',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo':'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar':'IntegerField',
|
||||||
'baz':'CharField',
|
'baz':'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_options_method_model_resource_list_view(self):
|
def test_options_method_model_resource_list_view(self):
|
||||||
response = self.client.options('/model/')
|
response = self.client.options('/model/')
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Mock List',
|
name='Mock List',
|
||||||
description='This is a mock model-based resource',
|
description='This is a mock model-based resource',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo':'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar':'IntegerField',
|
||||||
'baz':'CharField',
|
'baz':'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_options_method_model_resource_detail_view(self):
|
def test_options_method_model_resource_detail_view(self):
|
||||||
response = self.client.options('/model/0/')
|
response = self.client.options('/model/0/')
|
||||||
self._verify_options_response(response,
|
self._verify_options_response(response,
|
||||||
name='Mock Instance',
|
name='Mock Instance',
|
||||||
description='This is a mock model-based resource',
|
description='This is a mock model-based resource',
|
||||||
fields={'foo':'BooleanField',
|
fields={'foo':'BooleanField',
|
||||||
'bar':'IntegerField',
|
'bar':'IntegerField',
|
||||||
'baz':'CharField',
|
'baz':'CharField',
|
||||||
})
|
})
|
||||||
|
|
||||||
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||||
mime_type='application/json'):
|
mime_type='application/json'):
|
||||||
self.assertEqual(response.status_code, status)
|
self.assertEqual(response.status_code, status)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||||
parser = JSONParser(None)
|
parser = JSONParser(None)
|
||||||
(data, files) = parser.parse(StringIO(response.content))
|
(data, files) = parser.parse(StringIO(response.content))
|
||||||
self.assertTrue('application/json' in data['renders'])
|
self.assertTrue('application/json' in data['renders'])
|
||||||
self.assertEqual(name, data['name'])
|
self.assertEqual(name, data['name'])
|
||||||
self.assertEqual(description, data['description'])
|
self.assertEqual(description, data['description'])
|
||||||
if fields is None:
|
if fields is None:
|
||||||
self.assertFalse(hasattr(data, 'fields'))
|
self.assertFalse(hasattr(data, 'fields'))
|
||||||
else:
|
else:
|
||||||
self.assertEqual(data['fields'], fields)
|
self.assertEqual(data['fields'], fields)
|
||||||
|
|
||||||
|
|
||||||
class ExtraViewsTests(TestCase):
|
class ExtraViewsTests(TestCase):
|
||||||
"""Test the extra views djangorestframework provides"""
|
"""Test the extra views djangorestframework provides"""
|
||||||
urls = 'djangorestframework.tests.views'
|
urls = 'djangorestframework.tests.views'
|
||||||
|
|
||||||
def test_robots_view(self):
|
def test_robots_view(self):
|
||||||
"""Ensure the robots view exists"""
|
"""Ensure the robots view exists"""
|
||||||
|
|
|
@ -13,4 +13,4 @@ urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
urlpatterns += patterns('djangorestframework.utils.staticviews',
|
urlpatterns += patterns('djangorestframework.utils.staticviews',
|
||||||
(r'favicon.ico', 'favicon'),
|
(r'favicon.ico', 'favicon'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,12 +3,12 @@ from djangorestframework.utils.description import get_name
|
||||||
|
|
||||||
def get_breadcrumbs(url):
|
def get_breadcrumbs(url):
|
||||||
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
||||||
|
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
|
||||||
def breadcrumbs_recursive(url, breadcrumbs_list):
|
def breadcrumbs_recursive(url, breadcrumbs_list):
|
||||||
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
|
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(view, unused_args, unused_kwargs) = resolve(url)
|
(view, unused_args, unused_kwargs) = resolve(url)
|
||||||
except:
|
except:
|
||||||
|
@ -17,15 +17,15 @@ def get_breadcrumbs(url):
|
||||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||||
if isinstance(getattr(view, 'cls_instance', None), View):
|
if isinstance(getattr(view, 'cls_instance', None), View):
|
||||||
breadcrumbs_list.insert(0, (get_name(view), url))
|
breadcrumbs_list.insert(0, (get_name(view), url))
|
||||||
|
|
||||||
if url == '':
|
if url == '':
|
||||||
# All done
|
# All done
|
||||||
return breadcrumbs_list
|
return breadcrumbs_list
|
||||||
|
|
||||||
elif url.endswith('/'):
|
elif url.endswith('/'):
|
||||||
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
|
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
|
||||||
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
|
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
|
||||||
|
|
||||||
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
|
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
|
||||||
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
|
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||||
def get_name(view):
|
def get_name(view):
|
||||||
"""
|
"""
|
||||||
Return a name for the 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 view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ def get_name(view):
|
||||||
# If this view has a resource that's been overridden, then use that resource for the name
|
# 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):
|
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
||||||
name = view.resource.__name__
|
name = view.resource.__name__
|
||||||
|
|
||||||
# Chomp of any non-descriptive trailing part of the resource class name
|
# Chomp of any non-descriptive trailing part of the resource class name
|
||||||
if name.endswith('Resource') and name != 'Resource':
|
if name.endswith('Resource') and name != 'Resource':
|
||||||
name = name[:-len('Resource')]
|
name = name[:-len('Resource')]
|
||||||
|
@ -30,7 +30,7 @@ def get_name(view):
|
||||||
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
|
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
|
||||||
if getattr(view, '_suffix', None):
|
if getattr(view, '_suffix', None):
|
||||||
name += view._suffix
|
name += view._suffix
|
||||||
|
|
||||||
# Otherwise if it's a function view use the function's name
|
# Otherwise if it's a function view use the function's name
|
||||||
elif getattr(view, '__name__', None) is not None:
|
elif getattr(view, '__name__', None) is not None:
|
||||||
name = view.__name__
|
name = view.__name__
|
||||||
|
@ -62,12 +62,12 @@ def get_description(view):
|
||||||
# grok the class instance that we stored when as_view was called.
|
# grok the class instance that we stored when as_view was called.
|
||||||
if getattr(view, 'cls_instance', None):
|
if getattr(view, 'cls_instance', None):
|
||||||
view = view.cls_instance
|
view = view.cls_instance
|
||||||
|
|
||||||
|
|
||||||
# If this view has a resource that's been overridden, then use the resource's doctring
|
# 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):
|
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
|
||||||
doc = view.resource.__doc__
|
doc = view.resource.__doc__
|
||||||
|
|
||||||
# Otherwise use the view doctring
|
# Otherwise use the view doctring
|
||||||
elif getattr(view, '__doc__', None):
|
elif getattr(view, '__doc__', None):
|
||||||
doc = view.__doc__
|
doc = view.__doc__
|
||||||
|
@ -81,11 +81,11 @@ def get_description(view):
|
||||||
|
|
||||||
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
|
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
|
||||||
|
|
||||||
# unindent the docstring if needed
|
# unindent the docstring if needed
|
||||||
if whitespace_counts:
|
if whitespace_counts:
|
||||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||||
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
|
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
|
||||||
|
|
||||||
# otherwise return it as-is
|
# otherwise return it as-is
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ class _MediaType(object):
|
||||||
# return Decimal(self.params.get('q', '1.0'))
|
# return Decimal(self.params.get('q', '1.0'))
|
||||||
# except:
|
# except:
|
||||||
# return Decimal(0)
|
# return Decimal(0)
|
||||||
|
|
||||||
#def score(self):
|
#def score(self):
|
||||||
# """
|
# """
|
||||||
# Return an overall score for a given media type given it's quality and precedence.
|
# Return an overall score for a given media type given it's quality and precedence.
|
||||||
|
@ -119,7 +119,7 @@ class _MediaType(object):
|
||||||
# # NB. quality values should only have up to 3 decimal points
|
# # NB. quality values should only have up to 3 decimal points
|
||||||
# # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
|
# # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
|
||||||
# return self.quality * 10000 + self.precedence
|
# return self.quality * 10000 + self.precedence
|
||||||
|
|
||||||
#def as_tuple(self):
|
#def as_tuple(self):
|
||||||
# return (self.main_type, self.sub_type, self.params)
|
# return (self.main_type, self.sub_type, self.params)
|
||||||
|
|
||||||
|
|
|
@ -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
9
docs/check_sphinx.py
Normal 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)])
|
16
docs/conf.py
16
docs/conf.py
|
@ -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
10
docs/contents.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
howto
|
||||||
|
library
|
||||||
|
examples
|
||||||
|
|
23
docs/examples.rst
Normal file
23
docs/examples.rst
Normal 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/*
|
|
@ -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
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -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."]}}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
66
docs/examples/permissions.rst
Normal file
66
docs/examples/permissions.rst
Normal 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.
|
|
@ -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."]}}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
.. _sandbox:
|
|
||||||
|
|
||||||
Sandbox Root API
|
Sandbox Root API
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
|
@ -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
8
docs/howto.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
How Tos, FAQs & Notes
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
howto/*
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
|
@ -27,4 +27,4 @@ There are a few things that can be helpful to remember when using CURL with djan
|
||||||
|
|
||||||
#. You can use basic authentication to send the username and password::
|
#. You can use basic authentication to send the username and password::
|
||||||
|
|
||||||
curl -X GET -H 'Accept: application/json' -u <user>:<password> http://example.com/my-api/
|
curl -X GET -H 'Accept: application/json' -u <user>:<password> http://example.com/my-api/
|
||||||
|
|
39
docs/howto/usingurllib2.rst
Normal file
39
docs/howto/usingurllib2.rst
Normal 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.
|
|
@ -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
8
docs/library.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Library
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
library/*
|
2
docs/templates/layout.html
vendored
2
docs/templates/layout.html
vendored
|
@ -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
1
examples/.epio-app
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rest
|
|
@ -12,16 +12,16 @@ class BlogPostResource(ModelResource):
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
|
|
||||||
def comments(self, instance):
|
def comments(self, instance):
|
||||||
return reverse('comments', kwargs={'blogpost': instance.key})
|
return reverse('comments', kwargs={'blogpost': instance.key})
|
||||||
|
|
||||||
|
|
||||||
class CommentResource(ModelResource):
|
class CommentResource(ModelResource):
|
||||||
"""
|
"""
|
||||||
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
|
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
|
||||||
"""
|
"""
|
||||||
model = Comment
|
model = Comment
|
||||||
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
|
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
|
|
||||||
def blogpost(self, instance):
|
def blogpost(self, instance):
|
||||||
return reverse('blog-post', kwargs={'key': instance.blogpost.key})
|
return reverse('blog-post', kwargs={'key': instance.blogpost.key})
|
||||||
|
|
|
@ -15,68 +15,68 @@ from blogpost import models, urls
|
||||||
|
|
||||||
# class AcceptHeaderTests(TestCase):
|
# class AcceptHeaderTests(TestCase):
|
||||||
# """Test correct behaviour of the Accept header as specified by RFC 2616:
|
# """Test correct behaviour of the Accept header as specified by RFC 2616:
|
||||||
#
|
#
|
||||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
||||||
#
|
#
|
||||||
# def assert_accept_mimetype(self, mimetype, expect=None):
|
# def assert_accept_mimetype(self, mimetype, expect=None):
|
||||||
# """Assert that a request with given mimetype in the accept header,
|
# """Assert that a request with given mimetype in the accept header,
|
||||||
# gives a response with the appropriate content-type."""
|
# gives a response with the appropriate content-type."""
|
||||||
# if expect is None:
|
# if expect is None:
|
||||||
# expect = mimetype
|
# expect = mimetype
|
||||||
#
|
#
|
||||||
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
||||||
#
|
#
|
||||||
# self.assertEquals(resp['content-type'], expect)
|
# self.assertEquals(resp['content-type'], expect)
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# def dont_test_accept_json(self):
|
# def dont_test_accept_json(self):
|
||||||
# """Ensure server responds with Content-Type of JSON when requested."""
|
# """Ensure server responds with Content-Type of JSON when requested."""
|
||||||
# self.assert_accept_mimetype('application/json')
|
# self.assert_accept_mimetype('application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_xml(self):
|
# def dont_test_accept_xml(self):
|
||||||
# """Ensure server responds with Content-Type of XML when requested."""
|
# """Ensure server responds with Content-Type of XML when requested."""
|
||||||
# self.assert_accept_mimetype('application/xml')
|
# self.assert_accept_mimetype('application/xml')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_json_when_prefered_to_xml(self):
|
# def dont_test_accept_json_when_prefered_to_xml(self):
|
||||||
# """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
# """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
||||||
# self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
|
# self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_xml_when_prefered_to_json(self):
|
# def dont_test_accept_xml_when_prefered_to_json(self):
|
||||||
# """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
# """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
||||||
# self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
|
# self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
|
||||||
#
|
#
|
||||||
# def dont_test_default_json_prefered(self):
|
# def dont_test_default_json_prefered(self):
|
||||||
# """Ensure server responds with JSON in preference to XML."""
|
# """Ensure server responds with JSON in preference to XML."""
|
||||||
# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
|
# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_generic_subtype_format(self):
|
# def dont_test_accept_generic_subtype_format(self):
|
||||||
# """Ensure server responds with an appropriate type, when the subtype is left generic."""
|
# """Ensure server responds with an appropriate type, when the subtype is left generic."""
|
||||||
# self.assert_accept_mimetype('text/*', expect='text/html')
|
# self.assert_accept_mimetype('text/*', expect='text/html')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_generic_type_format(self):
|
# def dont_test_accept_generic_type_format(self):
|
||||||
# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
||||||
# self.assert_accept_mimetype('*/*', expect='application/json')
|
# self.assert_accept_mimetype('*/*', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_invalid_accept_header_returns_406(self):
|
# def dont_test_invalid_accept_header_returns_406(self):
|
||||||
# """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
# """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
||||||
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
||||||
# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
||||||
# self.assertEquals(resp.status_code, 406)
|
# self.assertEquals(resp.status_code, 406)
|
||||||
#
|
#
|
||||||
# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
|
# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
|
||||||
# """More specific accept types have precedence over less specific types."""
|
# """More specific accept types have precedence over less specific types."""
|
||||||
# self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
|
# self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
|
||||||
# self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
|
# self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# class AllowedMethodsTests(TestCase):
|
# class AllowedMethodsTests(TestCase):
|
||||||
# """Basic tests to check that only allowed operations may be performed on a Resource"""
|
# """Basic tests to check that only allowed operations may be performed on a Resource"""
|
||||||
#
|
#
|
||||||
# def dont_test_reading_a_read_only_resource_is_allowed(self):
|
# def dont_test_reading_a_read_only_resource_is_allowed(self):
|
||||||
# """GET requests on a read only resource should default to a 200 (OK) response"""
|
# """GET requests on a read only resource should default to a 200 (OK) response"""
|
||||||
# resp = self.client.get(reverse(views.RootResource))
|
# resp = self.client.get(reverse(views.RootResource))
|
||||||
# self.assertEquals(resp.status_code, 200)
|
# self.assertEquals(resp.status_code, 200)
|
||||||
#
|
#
|
||||||
# def dont_test_writing_to_read_only_resource_is_not_allowed(self):
|
# def dont_test_writing_to_read_only_resource_is_not_allowed(self):
|
||||||
# """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
# """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
||||||
# resp = self.client.put(reverse(views.RootResource), {})
|
# resp = self.client.put(reverse(views.RootResource), {})
|
||||||
|
@ -171,7 +171,7 @@ from blogpost import models, urls
|
||||||
|
|
||||||
|
|
||||||
class TestRotation(TestCase):
|
class TestRotation(TestCase):
|
||||||
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
|
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
|
||||||
Whenever a new Blogpost is posted the oldest one should be popped."""
|
Whenever a new Blogpost is posted the oldest one should be popped."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -193,7 +193,7 @@ class TestRotation(TestCase):
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
view(request)
|
view(request)
|
||||||
self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
|
self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
|
||||||
|
|
||||||
def test_fifo_behaviour(self):
|
def test_fifo_behaviour(self):
|
||||||
'''It's fine that the Blogposts are capped off at MAX_POSTS. But we want to make sure we see FIFO behaviour.'''
|
'''It's fine that the Blogposts are capped off at MAX_POSTS. But we want to make sure we see FIFO behaviour.'''
|
||||||
for post in range(15):
|
for post in range(15):
|
||||||
|
@ -201,11 +201,11 @@ class TestRotation(TestCase):
|
||||||
request = self.factory.post('/blog-post', data=form_data)
|
request = self.factory.post('/blog-post', data=form_data)
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
view(request)
|
view(request)
|
||||||
request = self.factory.get('/blog-post')
|
request = self.factory.get('/blog-post')
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response_posts = json.loads(response.content)
|
response_posts = json.loads(response.content)
|
||||||
response_titles = [d['title'] for d in response_posts]
|
response_titles = [d['title'] for d in response_posts]
|
||||||
response_titles.reverse()
|
response_titles.reverse()
|
||||||
self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])
|
self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])
|
||||||
|
|
||||||
|
|
62
examples/epio.ini
Normal file
62
examples/epio.ini
Normal 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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
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.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.mixins import ResponseMixin
|
from djangorestframework.mixins import ResponseMixin
|
||||||
from djangorestframework.renderers import DEFAULT_RENDERERS
|
from djangorestframework.renderers import DEFAULT_RENDERERS
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from objectstore.views import ObjectStoreRoot, StoredObject
|
from objectstore.views import ObjectStoreRoot, StoredObject
|
||||||
|
|
||||||
urlpatterns = patterns('objectstore.views',
|
urlpatterns = patterns('objectstore.views',
|
||||||
url(r'^$', ObjectStoreRoot.as_view(), name='object-store-root'),
|
url(r'^$', ObjectStoreRoot.as_view(), name='object-store-root'),
|
||||||
url(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', StoredObject.as_view(), name='stored-object'),
|
url(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', StoredObject.as_view(), name='stored-object'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -39,7 +42,7 @@ class ObjectStoreRoot(View):
|
||||||
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
|
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)]
|
key=operator.itemgetter(1), reverse=True)]
|
||||||
return [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]
|
return [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
Create a new stored object, with a unique key.
|
Create a new stored object, with a unique key.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
#for fixture loading
|
#for fixture loading
|
||||||
|
|
|
@ -6,33 +6,36 @@ class PermissionsExampleView(View):
|
||||||
"""
|
"""
|
||||||
A container view for permissions examples.
|
A container view for permissions examples.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return [{'name': 'Throttling Example', 'url': reverse('throttled-resource')},
|
return [{'name': 'Throttling Example', 'url': reverse('throttled-resource')},
|
||||||
{'name': 'Logged in example', 'url': reverse('loggedin-resource')},]
|
{'name': 'Logged in example', 'url': reverse('loggedin-resource')},]
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingExampleView(View):
|
class ThrottlingExampleView(View):
|
||||||
"""
|
"""
|
||||||
A basic read-only View that has a **per-user throttle** of 10 requests per minute.
|
A basic read-only View that has a **per-user throttle** of 10 requests per minute.
|
||||||
|
|
||||||
If a user exceeds the 10 requests limit within a period of one minute, the
|
If a user exceeds the 10 requests limit within a period of one minute, the
|
||||||
throttle will be applied until 60 seconds have passed since the first request.
|
throttle will be applied until 60 seconds have passed since the first request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
permissions = ( PerUserThrottling, )
|
permissions = ( PerUserThrottling, )
|
||||||
throttle = '10/min'
|
throttle = '10/min'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Handle GET requests.
|
Handle GET requests.
|
||||||
"""
|
"""
|
||||||
return "Successful response to GET request because throttle is not yet active."
|
return "Successful response to GET request because throttle is not yet active."
|
||||||
|
|
||||||
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?'
|
||||||
|
|
|
@ -13,7 +13,7 @@ class PygmentsForm(forms.Form):
|
||||||
|
|
||||||
code = forms.CharField(widget=forms.Textarea,
|
code = forms.CharField(widget=forms.Textarea,
|
||||||
label='Code Text',
|
label='Code Text',
|
||||||
max_length=1000000,
|
max_length=1000000,
|
||||||
help_text='(Copy and paste the code text here.)')
|
help_text='(Copy and paste the code text here.)')
|
||||||
title = forms.CharField(required=False,
|
title = forms.CharField(required=False,
|
||||||
help_text='(Optional)',
|
help_text='(Optional)',
|
||||||
|
|
|
@ -46,4 +46,4 @@ class TestPygmentsExample(TestCase):
|
||||||
self.assertEquals(locations, response_locations)
|
self.assertEquals(locations, response_locations)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.conf.urls.defaults import patterns, url
|
||||||
from pygments_api.views import PygmentsRoot, PygmentsInstance
|
from pygments_api.views import PygmentsRoot, PygmentsInstance
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', PygmentsRoot.as_view(), name='pygments-root'),
|
url(r'^$', PygmentsRoot.as_view(), name='pygments-root'),
|
||||||
url(r'^([a-zA-Z0-9-]+)/$', PygmentsInstance.as_view(), name='pygments-instance'),
|
url(r'^([a-zA-Z0-9-]+)/$', PygmentsInstance.as_view(), name='pygments-instance'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -72,10 +75,10 @@ class PygmentsRoot(View):
|
||||||
linenos = 'table' if self.CONTENT['linenos'] else False
|
linenos = 'table' if self.CONTENT['linenos'] else False
|
||||||
options = {'title': self.CONTENT['title']} if self.CONTENT['title'] else {}
|
options = {'title': self.CONTENT['title']} if self.CONTENT['title'] else {}
|
||||||
formatter = HtmlFormatter(style=self.CONTENT['style'], linenos=linenos, full=True, **options)
|
formatter = HtmlFormatter(style=self.CONTENT['style'], linenos=linenos, full=True, **options)
|
||||||
|
|
||||||
with open(pathname, 'w') as outfile:
|
with open(pathname, 'w') as outfile:
|
||||||
highlight(self.CONTENT['code'], lexer, formatter, outfile)
|
highlight(self.CONTENT['code'], lexer, formatter, outfile)
|
||||||
|
|
||||||
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
remove_oldest_files(HIGHLIGHTED_CODE_DIR, MAX_FILES)
|
||||||
|
|
||||||
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', args=[unique_id])})
|
return Response(status.HTTP_201_CREATED, headers={'Location': reverse('pygments-instance', args=[unique_id])})
|
||||||
|
|
3
examples/requirements-epio.txt
Normal file
3
examples/requirements-epio.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Pygments==1.4
|
||||||
|
Markdown==2.0.3
|
||||||
|
djangorestframework
|
|
@ -34,7 +34,7 @@ class AnotherExampleView(View):
|
||||||
if int(num) > 2:
|
if int(num) > 2:
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status.HTTP_404_NOT_FOUND)
|
||||||
return "GET request to AnotherExampleResource %s" % num
|
return "GET request to AnotherExampleResource %s" % num
|
||||||
|
|
||||||
def post(self, request, num):
|
def post(self, request, num):
|
||||||
"""
|
"""
|
||||||
Handle POST requests, with form validation.
|
Handle POST requests, with form validation.
|
||||||
|
|
|
@ -8,7 +8,7 @@ from coverage import coverage
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run the tests for the examples and generate a coverage report."""
|
"""Run the tests for the examples and generate a coverage report."""
|
||||||
|
|
||||||
# Discover the list of all modules that we should test coverage for
|
# Discover the list of all modules that we should test coverage for
|
||||||
project_dir = os.path.dirname(__file__)
|
project_dir = os.path.dirname(__file__)
|
||||||
cov_files = []
|
cov_files = []
|
||||||
|
@ -18,7 +18,7 @@ def main():
|
||||||
continue
|
continue
|
||||||
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
||||||
TestRunner = get_runner(settings)
|
TestRunner = get_runner(settings)
|
||||||
|
|
||||||
cov = coverage()
|
cov = coverage()
|
||||||
cov.erase()
|
cov.erase()
|
||||||
cov.start()
|
cov.start()
|
||||||
|
|
|
@ -14,8 +14,8 @@ class Sandbox(View):
|
||||||
bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer)
|
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)
|
bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer)
|
||||||
|
|
||||||
The examples provided:
|
The examples provided:
|
||||||
|
|
||||||
1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class.
|
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.
|
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 [RendererMixin](http://django-rest-framework.org/library/renderers.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).
|
||||||
|
|
|
@ -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,12 +47,12 @@ 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).
|
||||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||||
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
|
# NOTE: None of the djangorestframework examples serve media content via MEDIA_URL.
|
||||||
MEDIA_URL = ''
|
MEDIA_URL = ''
|
||||||
|
|
||||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||||
|
@ -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'
|
||||||
|
@ -90,10 +91,10 @@ TEMPLATE_DIRS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
# for loading initial data
|
# for loading initial data
|
||||||
##SERIALIZATION_MODULES = {
|
##SERIALIZATION_MODULES = {
|
||||||
# 'yml': "django.core.serializers.pyyaml"
|
# 'yml': "django.core.serializers.pyyaml"
|
||||||
|
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
|
@ -118,4 +119,4 @@ if os.environ.get('HUDSON_URL', None):
|
||||||
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
|
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
|
||||||
TEST_OUTPUT_VERBOSE = True
|
TEST_OUTPUT_VERBOSE = True
|
||||||
TEST_OUTPUT_DESCRIPTIONS = True
|
TEST_OUTPUT_DESCRIPTIONS = True
|
||||||
TEST_OUTPUT_DIR = 'xmlrunner'
|
TEST_OUTPUT_DIR = 'xmlrunner'
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
213
tox.ini
213
tox.ini
|
@ -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
|
||||||
|
@ -37,7 +44,9 @@ deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
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
|
||||||
|
@ -45,114 +54,206 @@ deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
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
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
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
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
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
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
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
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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:py26-django12e]
|
[testenv:py26-django12-examples]
|
||||||
basepython=python2.6
|
basepython=python2.6
|
||||||
commands=
|
commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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:py27-django12e]
|
[testenv:py27-django12-examples]
|
||||||
basepython=python2.7
|
basepython=python2.7
|
||||||
commands=
|
commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.2.4
|
django==1.2.4
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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-django13e]
|
[testenv:py25-django13-examples]
|
||||||
basepython=python2.5
|
basepython=python2.5
|
||||||
commands=
|
commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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:py26-django13e]
|
[testenv:py26-django13-examples]
|
||||||
basepython=python2.6
|
basepython=python2.6
|
||||||
commands=
|
commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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:py27-django13e]
|
[testenv:py27-django13-examples]
|
||||||
basepython=python2.7
|
basepython=python2.7
|
||||||
commands=
|
commands=
|
||||||
python examples/runtests.py
|
python examples/runtests.py
|
||||||
deps=
|
deps=
|
||||||
django==1.3
|
django==1.3
|
||||||
coverage==3.4
|
coverage==3.4
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
httplib2==0.6.0
|
httplib2==0.6.0
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user