Merge pull request #1 from tomchristie/master

latest
This commit is contained in:
pombredanne 2012-08-13 05:24:23 -07:00
commit bacedef4da
114 changed files with 4489 additions and 1766 deletions

14
.gitignore vendored
View File

@ -1,7 +1,9 @@
*.pyc
*.db
assetplatform.egg-info/*
*~
.*
assetplatform.egg-info/*
coverage.xml
env
docs/build
@ -14,11 +16,5 @@ dist/*
xmlrunner/*
djangorestframework.egg-info/*
MANIFEST
.project
.pydevproject
.settings
.cache
.coverage
.tox
.DS_Store
.idea/*
!.gitignore

View File

@ -1,25 +0,0 @@
syntax: glob
*.pyc
*.db
assetplatform.egg-info/*
*~
coverage.xml
env
docs/build
html
htmlcov
examples/media/pygments/[A-Za-z0-9]*
examples/media/objectstore/[A-Za-z0-9]*
build/*
dist/*
xmlrunner/*
djangorestframework.egg-info/*
MANIFEST
.project
.pydevproject
.settings
.cache
.coverage
.tox
.DS_Store

View File

@ -1 +0,0 @@
6865c2156447754e1c5d21f11faa7bd15112cc25 0.1

23
AUTHORS
View File

@ -1,4 +1,4 @@
Tom Christie <tomchristie> - tom@tomchristie.com, @thisneonsoul
Tom Christie <tomchristie> - tom@tomchristie.com, @_tomchristie
Marko Tibold <markotibold> (Additional thanks for providing & managing the Jenkins CI Server)
Paul Bagwell <pbgwl>
Sébastien Piquemal <sebpiq>
@ -12,12 +12,31 @@ Andrew Straw <astraw>
Zeth <zeth>
Fernando Zunino <fzunino>
Jens Alm <ulmus>
Craig Blaszczyk <jakul>
Craig Blaszczyk <jakul>
Garcia Solero <garciasolero>
Tom Drummond <devioustree>
Danilo Bargen <gwrtheyrn>
Andrew McCloud <amccloud>
Thomas Steinacher <thomasst>
Meurig Freeman <meurig>
Anthony Nemitz <anemitz>
Ewoud Kohl van Wijngaarden <ekohl>
Michael Ding <yandy>
Mjumbe Poe <mjumbewu>
Natim <natim>
Sebastian Żurek <sebzur>
Benoit C <dzen>
Chris Pickett <bunchesofdonald>
Ben Timby <btimby>
Michele Lazzeri <michelelazzeri-nextage>
Camille Harang <mammique>
Paul Oswald <poswald>
Sean C. Farley <scfarley>
Daniel Izquierdo <izquierdo>
Can Yavuz <tschan>
Shawn Lewis <shawnlewis>
Adam Ness <greylurk>
<yetist>
THANKS TO:

107
CHANGELOG.rst Normal file
View File

@ -0,0 +1,107 @@
Release Notes
=============
0.4.0-dev
---------
* Markdown < 2.0 is no longer supported.
0.3.3
-----
* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
* Use `staticfiles` for css files.
- Easier to override. Won't conflict with customised admin styles (eg grappelli)
* Templates are now nicely namespaced.
- Allows easier overriding.
* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- Too magical. Explict is better than implicit.
* Saner template variable autoescaping.
* Tider setup.py
* Updated for URLObject 2.0
* Bugfixes:
- Bug with PerUserThrottling when user contains unicode chars.
0.3.2
-----
* Bugfixes:
* Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
* serialize_model method in serializer.py may cause wrong value (#73)
* Fix Error when clicking OPTIONS button (#146)
* And many other fixes
* Remove short status codes
- Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
* get_name, get_description become methods on the view - makes them overridable.
* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
0.3.1
-----
* [not documented]
0.3.0
-----
* JSONP Support
* Bugfixes, including support for latest markdown release
0.2.4
-----
* Fix broken IsAdminUser permission.
* OPTIONS support.
* XMLParser.
* Drop mentions of Blog, BitBucket.
0.2.3
-----
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
0.2.2
-----
* Throttling support complete.
0.2.1
-----
* Couple of simple bugfixes over 0.2.0
0.2.0
-----
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
Use ``self.user`` inside a view to access the authenticated user.
* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
* ``ResponseException`` becomes ``ErrorResponse``.
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
You can reuse these mixin classes individually without using the ``View`` class.
0.1.1
-----
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
0.1.0
-----
* Initial release.

21
LICENSE
View File

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

View File

@ -1,5 +1,5 @@
recursive-include djangorestframework/static *.ico *.txt
recursive-include djangorestframework/static *.ico *.txt *.css
recursive-include djangorestframework/templates *.txt *.html
recursive-include examples .keep *.py *.txt
recursive-include docs *.py *.rst *.html *.txt
include AUTHORS LICENSE requirements.txt tox.ini
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini

View File

@ -1,13 +1,18 @@
Django REST framework
=====================
Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.
**Django REST framework makes it easy to build well-connected, self-describing RESTful Web APIs.**
**Author:** Tom Christie. `Follow me on Twitter <https://twitter.com/_tomchristie>`_.
Overview
========
Features:
* Creates awesome self-describing *web browse-able* APIs.
* Clean, modular design, using Django's class based views.
* Easily extended for custom content types, serialization formats and authentication policies.
* Easily extended for custom content types, serialization formats and authentication policies.
* Stable, well tested code-base.
* Active developer community.
@ -16,10 +21,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>`_.
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/djangorestframework1/>`_ which runs our test suite.
Requirements:
* Python (2.5, 2.6, 2.7 supported)
* Django (1.2, 1.3 supported)
* Django (1.2, 1.3, 1.4 supported)
Installation Notes
@ -30,15 +37,10 @@ To clone the project from GitHub using git::
git clone git@github.com:tomchristie/django-rest-framework.git
To clone the project from Bitbucket using mercurial::
hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework
To install django-rest-framework in a virtualenv environment::
cd django-rest-framework
virtualenv --no-site-packages --distribute --python=python2.6 env
virtualenv --no-site-packages --distribute env
source env/bin/activate
pip install -r requirements.txt # django, coverage
@ -79,54 +81,3 @@ To run the tests against the full set of supported configurations::
To create the sdist packages::
python setup.py sdist --formats=gztar,zip
Release Notes
=============
0.2.3
* Fix some throttling bugs.
* ``X-Throttle`` header on throttling.
* Support for nesting resources on related models.
0.2.2
* Throttling support complete.
0.2.1
* Couple of simple bugfixes over 0.2.0
0.2.0
* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
Use ``self.user`` inside a view to access the authenticated user.
* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
* ``ResponseException`` becomes ``ErrorResponse``.
* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
You can reuse these mixin classes individually without using the ``View`` class.
0.1.1
* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
0.1.0
* Initial release.

View File

@ -1,3 +1,3 @@
__version__ = '0.2.3'
__version__ = '0.4.0-dev'
VERSION = __version__ # synonym

View File

@ -8,8 +8,7 @@ The set of authentication methods which are used is then specified by setting th
"""
from django.contrib.auth import authenticate
from django.middleware.csrf import CsrfViewMiddleware
from djangorestframework.utils import as_tuple
from djangorestframework.compat import CsrfViewMiddleware
import base64
__all__ = (
@ -33,13 +32,13 @@ class BaseAuthentication(object):
def authenticate(self, request):
"""
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
.. [*] The authentication context *will* typically be a :obj:`User`,
but it need not be. It can be any user-like object so long as the
permissions classes (see the :mod:`permissions` module) on the view can
handle the object and use it to determine if the request has the required
permissions or not.
permissions or not.
This can be an important distinction if you're implementing some token
based authentication mechanism, where the authentication context
may be more involved than simply mapping to a :obj:`User`.
@ -55,10 +54,10 @@ class BasicAuthentication(BaseAuthentication):
def authenticate(self, request):
"""
Returns a :obj:`User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns :const:`None`.
using HTTP Basic authentication. Otherwise returns :const:`None`.
"""
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2 and auth[0].lower() == "basic":
@ -66,17 +65,17 @@ class BasicAuthentication(BaseAuthentication):
auth_parts = base64.b64decode(auth[1]).partition(':')
except TypeError:
return None
try:
uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
except DjangoUnicodeDecodeError:
return None
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
return user
return None
class UserLoggedInAuthentication(BaseAuthentication):
"""
@ -88,18 +87,14 @@ class UserLoggedInAuthentication(BaseAuthentication):
Returns a :obj:`User` if the request session currently has a logged in user.
Otherwise returns :const:`None`.
"""
# TODO: Switch this back to request.POST, and let FormParser/MultiPartParser deal with the consequences.
self.view.DATA # Make sure our generic parsing runs first
if getattr(request, 'user', None) and request.user.is_active:
# If this is a POST request we enforce CSRF validation.
if request.method.upper() == 'POST':
# Temporarily replace request.POST with .DATA,
# so that we use our more generic request parsing
request._post = self.view.DATA
resp = CsrfViewMiddleware().process_view(request, None, (), {})
del(request._post)
if resp is not None: # csrf failed
return None
return request.user
# Enforce CSRF validation for session based authentication.
resp = CsrfViewMiddleware().process_view(request, None, (), {})
if resp is None: # csrf passed
return request.user
return None

View File

@ -1,48 +1,49 @@
"""
The :mod:`compatibility ` 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:
import cStringIO as StringIO
except ImportError:
import StringIO
# parse_qs
# parse_qs from 'urlparse' module unless python 2.5, in which case from 'cgi'
try:
# python >= ?
# python >= 2.6
from urlparse import parse_qs
except ImportError:
# python <= ?
# python < 2.6
from cgi import parse_qs
# django.test.client.RequestFactory (Django >= 1.3)
# django.test.client.RequestFactory (Required for Django < 1.3)
try:
from django.test.client import RequestFactory
except ImportError:
from django.test import Client
from django.core.handlers.wsgi import WSGIRequest
# From: http://djangosnippets.org/snippets/963/
# Lovely stuff
class RequestFactory(Client):
"""
Class that lets you create mock :obj:`Request` objects for use in testing.
Usage::
rf = RequestFactory()
get_request = rf.get('/hello/')
post_request = rf.post('/submit/', {'foo': 'bar'})
This class re-uses the :class:`django.test.client.Client` interface. Of which
you can find the docs here__.
__ 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.
"""
def request(self, **request):
@ -68,30 +69,30 @@ except ImportError:
try:
from django.views.generic import View
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
class ViewPlusHead(View):
def head(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
View = ViewPlusHead
except ImportError:
from django import http
from django.utils.functional import update_wrapper
# from django.utils.log import getLogger
# from django.utils.decorators import classonlymethod
# 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
class View(object):
"""
Intentionally simple parent class for all views. Only implements
dispatch-by-method and simple sanity checking.
"""
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
def __init__(self, **kwargs):
"""
Constructor. Called in the URLconf; can contain helpful extra
@ -101,7 +102,7 @@ except ImportError:
# instance, or raise an error.
for key, value in kwargs.iteritems():
setattr(self, key, value)
# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
@classmethod
def as_view(cls, **initkwargs):
@ -117,19 +118,19 @@ except ImportError:
if not hasattr(cls, key):
raise TypeError(u"%s() received an invalid keyword %r" % (
cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
def dispatch(self, request, *args, **kwargs):
# 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
@ -142,7 +143,7 @@ except ImportError:
self.args = args
self.kwargs = kwargs
return handler(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)]
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
@ -156,24 +157,237 @@ except ImportError:
def head(self, 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
try:
import markdown
import re
if markdown.version_info < (2, 0):
raise ImportError('Markdown < 2.0 is not supported.')
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.
"""
import re
# Detect Setext-style header. Must be first 2 lines of block.
RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
def test(self, parent, block):
return bool(self.RE.match(block))
def run(self, parent, blocks):
lines = blocks.pop(0).split('\n')
# Determine level. ``=`` is 1 and ``-`` is 2.
@ -186,21 +400,25 @@ try:
if len(lines) > 2:
# Block contains additional lines. Add to master blocks for later.
blocks.insert(0, '\n'.join(lines[2:]))
def apply_markdown(text):
"""
Simple wrapper around :func:`markdown.markdown` to apply our :class:`CustomSetextHeaderProcessor`,
and also set the base level of '#' style headers to <h2>.
Simple wrapper around :func:`markdown.markdown` to set the base level
of '#' style headers to <h2>.
"""
extensions = ['headerid(level=2)']
safe_mode = False,
output_format = markdown.DEFAULT_OUTPUT_FORMAT
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
safe_mode=safe_mode,
if markdown.version_info < (2, 1):
output_format = markdown.DEFAULT_OUTPUT_FORMAT
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
safe_mode=safe_mode,
output_format=output_format)
md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
else:
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text)
except ImportError:
@ -211,3 +429,37 @@ try:
import yaml
except ImportError:
yaml = None
import unittest
try:
import unittest.skip
except ImportError: # python < 2.7
from unittest import TestCase
import functools
def skip(reason):
# Pasted from py27/lib/unittest/case.py
"""
Unconditionally skip a test.
"""
def decorator(test_item):
if not (isinstance(test_item, type) and issubclass(test_item, TestCase)):
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
pass
test_item = skip_wrapper
test_item.__unittest_skip__ = True
test_item.__unittest_skip_why__ = reason
return test_item
return decorator
unittest.skip = skip
# xml.etree.parse only throws ParseError for python >= 2.7
try:
from xml.etree import ParseError as ETParseError
except ImportError: # python < 2.7
ETParseError = None

View File

@ -1,23 +1,21 @@
"""
The :mod:`mixins` module provides a set of reusable `mixin`
The :mod:`mixins` module provides a set of reusable `mixin`
classes that can be added to a `View`.
"""
from django.contrib.auth.models import AnonymousUser
from django.db.models.query import QuerySet
from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey
from django.http import HttpResponse
from urlobject import URLObject
from djangorestframework import status
from djangorestframework.parsers import FormParser, MultiPartParser
from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
from decimal import Decimal
import re
from StringIO import StringIO
@ -27,14 +25,13 @@ __all__ = (
'ResponseMixin',
'AuthMixin',
'ResourceMixin',
# Reverse URL lookup behavior
'InstanceMixin',
# Model behavior mixins
'ReadModelMixin',
'CreateModelMixin',
'UpdateModelMixin',
'DeleteModelMixin',
'ListModelMixin'
'ListModelMixin',
'PaginatorMixin'
)
@ -50,12 +47,12 @@ class RequestMixin(object):
_CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content'
parsers = ()
"""
The set of request parsers that the view can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module.
"""
parsers = ()
@property
def method(self):
@ -69,7 +66,6 @@ class RequestMixin(object):
self._load_method_and_content_type()
return self._method
@property
def content_type(self):
"""
@ -83,7 +79,6 @@ class RequestMixin(object):
self._load_method_and_content_type()
return self._content_type
@property
def DATA(self):
"""
@ -96,7 +91,6 @@ class RequestMixin(object):
self._load_data_and_files()
return self._data
@property
def FILES(self):
"""
@ -108,7 +102,6 @@ class RequestMixin(object):
self._load_data_and_files()
return self._files
def _load_data_and_files(self):
"""
Parse the request content into self.DATA and self.FILES.
@ -119,7 +112,6 @@ class RequestMixin(object):
if not hasattr(self, '_data'):
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
def _load_method_and_content_type(self):
"""
Set the method and content_type, and then check if they've been overridden.
@ -128,7 +120,6 @@ class RequestMixin(object):
self._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
def _get_stream(self):
"""
Returns an object that may be used to stream the request content.
@ -145,10 +136,9 @@ class RequestMixin(object):
if content_length == 0:
return None
elif hasattr(request, 'read'):
return request
return request
return StringIO(request.raw_post_data)
def _perform_form_overloading(self):
"""
If this is a form POST request, then we need to check if the method and content/content_type have been
@ -158,7 +148,7 @@ class RequestMixin(object):
# We only need to use form overloading on form POST requests.
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
return
# At this point we're committed to parsing the request as form data.
self._data = data = self.request.POST.copy()
self._files = self.request.FILES
@ -174,7 +164,6 @@ class RequestMixin(object):
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
(self._data, self._files) = self._parse(stream, self._content_type)
def _parse(self, stream, content_type):
"""
Parse the request content.
@ -192,10 +181,9 @@ class RequestMixin(object):
return parser.parse(stream)
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
{'error': 'Unsupported media type in request \'%s\'.' %
{'error': 'Unsupported media type in request \'%s\'.' %
content_type})
@property
def _parsed_media_types(self):
"""
@ -203,22 +191,20 @@ class RequestMixin(object):
"""
return [parser.media_type for parser in self.parsers]
@property
def _default_parser(self):
"""
Return the view's default parser class.
"""
"""
return self.parsers[0]
########## ResponseMixin ##########
class ResponseMixin(object):
"""
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
Default behavior is to use standard HTTP Accept header content negotiation.
Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL.
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
@ -227,13 +213,19 @@ class ResponseMixin(object):
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
_IGNORE_IE_ACCEPT_HEADER = True
renderers = ()
"""
The set of response renderers that the view can handle.
Should be a tuple/list of classes as described in the :mod:`renderers` module.
"""
renderers = ()
Should be a tuple/list of classes as described in the :mod:`renderers` module.
"""
def get_renderers(self):
"""
Return an iterable of available renderers. Override if you want to change
this list at runtime, say depending on what settings you have enabled.
"""
return self.renderers
# TODO: wrap this behavior around dispatch(), ensuring it works
# out of the box with existing Django classes that use render_to_response.
@ -253,7 +245,7 @@ class ResponseMixin(object):
# Set the media type of the response
# Note that the renderer *could* override it in .render() if required.
response.media_type = renderer.media_type
# Serialize the response content
if response.has_content_body:
content = renderer.render(response.cleaned_content, media_type)
@ -267,7 +259,6 @@ class ResponseMixin(object):
return resp
def _determine_renderer(self, request):
"""
Determines the appropriate renderer for the output, given the client's 'Accept' header,
@ -282,13 +273,14 @@ class ResponseMixin(object):
# Use _accept parameter override
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
elif (self._IGNORE_IE_ACCEPT_HEADER and
request.META.has_key('HTTP_USER_AGENT') and
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
'HTTP_USER_AGENT' in request.META and
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and
request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'):
# Ignore MSIE's broken accept behavior and do something sensible instead
accept_list = ['text/html', '*/*']
elif request.META.has_key('HTTP_ACCEPT'):
elif 'HTTP_ACCEPT' in request.META:
# Use standard HTTP Accept negotiation
accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')]
accept_list = [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')]
else:
# No accept header specified
accept_list = ['*/*']
@ -297,7 +289,7 @@ class ResponseMixin(object):
# attempting more specific media types first
# NB. The inner loop here isn't as bad as it first looks :)
# Worst case is we're looping over len(accept_list) * len(self.renderers)
renderers = [renderer_cls(self) for renderer_cls in self.renderers]
renderers = [renderer_cls(self) for renderer_cls in self.get_renderers()]
for accepted_media_type_lst in order_by_precedence(accept_list):
for renderer in renderers:
@ -310,14 +302,13 @@ class ResponseMixin(object):
{'detail': 'Could not satisfy the client\'s Accept header',
'available_types': self._rendered_media_types})
@property
def _rendered_media_types(self):
"""
Return an list of all the media types that this view can render.
"""
return [renderer.media_type for renderer in self.renderers]
@property
def _rendered_formats(self):
"""
@ -339,33 +330,31 @@ class AuthMixin(object):
"""
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
"""
authentication = ()
"""
The set of authentication types that this view can handle.
Should be a tuple/list of classes as described in the :mod:`authentication` module.
"""
authentication = ()
Should be a tuple/list of classes as described in the :mod:`authentication` module.
"""
permissions = ()
"""
The set of permissions that will be enforced on this view.
Should be a tuple/list of classes as described in the :mod:`permissions` module.
"""
permissions = ()
Should be a tuple/list of classes as described in the :mod:`permissions` module.
"""
@property
def user(self):
"""
Returns the :obj:`user` for the current request, as determined by the set of
:class:`authentication` classes applied to the :class:`View`.
:class:`authentication` classes applied to the :class:`View`.
"""
if not hasattr(self, '_user'):
self._user = self._authenticate()
return self._user
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication class in turn.
@ -378,7 +367,6 @@ class AuthMixin(object):
return user
return AnonymousUser()
# TODO: wrap this behavior around dispatch()
def _check_permissions(self):
"""
@ -451,60 +439,108 @@ class ResourceMixin(object):
return self._resource.filter_response(obj)
def get_bound_form(self, content=None, method=None):
return self._resource.get_bound_form(content, method=method)
##########
class InstanceMixin(object):
"""
`Mixin` class that is used to identify a `View` class as being the canonical identifier
for the resources it is mapped to.
"""
@classmethod
def as_view(cls, **initkwargs):
"""
Store the callable object on the resource class that has been associated with this view.
"""
view = super(InstanceMixin, cls).as_view(**initkwargs)
resource = getattr(cls(**initkwargs), 'resource', None)
if resource:
# We do a little dance when we store the view callable...
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
# as a function when we later look it up (rather than turning it into a method).
# This makes sure our URL reversing works ok.
resource.view_callable = (view,)
return view
if hasattr(self._resource, 'get_bound_form'):
return self._resource.get_bound_form(content, method=method)
else:
return None
########## Model Mixins ##########
class ReadModelMixin(object):
class ModelMixin(object):
""" Implements mechanisms used by other classes (like *ModelMixin group) to
define a query that represents Model instances the Mixin is working with.
If a *ModelMixin is going to retrive an instance (or queryset) using args and kwargs
passed by as URL arguments, it should provied arguments to objects.get and objects.filter
methods wrapped in by `build_query`
If a *ModelMixin is going to create/update an instance get_instance_data
handles the instance data creation/preaparation.
"""
queryset = None
def get_query_kwargs(self, *args, **kwargs):
"""
Return a dict of kwargs that will be used to build the
model instance retrieval or to filter querysets.
"""
kwargs = dict(kwargs)
# If the URLconf includes a .(?P<format>\w+) pattern to match against
# a .json, .xml suffix, then drop the 'format' kwarg before
# constructing the query.
if BaseRenderer._FORMAT_QUERY_PARAM in kwargs:
del kwargs[BaseRenderer._FORMAT_QUERY_PARAM]
return kwargs
def get_instance_data(self, model, content, **kwargs):
"""
Returns the dict with the data for model instance creation/update.
Arguments:
- model: model class (django.db.models.Model subclass) to work with
- content: a dictionary with instance data
- kwargs: a dict of URL provided keyword arguments
The create/update queries are created basicly with the contet provided
with POST/PUT HTML methods and kwargs passed in the URL. This methods
simply merges the URL data and the content preaparing the ready-to-use
data dictionary.
"""
tmp = dict(kwargs)
for field in model._meta.fields:
if isinstance(field, ForeignKey) and field.name in tmp:
# translate 'related_field' kwargs into 'related_field_id'
tmp[field.name + '_id'] = tmp[field.name]
del tmp[field.name]
all_kw_args = dict(content.items() + tmp.items())
return all_kw_args
def get_instance(self, **kwargs):
"""
Get a model instance for read/update/delete requests.
"""
return self.get_queryset().get(**kwargs)
def get_queryset(self):
"""
Return the queryset for this view.
"""
return getattr(self.resource, 'queryset',
self.resource.model.objects.all())
def get_ordering(self):
"""
Return the ordering for this view.
"""
return getattr(self.resource, 'ordering', None)
class ReadModelMixin(ModelMixin):
"""
Behavior to read a `model` instance on GET requests
"""
def get(self, request, *args, **kwargs):
model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
else:
# Otherwise assume the kwargs uniquely identify the model
filtered_keywords = kwargs.copy()
if BaseRenderer._FORMAT_QUERY_PARAM in filtered_keywords:
del filtered_keywords[BaseRenderer._FORMAT_QUERY_PARAM]
self.model_instance = model.objects.get(**filtered_keywords)
self.model_instance = self.get_instance(**query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return self.model_instance
class CreateModelMixin(object):
class CreateModelMixin(ModelMixin):
"""
Behavior to create a `model` instance on POST requests
"""
@ -515,86 +551,66 @@ class CreateModelMixin(object):
content = dict(self.CONTENT)
m2m_data = {}
for field in model._meta.fields:
if isinstance(field, ForeignKey) and kwargs.has_key(field.name):
# translate 'related_field' kwargs into 'related_field_id'
kwargs[field.name + '_id'] = kwargs[field.name]
del kwargs[field.name]
for field in model._meta.many_to_many:
if content.has_key(field.name):
if field.name in content:
m2m_data[field.name] = (
field.m2m_reverse_field_name(), content[field.name]
)
del content[field.name]
all_kw_args = dict(content.items() + kwargs.items())
if args:
instance = model(pk=args[-1], **all_kw_args)
else:
instance = model(**all_kw_args)
instance = model(**self.get_instance_data(model, content, *args, **kwargs))
instance.save()
for fieldname in m2m_data:
manager = getattr(instance, fieldname)
if hasattr(manager, 'add'):
manager.add(*m2m_data[fieldname][1])
else:
data = {}
data[manager.source_field_name] = instance
for related_item in m2m_data[fieldname][1]:
data[m2m_data[fieldname][0]] = related_item
manager.through(**data).save()
headers = {}
if hasattr(instance, 'get_absolute_url'):
if hasattr(self.resource, 'url'):
headers['Location'] = self.resource(self).url(instance)
return Response(status.HTTP_201_CREATED, instance, headers)
class UpdateModelMixin(object):
class UpdateModelMixin(ModelMixin):
"""
Behavior to update a `model` instance on PUT requests
"""
def put(self, request, *args, **kwargs):
model = self.resource.model
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
# TODO: update on the url of a non-existing resource url doesn't work
# correctly at the moment - will end up with a new url
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
else:
# Otherwise assume the kwargs uniquely identify the model
self.model_instance = model.objects.get(**kwargs)
self.model_instance = self.get_instance(**query_kwargs)
for (key, val) in self.CONTENT.items():
setattr(self.model_instance, key, val)
except model.DoesNotExist:
self.model_instance = model(**self.CONTENT)
self.model_instance.save()
self.model_instance = model(**self.get_instance_data(model, self.CONTENT, *args, **kwargs))
self.model_instance.save()
return self.model_instance
class DeleteModelMixin(object):
class DeleteModelMixin(ModelMixin):
"""
Behavior to delete a `model` instance on DELETE requests
"""
def delete(self, request, *args, **kwargs):
model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
try:
if args:
# If we have any none kwargs then assume the last represents the primrary key
instance = model.objects.get(pk=args[-1], **kwargs)
else:
# Otherwise assume the kwargs uniquely identify the model
instance = model.objects.get(**kwargs)
instance = self.get_instance(**query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
@ -602,38 +618,119 @@ class DeleteModelMixin(object):
return
class ListModelMixin(object):
class ListModelMixin(ModelMixin):
"""
Behavior to list a set of `model` instances on GET requests
"""
# NB. Not obvious to me if it would be better to set this on the resource?
#
# Presumably it's more useful to have on the view, because that way you can
# have multiple views across different querysets mapping to the same resource.
#
# Perhaps it ought to be:
#
# 1) View.queryset
# 2) if None fall back to Resource.queryset
# 3) if None fall back to Resource.model.objects.all()
#
# Any feedback welcomed.
queryset = None
def get(self, request, *args, **kwargs):
model = self.resource.model
queryset = self.queryset if self.queryset is not None else model.objects.all()
if hasattr(self, 'resource'):
ordering = getattr(self.resource, 'ordering', None)
else:
ordering = None
queryset = self.get_queryset()
ordering = self.get_ordering()
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
queryset = queryset.filter(**query_kwargs)
if ordering:
args = as_tuple(ordering)
queryset = queryset.order_by(*args)
return queryset.filter(**kwargs)
queryset = queryset.order_by(*ordering)
return queryset
########## Pagination Mixins ##########
class PaginatorMixin(object):
"""
Adds pagination support to GET requests
Obviously should only be used on lists :)
A default limit can be set by setting `limit` on the object. This will also
be used as the maximum if the client sets the `limit` GET param
"""
limit = 20
def get_limit(self):
"""
Helper method to determine what the `limit` should be
"""
try:
limit = int(self.request.GET.get('limit', self.limit))
return min(limit, self.limit)
except ValueError:
return self.limit
def url_with_page_number(self, page_number):
"""
Constructs a url used for getting the next/previous urls
"""
url = URLObject(self.request.get_full_path())
url = url.set_query_param('page', str(page_number))
limit = self.get_limit()
if limit != self.limit:
url = url.set_query_param('limit', str(limit))
return url
def next(self, page):
"""
Returns a url to the next page of results (if any)
"""
if not page.has_next():
return None
return self.url_with_page_number(page.next_page_number())
def previous(self, page):
""" Returns a url to the previous page of results (if any) """
if not page.has_previous():
return None
return self.url_with_page_number(page.previous_page_number())
def serialize_page_info(self, page):
"""
This is some useful information that is added to the response
"""
return {
'next': self.next(page),
'page': page.number,
'pages': page.paginator.num_pages,
'per_page': self.get_limit(),
'previous': self.previous(page),
'total': page.paginator.count,
}
def filter_response(self, obj):
"""
Given the response content, paginate and then serialize.
The response is modified to include to useful data relating to the number
of objects, number of pages, next/previous urls etc. etc.
The serialised objects are put into `results` on this new, modified
response
"""
# We don't want to paginate responses for anything other than GET requests
if self.method.upper() != 'GET':
return self._resource.filter_response(obj)
paginator = Paginator(obj, self.get_limit())
try:
page_num = int(self.request.GET.get('page', '1'))
except ValueError:
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
{'detail': 'That page contains no results'})
if page_num not in paginator.page_range:
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
{'detail': 'That page contains no results'})
page = paginator.page(page_num)
serialized_object_list = self._resource.filter_response(page.object_list)
serialized_page_info = self.serialize_page_info(page)
serialized_page_info['results'] = serialized_object_list
return serialized_page_info

View File

@ -19,6 +19,11 @@ from djangorestframework import status
from djangorestframework.compat import yaml
from djangorestframework.response import ErrorResponse
from djangorestframework.utils.mediatypes import media_type_matches
from xml.etree import ElementTree as ET
from djangorestframework.compat import ETParseError
from xml.parsers.expat import ExpatError
import datetime
import decimal
__all__ = (
@ -28,6 +33,7 @@ __all__ = (
'FormParser',
'MultiPartParser',
'YAMLParser',
'XMLParser'
)
@ -45,15 +51,15 @@ class BaseParser(object):
in case the parser needs to access any metadata on the :obj:`View` object.
"""
self.view = view
def can_handle_request(self, 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*
argument against the :attr:`media_type` attribute set on the class to see if
they match.
This may be overridden to provide for other behavior, but typically you'll
instead want to just set the :attr:`media_type` attribute on the class.
"""
@ -88,28 +94,26 @@ class JSONParser(BaseParser):
{'detail': 'JSON parse error - %s' % unicode(exc)})
if yaml:
class YAMLParser(BaseParser):
class YAMLParser(BaseParser):
"""
Parses YAML-serialized data.
"""
media_type = 'application/yaml'
def parse(self, stream):
"""
Parses YAML-serialized data.
Returns a 2-tuple of `(data, files)`.
`data` will be an object which is the parsed content of the response.
`files` will always be `None`.
"""
media_type = 'application/yaml'
def parse(self, stream):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be an object which is the parsed content of the response.
`files` will always be `None`.
"""
try:
return (yaml.safe_load(stream), None)
except ValueError, exc:
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
{'detail': 'YAML parse error - %s' % unicode(exc)})
else:
YAMLParser = None
try:
return (yaml.safe_load(stream), None)
except (ValueError, yaml.parser.ParserError), exc:
content = {'detail': 'YAML parse error - %s' % unicode(exc)}
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content)
class PlainTextParser(BaseParser):
"""
@ -121,7 +125,7 @@ class PlainTextParser(BaseParser):
def parse(self, stream):
"""
Returns a 2-tuple of `(data, files)`.
`data` will simply be a string representing the body of the request.
`files` will always be `None`.
"""
@ -138,7 +142,7 @@ class FormParser(BaseParser):
def parse(self, stream):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be a :class:`QueryDict` containing all the form parameters.
`files` will always be :const:`None`.
"""
@ -156,21 +160,99 @@ class MultiPartParser(BaseParser):
def parse(self, stream):
"""
Returns a 2-tuple of `(data, files)`.
`data` will be a :class:`QueryDict` containing all the form parameters.
`files` will be a :class:`QueryDict` containing all the form files.
"""
upload_handlers = self.view.request._get_upload_handlers()
try:
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
return django_parser.parse()
except MultiPartParserError, exc:
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
{'detail': 'multipart parse error - %s' % unicode(exc)})
return django_parser.parse()
DEFAULT_PARSERS = ( JSONParser,
FormParser,
MultiPartParser )
if YAMLParser:
DEFAULT_PARSERS += ( YAMLParser, )
class XMLParser(BaseParser):
"""
XML parser.
"""
media_type = 'application/xml'
def parse(self, stream):
"""
Returns a 2-tuple of `(data, files)`.
`data` will simply be a string representing the body of the request.
`files` will always be `None`.
"""
try:
tree = ET.parse(stream)
except (ExpatError, ETParseError, ValueError), exc:
content = {'detail': 'XML parse error - %s' % unicode(exc)}
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content)
data = self._xml_convert(tree.getroot())
return (data, None)
def _xml_convert(self, element):
"""
convert the xml `element` into the corresponding python object
"""
children = element.getchildren()
if len(children) == 0:
return self._type_convert(element.text)
else:
# if the fist child tag is list-item means all children are list-item
if children[0].tag == "list-item":
data = []
for child in children:
data.append(self._xml_convert(child))
else:
data = {}
for child in children:
data[child.tag] = self._xml_convert(child)
return data
def _type_convert(self, value):
"""
Converts the value returned by the XMl parse into the equivalent
Python type
"""
if value is None:
return value
try:
return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
except ValueError:
pass
try:
return int(value)
except ValueError:
pass
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
pass
return value
DEFAULT_PARSERS = (
JSONParser,
FormParser,
MultiPartParser,
XMLParser
)
if yaml:
DEFAULT_PARSERS += (YAMLParser, )
else:
YAMLParser = None

View File

@ -1,6 +1,6 @@
"""
The :mod:`permissions` module bundles a set of permission classes that are used
for checking if a request passes a certain set of constraints. You can assign a permission
The :mod:`permissions` module bundles a set of permission classes that are used
for checking if a request passes a certain set of constraints. You can assign a permission
class to your view by setting your View's :attr:`permissions` class attribute.
"""
@ -20,6 +20,8 @@ __all__ = (
'PerResourceThrottling'
)
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
_403_FORBIDDEN_RESPONSE = ErrorResponse(
status.HTTP_403_FORBIDDEN,
@ -40,7 +42,7 @@ class BasePermission(object):
Permission classes are always passed the current view on creation.
"""
self.view = view
def check_permission(self, auth):
"""
Should simply return, or raise an :exc:`response.ErrorResponse`.
@ -64,7 +66,7 @@ class IsAuthenticated(BasePermission):
def check_permission(self, user):
if not user.is_authenticated():
raise _403_FORBIDDEN_RESPONSE
raise _403_FORBIDDEN_RESPONSE
class IsAdminUser(BasePermission):
@ -82,10 +84,56 @@ class IsUserOrIsAnonReadOnly(BasePermission):
The request is authenticated as a user, or is a read-only request.
"""
def check_permission(self, user):
def check_permission(self, user):
if (not user.is_authenticated() and
self.view.method != 'GET' and
self.view.method != 'HEAD'):
self.view.method not in SAFE_METHODS):
raise _403_FORBIDDEN_RESPONSE
class DjangoModelPermissions(BasePermission):
"""
The request is authenticated using `django.contrib.auth` permissions.
See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions
It ensures that the user is authenticated, and has the appropriate
`add`/`change`/`delete` permissions on the model.
This permission should only be used on views with a `ModelResource`.
"""
# Map methods into required permission codes.
# Override this if you need to also provide 'read' permissions,
# or if you want to provide custom permission codes.
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def get_required_permissions(self, method, model_cls):
"""
Given a model and an HTTP method, return the list of permission
codes that the user is required to have.
"""
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name
}
try:
return [perm % kwargs for perm in self.perms_map[method]]
except KeyError:
ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED)
def check_permission(self, user):
method = self.view.method
model_cls = self.view.resource.model
perms = self.get_required_permissions(method, model_cls)
if not user.is_authenticated or not user.has_perms(perms):
raise _403_FORBIDDEN_RESPONSE
@ -100,7 +148,7 @@ class BaseThrottle(BasePermission):
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
Previous request information used for throttling is stored in the cache.
"""
"""
attr_name = 'throttle'
default = '0/sec'
@ -109,7 +157,7 @@ class BaseThrottle(BasePermission):
def get_cache_key(self):
"""
Should return a unique cache-key which can be used for throttling.
Muse be overridden.
Must be overridden.
"""
pass
@ -123,7 +171,7 @@ class BaseThrottle(BasePermission):
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
self.auth = auth
self.check_throttle()
def check_throttle(self):
"""
Implement the check to see if the request should be throttled.
@ -134,7 +182,7 @@ class BaseThrottle(BasePermission):
self.key = self.get_cache_key()
self.history = cache.get(self.key, [])
self.now = self.timer()
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
@ -153,7 +201,7 @@ class BaseThrottle(BasePermission):
cache.set(self.key, self.history, self.duration)
header = 'status=SUCCESS; next=%s sec' % self.next()
self.view.add_header('X-Throttle', header)
def throttle_failure(self):
"""
Called when a request to the API has failed due to throttling.
@ -162,7 +210,7 @@ class BaseThrottle(BasePermission):
header = 'status=FAILURE; next=%s sec' % self.next()
self.view.add_header('X-Throttle', header)
raise _503_SERVICE_UNAVAILABLE
def next(self):
"""
Returns the recommended next request time in seconds.
@ -188,7 +236,7 @@ class PerUserThrottling(BaseThrottle):
def get_cache_key(self):
if self.auth.is_authenticated():
ident = str(self.auth)
ident = self.auth.id
else:
ident = self.view.request.META.get('REMOTE_ADDR', None)
return 'throttle_user_%s' % ident
@ -205,7 +253,7 @@ class PerViewThrottling(BaseThrottle):
def get_cache_key(self):
return 'throttle_view_%s' % self.view.__class__.__name__
class PerResourceThrottling(BaseThrottle):
"""
Limits the rate of API calls that may be used against all views on

View File

@ -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,
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.conf import settings
@ -12,10 +12,9 @@ from django.template import RequestContext, loader
from django.utils import simplejson as json
from djangorestframework.compat import apply_markdown, yaml
from djangorestframework.compat import yaml
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.utils.description import get_name, get_description
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from djangorestframework import VERSION
@ -26,6 +25,7 @@ __all__ = (
'BaseRenderer',
'TemplateRenderer',
'JSONRenderer',
'JSONPRenderer',
'DocumentingHTMLRenderer',
'DocumentingXHTMLRenderer',
'DocumentingPlainTextRenderer',
@ -39,7 +39,7 @@ class BaseRenderer(object):
All renderers must extend this class, set the :attr:`media_type` attribute,
and override the :meth:`render` method.
"""
_FORMAT_QUERY_PARAM = 'format'
media_type = None
@ -81,7 +81,7 @@ class BaseRenderer(object):
"""
if obj is None:
return ''
return str(obj)
@ -113,6 +113,28 @@ class JSONRenderer(BaseRenderer):
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):
"""
Renderer which serializes to XML.
@ -130,25 +152,22 @@ class XMLRenderer(BaseRenderer):
return dict2xml(obj)
if yaml:
class YAMLRenderer(BaseRenderer):
"""
Renderer which serializes to YAML.
"""
media_type = 'application/yaml'
format = 'yaml'
def render(self, obj=None, media_type=None):
"""
Renders *obj* into serialized YAML.
"""
if obj is None:
return ''
class YAMLRenderer(BaseRenderer):
"""
Renderer which serializes to YAML.
"""
return yaml.dump(obj)
else:
YAMLRenderer = None
media_type = 'application/yaml'
format = 'yaml'
def render(self, obj=None, media_type=None):
"""
Renders *obj* into serialized YAML.
"""
if obj is None:
return ''
return yaml.safe_dump(obj)
class TemplateRenderer(BaseRenderer):
@ -192,7 +211,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
"""
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
renderers = [renderer for renderer in view.renderers if not isinstance(renderer, DocumentingTemplateRenderer)]
renderers = [renderer for renderer in view.renderers if not issubclass(renderer, DocumentingTemplateRenderer)]
if not renderers:
return '[No renderers were found]'
@ -200,9 +219,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
content = renderers[0](view).render(obj, media_type)
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
return content
return content
def _get_form_instance(self, view, method):
"""
@ -223,22 +241,21 @@ class DocumentingTemplateRenderer(BaseRenderer):
form_instance = view.get_bound_form(view.response.cleaned_content, method=method)
if form_instance and not form_instance.is_valid():
form_instance = None
except:
except Exception:
form_instance = None
# If we still don't have a form instance then try to get an unbound form
if not form_instance:
try:
form_instance = view.get_bound_form(method=method)
except:
except Exception:
pass
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
if not form_instance:
form_instance = self._get_generic_content_form(view)
return form_instance
return form_instance
def _get_generic_content_form(self, view):
"""
@ -275,6 +292,19 @@ class DocumentingTemplateRenderer(BaseRenderer):
# Okey doke, let's do it
return GenericContentForm(view)
def get_name(self):
try:
return self.view.get_name()
except AttributeError:
return self.view.__doc__
def get_description(self, html=None):
if html is None:
html = bool('html' in self.format)
try:
return self.view.get_description(html)
except AttributeError:
return self.view.__doc__
def render(self, obj=None, media_type=None):
"""
@ -296,15 +326,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
login_url = None
logout_url = None
name = get_name(self.view)
description = get_description(self.view)
markeddown = None
if apply_markdown:
try:
markeddown = apply_markdown(description)
except AttributeError:
markeddown = None
name = self.get_name()
description = self.get_description()
breadcrumb_list = get_breadcrumbs(self.view.request.path)
@ -312,23 +335,19 @@ class DocumentingTemplateRenderer(BaseRenderer):
context = RequestContext(self.view.request, {
'content': content,
'view': self.view,
'request': self.view.request, # TODO: remove
'request': self.view.request,
'response': self.view.response,
'description': description,
'name': name,
'version': VERSION,
'markeddown': markeddown,
'breadcrumblist': breadcrumb_list,
'available_formats': self.view._rendered_formats,
'put_form': put_form_instance,
'post_form': post_form_instance,
'login_url': login_url,
'logout_url': logout_url,
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
})
ret = template.render(context)
# Munge DELETE Response code to allow us to return content
@ -348,7 +367,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'text/html'
format = 'html'
template = 'renderer.html'
template = 'djangorestframework/api.html'
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
@ -360,7 +379,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
media_type = 'application/xhtml+xml'
format = 'xhtml'
template = 'renderer.html'
template = 'djangorestframework/api.html'
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
@ -372,14 +391,19 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
media_type = 'text/plain'
format = 'txt'
template = 'renderer.txt'
template = 'djangorestframework/api.txt'
DEFAULT_RENDERERS = ( JSONRenderer,
DocumentingHTMLRenderer,
DocumentingXHTMLRenderer,
DocumentingPlainTextRenderer,
XMLRenderer )
DEFAULT_RENDERERS = (
JSONRenderer,
JSONPRenderer,
DocumentingHTMLRenderer,
DocumentingXHTMLRenderer,
DocumentingPlainTextRenderer,
XMLRenderer
)
if YAMLRenderer:
DEFAULT_RENDERERS += (YAMLRenderer,)
if yaml:
DEFAULT_RENDERERS += (YAMLRenderer, )
else:
YAMLRenderer = None

View File

@ -1,24 +1,14 @@
from django import forms
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
from django.db import models
from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField
from django.utils.encoding import smart_unicode
from djangorestframework.response import ErrorResponse
from djangorestframework.serializer import Serializer, _SkipField
from djangorestframework.serializer import Serializer
from djangorestframework.utils import as_tuple
import decimal
import inspect
import re
class BaseResource(Serializer):
"""
Base class for all Resource classes, which simply defines the interface they provide.
Base class for all Resource classes, which simply defines the interface
they provide.
"""
fields = None
include = None
@ -27,14 +17,16 @@ class BaseResource(Serializer):
def __init__(self, view=None, depth=None, stack=[], **kwargs):
super(BaseResource, self).__init__(depth, stack, **kwargs)
self.view = view
self.request = getattr(view, 'request', None)
def validate_request(self, data, files=None):
"""
Given the request content return the cleaned, validated content.
Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
Typically raises a :exc:`response.ErrorResponse` with status code 400
(Bad Request) on failure.
"""
return data
def filter_response(self, obj):
"""
Given the response content, filter it into a serializable object.
@ -45,13 +37,14 @@ class BaseResource(Serializer):
class Resource(BaseResource):
"""
A Resource determines how a python object maps to some serializable data.
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
Objects that a resource can act on include plain Python object instances,
Django Models, and Django QuerySets.
"""
# The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model)
model = None
# By default the set of returned fields will be the set of:
#
# 0. All the fields on the model, excluding 'id'.
@ -78,13 +71,21 @@ class FormResource(Resource):
This can be overridden by a :attr:`form` attribute on the :class:`views.View`.
"""
allow_unknown_form_fields = False
"""
Flag to check for unknown fields when validating a form. If set to false and
we receive request data that is not expected by the form it raises an
:exc:`response.ErrorResponse` with status code 400. If set to true, only
expected fields are validated.
"""
def validate_request(self, data, files=None):
"""
Given some content as input return some cleaned, validated content.
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied.
Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied
if :attr:`self.allow_unknown_form_fields` is ``False``.
On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
@ -92,34 +93,33 @@ class FormResource(Resource):
"""
return self._validate(data, files)
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
"""
Wrapped by validate to hide the extra flags that are used in the implementation.
allowed_extra_fields is a list of fields which are not defined by the form, but which we still
expect to see on the input.
fake_data is a string that should be used as an extra key, as a kludge to force .errors
to be populated when an empty dict is supplied in `data`
"""
# We'd like nice error messages even if no content is supplied.
# Typically if an empty dict is given to a form Django will
# return .is_valid() == False, but .errors == {}
#
# To get around this case we revalidate with some fake data.
# To get around this case we revalidate with some fake data.
if fake_data:
data[fake_data] = '_fake_data'
allowed_extra_fields = tuple(allowed_extra_fields) + ('_fake_data',)
bound_form = self.get_bound_form(data, files)
if bound_form is None:
return data
self.view.bound_form_instance = bound_form
data = data and data or {}
files = files and files or {}
@ -130,9 +130,9 @@ class FormResource(Resource):
# 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
if bound_form.is_valid() and not unknown_fields:
if bound_form.is_valid() and (self.allow_unknown_form_fields or not unknown_fields):
# Validation succeeded...
cleaned_data = bound_form.cleaned_data
@ -155,7 +155,7 @@ class FormResource(Resource):
# If we've already set fake_dict and we're still here, fallback gracefully.
detail = {u'errors': [u'No content was supplied.']}
else:
else:
# Add any non-field errors
if bound_form.non_field_errors():
detail[u'errors'] = bound_form.non_field_errors()
@ -171,14 +171,13 @@ class FormResource(Resource):
# Add any unknown field errors
for key in unknown_fields:
field_errors[key] = [u'This field does not exist.']
if field_errors:
detail[u'field-errors'] = field_errors
detail[u'field_errors'] = field_errors
# Return HTTP 400 response (BAD REQUEST)
raise ErrorResponse(400, detail)
def get_form_class(self, method=None):
"""
Returns the form class used to validate this resource.
@ -199,7 +198,6 @@ class FormResource(Resource):
form = getattr(self.view, '%s_form' % method.lower(), form)
return form
def get_bound_form(self, data=None, files=None, method=None):
"""
@ -217,29 +215,12 @@ class FormResource(Resource):
return form()
#class _RegisterModelResource(type):
# """
# Auto register new ModelResource classes into ``_model_to_resource``
# """
# def __new__(cls, name, bases, dct):
# resource_cls = type.__new__(cls, name, bases, dct)
# model_cls = dct.get('model', None)
# if model_cls:
# _model_to_resource[model_cls] = resource_cls
# return resource_cls
class ModelResource(FormResource):
"""
Resource class that uses forms for validation and otherwise falls back to a model form if no form is set.
Also provides a :meth:`get_bound_form` method which may be used by some renderers.
"""
# Auto-register new ModelResource classes into _model_to_resource
#__metaclass__ = _RegisterModelResource
form = None
"""
The form class that should be used for request validation.
@ -258,23 +239,22 @@ class ModelResource(FormResource):
fields = None
"""
The list of fields to use on the output.
May be any of:
The name of a model field. To view nested resources, give the field as a tuple of ("fieldName", resource) where `resource` may be any of ModelResource reference, the name of a ModelResourc reference as a string or a tuple of strings representing fields on the nested model.
The name of an attribute on the model.
The name of an attribute on the resource.
The name of a method on the model, with a signature like ``func(self)``.
The name of a method on the resource, with a signature like ``func(self, instance)``.
"""
exclude = ('id', 'pk')
"""
The list of fields to exclude. This is only used if :attr:`fields` is not set.
"""
include = ('url',)
include = ()
"""
The list of extra fields to include. This is only used if :attr:`fields` is not set.
"""
@ -289,12 +269,11 @@ class ModelResource(FormResource):
self.model = getattr(view, 'model', None) or self.model
def validate_request(self, data, files=None):
"""
Given some content as input return some cleaned, validated content.
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
Validation is standard form or model form validation,
with an additional constraint that no extra unknown fields may be supplied,
and that all fields specified by the fields class attribute must be supplied,
@ -306,7 +285,6 @@ class ModelResource(FormResource):
"""
return self._validate(data, files, allowed_extra_fields=self._property_fields_set)
def get_bound_form(self, data=None, files=None, method=None):
"""
Given some content return a ``Form`` instance bound to that content.
@ -339,49 +317,6 @@ class ModelResource(FormResource):
return form()
def url(self, instance):
"""
Attempts to reverse resolve the url of the given model *instance* for this resource.
Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource.
This method can be overridden if you need to set the resource url reversing explicitly.
"""
if not hasattr(self, 'view_callable'):
raise _SkipField
# dis does teh magicks...
urlconf = get_urlconf()
resolver = get_resolver(urlconf)
possibilities = resolver.reverse_dict.getlist(self.view_callable[0])
for tuple_item in possibilities:
possibility = tuple_item[0]
# pattern = tuple_item[1]
# Note: defaults = tuple_item[2] for django >= 1.3
for result, params in possibility:
#instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ])
instance_attrs = {}
for param in params:
if not hasattr(instance, param):
continue
attr = getattr(instance, param)
if isinstance(attr, models.Model):
instance_attrs[param] = attr.pk
else:
instance_attrs[param] = attr
try:
return reverse(self.view_callable[0], kwargs=instance_attrs)
except NoReverseMatch:
pass
raise _SkipField
@property
def _model_fields_set(self):
"""
@ -389,11 +324,11 @@ class ModelResource(FormResource):
"""
model_fields = set(field.name for field in self.model._meta.fields)
if fields:
if self.fields:
return model_fields & set(as_tuple(self.fields))
return model_fields - set(as_tuple(self.exclude))
@property
def _property_fields_set(self):
"""

View File

@ -1,8 +1,8 @@
"""
The :mod:`response` module provides Response classes you can use in your
views to return a certain HTTP response. Typically a response is *rendered*
The :mod:`response` module provides Response classes you can use in your
views to return a certain HTTP response. Typically a response is *rendered*
into a HTTP response depending on what renderers are set on your view and
als depending on the accept header of the request.
als depending on the accept header of the request.
"""
from django.core.handlers.wsgi import STATUS_CODE_TEXT
@ -11,6 +11,7 @@ __all__ = ('Response', 'ErrorResponse')
# TODO: remove raw_content/cleaned_content and just use content?
class Response(object):
"""
An HttpResponse that may include content that hasn't yet been serialized.
@ -23,7 +24,7 @@ class Response(object):
self.raw_content = content # content prior to filtering
self.cleaned_content = content # content after filtering
self.headers = headers or {}
@property
def status_text(self):
"""
@ -33,7 +34,7 @@ class Response(object):
return STATUS_CODE_TEXT.get(self.status, '')
class ErrorResponse(BaseException):
class ErrorResponse(Exception):
"""
An exception representing an Response that should be returned immediately.
Any content should be serialized as-is, without being filtered.

View File

@ -0,0 +1,20 @@
"""
Provide reverse functions that return fully qualified URLs
"""
from django.core.urlresolvers import reverse as django_reverse
from django.utils.functional import lazy
def reverse(viewname, *args, **kwargs):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
"""
request = kwargs.pop('request', None)
url = django_reverse(viewname, *args, **kwargs)
if request:
return request.build_absolute_uri(url)
return url
reverse_lazy = lazy(reverse, str)

View File

@ -35,7 +35,6 @@ def main():
else:
test_runner = TestRunner()
failures = test_runner.run_tests(['djangorestframework'])
cov.stop()
# Discover the list of all modules that we should test coverage for
@ -51,10 +50,10 @@ def main():
# 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.
# (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:
files.remove('compat.py')
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files)

View File

@ -16,12 +16,12 @@ from django.test.utils import get_runner
def usage():
return """
Usage: python runtests.py [UnitTestClass].[method]
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.
"""
def main():
TestRunner = get_runner(settings)

View File

@ -53,11 +53,6 @@ MEDIA_ROOT = ''
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
@ -95,9 +90,16 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'djangorestframework',
'djangorestframework.tests',
)
STATIC_URL = '/static/'
import django
if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',)
# OAuth support is optional, so we only test oauth if it's installed.
try:
import oauth_provider

View File

@ -4,4 +4,4 @@ Blank URLConf just to keep runtests.py happy.
from django.conf.urls.defaults import *
urlpatterns = patterns('',
)
)

View File

@ -2,11 +2,9 @@
Customizable serialization.
"""
from django.db import models
from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField
from django.db.models.query import QuerySet, RawQuerySet
from django.utils.encoding import smart_unicode, is_protected_type, smart_str
import decimal
import inspect
import types
@ -18,23 +16,18 @@ _serializers = {}
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)):
return (field[0], field[1])
return (field, None)
def _fields_to_list(fields):
"""
Return a list of field names.
Return a list of field tuples.
"""
return [_field_to_tuple(field)[0] for field in fields or ()]
def _fields_to_dict(fields):
"""
Return a `dict` of field name -> None, or tuple of fields, or Serializer class
"""
return dict([_field_to_tuple(field) for field in fields or ()])
return [_field_to_tuple(field) for field in fields or ()]
class _SkipField(Exception):
@ -52,7 +45,7 @@ class _RegisterSerializer(type):
"""
def __new__(cls, name, bases, attrs):
# 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
return ret
@ -61,19 +54,19 @@ class Serializer(object):
"""
Converts python objects into plain old native types suitable for
serialization. In particular it handles models and querysets.
The output format is specified by setting a number of attributes
on the class.
You may also override any of the serialization methods, to provide
for more flexible behavior.
Valid output types include anything that may be directly rendered into
json, xml etc...
"""
__metaclass__ = _RegisterSerializer
fields = ()
fields = ()
"""
Specify the fields to be serialized on a model or dict.
Overrides `include` and `exclude`.
@ -104,17 +97,12 @@ class Serializer(object):
The maximum depth to serialize to, or `None`.
"""
def __init__(self, depth=None, stack=[], **kwargs):
if depth is not None:
self.depth = depth
self.stack = stack
def get_fields(self, obj):
"""
Return the set of field names/keys to use for a model instance/dict.
"""
fields = self.fields
# If `fields` is not set, we use the default fields and modify
@ -125,12 +113,8 @@ class Serializer(object):
exclude = self.exclude or ()
fields = set(default + list(include)) - set(exclude)
else:
fields = _fields_to_list(self.fields)
return fields
def get_default_fields(self, obj):
"""
Return the default list of field names/keys for a model instance/dict.
@ -142,15 +126,12 @@ class Serializer(object):
else:
return obj.keys()
def get_related_serializer(self, key):
info = _fields_to_dict(self.fields).get(key, None)
def get_related_serializer(self, info):
# If an element in `fields` is a 2-tuple of (str, tuple)
# then the second element of the tuple is the fields to
# set on the related serializer
if isinstance(info, (list, tuple)):
class OnTheFlySerializer(Serializer):
class OnTheFlySerializer(self.__class__):
fields = info
return OnTheFlySerializer
@ -168,11 +149,10 @@ class Serializer(object):
# Similar to what Django does for cyclically related models.
elif isinstance(info, str) and info in _serializers:
return _serializers[info]
# Otherwise use `related_serializer` or fall back to `Serializer`
return getattr(self, 'related_serializer') or Serializer
def serialize_key(self, key):
"""
Keys serialize to their string value,
@ -180,13 +160,12 @@ class Serializer(object):
"""
return self.rename.get(smart_str(key), smart_str(key))
def serialize_val(self, key, obj):
def serialize_val(self, key, obj, related_info):
"""
Convert a model field or dict value into a serializable representation.
"""
related_serializer = self.get_related_serializer(key)
related_serializer = self.get_related_serializer(related_info)
if self.depth is None:
depth = None
elif self.depth <= 0:
@ -200,8 +179,8 @@ class Serializer(object):
stack = self.stack[:]
stack.append(obj)
return related_serializer(depth=depth, stack=stack).serialize(obj)
return related_serializer(depth=depth, stack=stack).serialize(
obj, request=getattr(self, 'request', None))
def serialize_max_depth(self, obj):
"""
@ -210,7 +189,6 @@ class Serializer(object):
"""
raise _SkipField
def serialize_recursion(self, obj):
"""
Determine how objects should be serialized if recursion occurs.
@ -218,7 +196,6 @@ class Serializer(object):
"""
raise _SkipField
def serialize_model(self, instance):
"""
Given a model instance or dict, serialize it to a dict..
@ -227,69 +204,69 @@ class Serializer(object):
fields = self.get_fields(instance)
# serialize each required field
for fname in fields:
if hasattr(self, smart_str(fname)):
# check first for a method 'fname' on self first
meth = getattr(self, fname)
if inspect.ismethod(meth) and len(inspect.getargspec(meth)[0]) == 2:
obj = meth(instance)
elif hasattr(instance, '__contains__') and fname in instance:
# check for a key 'fname' on the instance
obj = instance[fname]
elif hasattr(instance, smart_str(fname)):
# finally check for an attribute 'fname' on the instance
obj = getattr(instance, fname)
else:
continue
# serialize each required field
for fname, related_info in _fields_to_list(fields):
try:
# we first check for a method 'fname' on self,
# 'fname's signature must be 'def fname(self, instance)'
meth = getattr(self, fname, None)
if (inspect.ismethod(meth) and
len(inspect.getargspec(meth)[0]) == 2):
obj = meth(instance)
elif hasattr(instance, '__contains__') and fname in instance:
# then check for a key 'fname' on the instance
obj = instance[fname]
elif hasattr(instance, smart_str(fname)):
# finally check for an attribute 'fname' on the instance
obj = getattr(instance, fname)
else:
continue
key = self.serialize_key(fname)
val = self.serialize_val(fname, obj)
val = self.serialize_val(fname, obj, related_info)
data[key] = val
except _SkipField:
pass
return data
def serialize_iter(self, obj):
"""
Convert iterables into a serializable representation.
"""
return [self.serialize(item) for item in obj]
def serialize_func(self, obj):
"""
Convert no-arg methods and functions into a serializable representation.
"""
return self.serialize(obj())
def serialize_manager(self, obj):
"""
Convert a model manager into a serializable representation.
"""
return self.serialize_iter(obj.all())
def serialize_fallback(self, obj):
"""
Convert any unhandled object into a serializable representation.
"""
return smart_unicode(obj, strings_only=True)
def serialize(self, obj):
def serialize(self, obj, request=None):
"""
Convert any object into a serializable representation.
"""
# Request from related serializer.
if request is not None:
self.request = request
if isinstance(obj, (dict, models.Model)):
# Model instances & dictionaries
return self.serialize_model(obj)
elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
elif isinstance(obj, (tuple, list, set, QuerySet, RawQuerySet, types.GeneratorType)):
# basic iterables
return self.serialize_iter(obj)
elif isinstance(obj, models.Manager):

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@ -5,7 +5,6 @@ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
"""
# Verbose format
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
@ -47,46 +46,3 @@ HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
# Short format
CONTINUE = 100
SWITCHING_PROTOCOLS = 101
OK = 200
CREATED = 201
ACCEPTED = 202
NON_AUTHORITATIVE_INFORMATION = 203
NO_CONTENT = 204
RESET_CONTENT = 205
PARTIAL_CONTENT = 206
MULTIPLE_CHOICES = 300
MOVED_PERMANENTLY = 301
FOUND = 302
SEE_OTHER = 303
NOT_MODIFIED = 304
USE_PROXY = 305
RESERVED = 306
TEMPORARY_REDIRECT = 307
BAD_REQUEST = 400
UNAUTHORIZED = 401
PAYMENT_REQUIRED = 402
FORBIDDEN = 403
NOT_FOUND = 404
METHOD_NOT_ALLOWED = 405
NOT_ACCEPTABLE = 406
PROXY_AUTHENTICATION_REQUIRED = 407
REQUEST_TIMEOUT = 408
CONFLICT = 409
GONE = 410
LENGTH_REQUIRED = 411
PRECONDITION_FAILED = 412
REQUEST_ENTITY_TOO_LARGE = 413
REQUEST_URI_TOO_LONG = 414
UNSUPPORTED_MEDIA_TYPE = 415
REQUESTED_RANGE_NOT_SATISFIABLE = 416
EXPECTATION_FAILED = 417
INTERNAL_SERVER_ERROR = 500
NOT_IMPLEMENTED = 501
BAD_GATEWAY = 502
SERVICE_UNAVAILABLE = 503
GATEWAY_TIMEOUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505

View File

@ -1,48 +0,0 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/login.css' />
<style>
.form-row {border-bottom: 0.25em !important}</style>
</head>
<body class="login">
<div id="container">
<div id="header">
<div id="branding">
<h1 id="site-name">Django REST framework</h1>
</div>
</div>
<div id="content" class="colM">
<div id="content-main">
<form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
{% csrf_token %}
<div class="form-row">
<label for="id_username">Username:</label> {{ form.username }}
</div>
<div class="form-row">
<label for="id_password">Password:</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
<div class="form-row">
<label>&nbsp;</label><input type="submit" value="Log in">
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
<br class="clear">
</div>
<div id="footer"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,3 @@
{% extends "djangorestframework/base.html" %}
{# Override this template in your own templates directory to customize #}

View File

@ -1,8 +1,8 @@
{{ name }}
{% autoescape off %}{{ name }}
{{ description }}
{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
HTTP/1.0 {{ response.status }} {{ response.status_text }}
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
{% endfor %}
{{ content }}{% endautoescape %}

View File

@ -1,43 +1,60 @@
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{% load urlize_quoted_links %}
{% load add_query_param %}
{% load static %}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
/* Override some of the Django admin styling */
#site-name a {color: #F4F379 !important;}
.errorlist {display: inline !important}
.errorlist li {display: inline !important; background: white !important; color: black !important; border: 0 !important;}
/* Custom styles */
.version{font-size:8px;}
</style>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/base.css'/>
<link rel="stylesheet" type="text/css" href='{{ADMIN_MEDIA_PREFIX}}css/forms.css'/>
<title>Django REST framework - {{ name }}</title>
</head>
<body>
<head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}djangorestframework/css/style.css'/>
{% block extrastyle %}{% endblock %}
<title>{% block title %}Django REST framework - {{ name }}{% endblock %}</title>
{% block extrahead %}{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
</head>
<body class="{% block bodyclass %}{% endblock %}">
<div id="container">
<div id="header">
<div id="branding">
<h1 id="site-name"><a href='http://django-rest-framework.org'>Django REST framework</a> <span class="version"> v {{ version }}</span></h1>
<h1 id="site-name">{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework</a> <span class="version"> v {{ version }}</span>{% endblock %}</h1>
</div>
<div id="user-tools">
{% if user.is_active %}Welcome, {{ user }}.{% if logout_url %} <a href='{{ logout_url }}'>Log out</a>{% endif %}{% else %}Anonymous {% if login_url %}<a href='{{ login_url }}'>Log in</a>{% endif %}{% endif %}
{% block userlinks %}
{% if user.is_active %}
Welcome, {{ user }}.
<a href='{% url djangorestframework:logout %}?next={{ request.path }}'>Log out</a>
{% else %}
Anonymous
<a href='{% url djangorestframework:login %}?next={{ request.path }}'>Log in</a>
{% endif %}
{% endblock %}
</div>
{% block nav-global %}{% endblock %}
</div>
<div class="breadcrumbs">
{% block breadcrumbs %}
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}&rsaquo;{% endif %}
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}&rsaquo;{% endif %}
{% endfor %}
{% endblock %}
</div>
<!-- Content -->
<div id="content" class="{% block coltype %}colM{% endblock %}">
{% if 'OPTIONS' in view.allowed_methods %}
<form action="{{ request.get_full_path }}" method="post">
{% csrf_token %}
<input type="hidden" name="{{ METHOD_PARAM }}" value="OPTIONS" />
<input type="submit" value="OPTIONS" class="default" />
</form>
{% endif %}
<div class='content-main'>
<h1>{{ name }}</h1>
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p>
<p>{{ description }}</p>
<div class='module'>
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
@ -59,8 +76,8 @@
</fieldset>
</form>
{% endif %}
{# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #}
{# Only display the POST/PUT/DELETE forms if method tunneling via POST forms is enabled and the user has permissions on this view. #}
{% if METHOD_PARAM and response.status != 403 %}
{% if 'POST' in view.allowed_methods %}
@ -83,7 +100,7 @@
</fieldset>
</form>
{% endif %}
{% if 'PUT' in view.allowed_methods %}
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
<fieldset class='module aligned'>
@ -96,23 +113,23 @@
{{ field.label_tag }}
{{ field }}
<span class='help'>{{ field.help_text }}</span>
{{ field.errors }}
{{ field.errors }}
</div>
{% endfor %}
<div class='submit-row' style='margin: 0; border: 0'>
<div class='submit-row' style='margin: 0; border: 0'>
<input type="submit" value="PUT" class="default" />
</div>
</fieldset>
</form>
{% endif %}
{% if 'DELETE' in view.allowed_methods %}
<form action="{{ request.get_full_path }}" method="post">
<fieldset class='module aligned'>
<fieldset class='module aligned'>
<h2>DELETE {{ name }}</h2>
{% csrf_token %}
<input type="hidden" name="{{ METHOD_PARAM }}" value="DELETE" />
<div class='submit-row' style='margin: 0; border: 0'>
<div class='submit-row' style='margin: 0; border: 0'>
<input type="submit" value="DELETE" class="default" />
</div>
</fieldset>
@ -121,7 +138,12 @@
{% endif %}
</div>
<!-- END content-main -->
</div>
<!-- END Content -->
{% block footer %}<div id="footer"></div>{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,44 @@
{% load static %}
<html>
<head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}djangorestframework/css/style.css'/>
</head>
<body class="login">
<div id="container">
<div id="header">
<div id="branding">
<h1 id="site-name">Django REST framework</h1>
</div>
</div>
<div id="content" class="colM">
<div id="content-main">
<form method="post" action="{% url djangorestframework:login %}" id="login-form">
{% csrf_token %}
<div class="form-row">
<label for="id_username">Username:</label> {{ form.username }}
</div>
<div class="form-row">
<label for="id_password">Password:</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
<div class="form-row">
<label>&nbsp;</label><input type="submit" value="Log in">
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
<br class="clear">
</div>
<div id="footer"></div>
</div>
</body>
</html>

View File

@ -1,17 +1,10 @@
from django.template import Library
from urlparse import urlparse, urlunparse
from urllib import quote
from urlobject import URLObject
register = Library()
def add_query_param(url, param):
(key, sep, val) = param.partition('=')
param = '%s=%s' % (key, quote(val))
(scheme, netloc, path, params, query, fragment) = urlparse(url)
if query:
query += "&" + param
else:
query = param
return urlunparse((scheme, netloc, path, params, query, fragment))
return unicode(URLObject(url).add_query_param(*param.split('=')))
register.filter('add_query_param', add_query_param)
register.filter('add_query_param', add_query_param)

View File

@ -1,6 +1,6 @@
"""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.
"""

View File

@ -1,3 +1,4 @@
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.views import View
@ -13,9 +14,19 @@ SAFARI_5_0_USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/5
OPERA_11_0_MSIE_USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00'
OPERA_11_0_OPERA_USER_AGENT = 'Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00'
urlpatterns = patterns('',
url(r'^api', include('djangorestframework.urls', namespace='djangorestframework'))
)
class UserAgentMungingTest(TestCase):
"""We need to fake up the accept headers when we deal with MSIE. Blergh.
http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
"""
We need to fake up the accept headers when we deal with MSIE. Blergh.
http://www.gethifi.com/blog/browser-rest-http-accept-headers
"""
urls = 'djangorestframework.tests.accept'
def setUp(self):
@ -39,6 +50,16 @@ class UserAgentMungingTest(TestCase):
resp = self.view(req)
self.assertEqual(resp['Content-Type'], 'text/html')
def test_dont_munge_msie_with_x_requested_with_header(self):
"""Send MSIE user agent strings, and an X-Requested-With header, and
ensure that we get a JSON response if we set a */* Accept header."""
for user_agent in (MSIE_9_USER_AGENT,
MSIE_8_USER_AGENT,
MSIE_7_USER_AGENT):
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
resp = self.view(req)
self.assertEqual(resp['Content-Type'], 'application/json')
def test_dont_rewrite_msie_accept_header(self):
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
that we get a JSON response if we set a */* accept header."""
@ -50,7 +71,7 @@ class UserAgentMungingTest(TestCase):
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
resp = view(req)
self.assertEqual(resp['Content-Type'], 'application/json')
def test_dont_munge_nice_browsers_accept_header(self):
"""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)"""
@ -63,6 +84,3 @@ class UserAgentMungingTest(TestCase):
resp = self.view(req)
self.assertEqual(resp['Content-Type'], 'application/json')

View File

@ -1,11 +1,9 @@
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.contrib.auth import login
from django.test import Client, TestCase
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
from djangorestframework.views import View
from djangorestframework import permissions
@ -13,9 +11,13 @@ import base64
class MockView(View):
permissions = ( permissions.IsAuthenticated, )
permissions = (permissions.IsAuthenticated,)
def post(self, request):
return {'a':1, 'b':2, 'c':3}
return {'a': 1, 'b': 2, 'c': 3}
def put(self, request):
return {'a': 1, 'b': 2, 'c': 3}
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
@ -31,7 +33,7 @@ class BasicAuthTests(TestCase):
self.username = 'john'
self.email = 'lennon@thebeatles.com'
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):
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
@ -66,25 +68,38 @@ class SessionAuthTests(TestCase):
self.username = 'john'
self.email = 'lennon@thebeatles.com'
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):
self.csrf_client.logout()
def test_post_form_session_auth_failing_csrf(self):
"""Ensure POSTing form over session authentication without CSRF token fails."""
"""
Ensure POSTing form over session authentication without CSRF token fails.
"""
self.csrf_client.login(username=self.username, password=self.password)
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
def test_post_form_session_auth_passing(self):
"""Ensure POSTing form over session authentication with logged in user and CSRF token passes."""
"""
Ensure POSTing form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password)
response = self.non_csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 200)
def test_put_form_session_auth_passing(self):
"""
Ensure PUTting form over session authentication with logged in user and CSRF token passes.
"""
self.non_csrf_client.login(username=self.username, password=self.password)
response = self.non_csrf_client.put('/', {'example': 'example'})
self.assertEqual(response.status_code, 200)
def test_post_form_session_auth_failing(self):
"""Ensure POSTing form over session authentication without logged in user fails."""
"""
Ensure POSTing form over session authentication without logged in user fails.
"""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)

View File

@ -64,4 +64,4 @@ class BreadcrumbTests(TestCase):
def test_broken_url_breadcrumbs_handled_gracefully(self):
url = '/foobar'
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])

View File

@ -6,7 +6,7 @@ from django.contrib.auth.models import User
from django.test import TestCase, Client
from djangorestframework import status
from djangorestframework.authentication import UserLoggedInAuthentication
from djangorestframework.compat import RequestFactory
from djangorestframework.compat import RequestFactory, unittest
from djangorestframework.mixins import RequestMixin
from djangorestframework.parsers import FormParser, MultiPartParser, \
PlainTextParser, JSONParser
@ -17,8 +17,8 @@ class MockView(View):
authentication = (UserLoggedInAuthentication,)
def post(self, request):
if request.POST.get('example') is not None:
return Response(status.OK)
return Response(status.HTTP_200_OK)
return Response(status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('',
@ -103,104 +103,105 @@ class TestContentParsing(TestCase):
view.request = self.req.post('/', form_data)
view.parsers = (PlainTextParser,)
self.assertEqual(view.DATA, content)
def test_accessing_post_after_data_form(self):
"""Ensures request.POST can be accessed after request.DATA in form request"""
form_data = {'qwerty': 'uiop'}
view = RequestMixin()
view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.DATA.items(), form_data.items())
self.assertEqual(view.request.POST.items(), form_data.items())
@unittest.skip('This test was disabled some time ago for some reason')
def test_accessing_post_after_data_for_json(self):
"""Ensures request.POST can be accessed after request.DATA in json request"""
from django.utils import simplejson as json
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
view = RequestMixin()
view.parsers = (JSONParser,)
view.request = self.req.post('/', content, content_type=content_type)
self.assertEqual(view.DATA.items(), data.items())
self.assertEqual(view.request.POST.items(), [])
def test_accessing_post_after_data_for_overloaded_json(self):
"""Ensures request.POST can be accessed after request.DATA in overloaded json request"""
from django.utils import simplejson as json
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
view = RequestMixin()
view.parsers = (JSONParser,)
form_data = {view._CONTENT_PARAM: content,
view._CONTENTTYPE_PARAM: content_type}
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.DATA.items(), data.items())
self.assertEqual(view.request.POST.items(), form_data.items())
def test_accessing_data_after_post_form(self):
"""Ensures request.DATA can be accessed after request.POST in form request"""
form_data = {'qwerty': 'uiop'}
view = RequestMixin()
view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.request.POST.items(), form_data.items())
self.assertEqual(view.DATA.items(), form_data.items())
def test_accessing_data_after_post_for_json(self):
"""Ensures request.DATA can be accessed after request.POST in json request"""
from django.utils import simplejson as json
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
view = RequestMixin()
view.parsers = (JSONParser,)
view.request = self.req.post('/', content, content_type=content_type)
post_items = view.request.POST.items()
self.assertEqual(len(post_items), 1)
self.assertEqual(len(post_items[0]), 2)
self.assertEqual(post_items[0][0], content)
self.assertEqual(view.DATA.items(), data.items())
def test_accessing_data_after_post_for_overloaded_json(self):
"""Ensures request.DATA can be accessed after request.POST in overloaded json request"""
from django.utils import simplejson as json
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
view = RequestMixin()
view.parsers = (JSONParser,)
form_data = {view._CONTENT_PARAM: content,
view._CONTENTTYPE_PARAM: content_type}
view.request = self.req.post('/', data=form_data)
self.assertEqual(view.request.POST.items(), form_data.items())
self.assertEqual(view.DATA.items(), data.items())
class TestContentParsingWithAuthentication(TestCase):
urls = 'djangorestframework.tests.content'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.username = 'john'
@ -208,25 +209,25 @@ class TestContentParsingWithAuthentication(TestCase):
self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password)
self.req = RequestFactory()
def test_user_logged_in_authentication_has_post_when_not_logged_in(self):
"""Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in"""
content = {'example': 'example'}
response = self.client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed")
response = self.csrf_client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
def test_user_logged_in_authentication_has_post_when_logged_in(self):
"""Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
self.client.login(username='john', password='password')
self.csrf_client.login(username='john', password='password')
content = {'example': 'example'}
response = self.client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
response = self.csrf_client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
self.assertEqual(status.HTTP_200_OK, response.status_code, "POST data is malformed")
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
# self.client.login(username='john', password='password')
# self.csrf_client.login(username='john', password='password')
# content = {'example': 'example'}
# response = self.client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
# response = self.csrf_client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")

View File

@ -1,7 +1,6 @@
from django.test import TestCase
from djangorestframework.views import View
from djangorestframework.compat import apply_markdown
from djangorestframework.utils.description import get_name, get_description
# We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring
@ -19,8 +18,11 @@ indented
# hash style header #"""
# If markdown is installed we also test it's working (and that our wrapped forces '=' to h2 and '-' to h3)
MARKED_DOWN = """<h2>an example docstring</h2>
# If markdown is installed we also test it's working
# (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>
<li>list</li>
<li>list</li>
@ -31,21 +33,32 @@ MARKED_DOWN = """<h2>an example docstring</h2>
<p>indented</p>
<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):
def test_resource_name_uses_classname_by_default(self):
"""Ensure Resource names are based on the classname by default."""
class MockView(View):
pass
self.assertEquals(get_name(MockView()), 'Mock')
self.assertEquals(MockView().get_name(), 'Mock')
# This has been turned off now.
#def test_resource_name_can_be_set_explicitly(self):
# """Ensure Resource names can be set using the 'name' class attribute."""
# example = 'Some Other Name'
# class MockView(View):
# name = example
# self.assertEquals(get_name(MockView()), example)
def test_resource_name_can_be_set_explicitly(self):
"""Ensure Resource names can be set using the 'get_name' method."""
example = 'Some Other Name'
class MockView(View):
def get_name(self):
return example
self.assertEquals(MockView().get_name(), example)
def test_resource_description_uses_docstring_by_default(self):
"""Ensure Resource names are based on the docstring by default."""
@ -55,41 +68,44 @@ class TestViewNamesAndDescriptions(TestCase):
* list
* list
another header
--------------
code block
indented
# hash style header #"""
self.assertEquals(get_description(MockView()), DESCRIPTION)
# This has been turned off now
#def test_resource_description_can_be_set_explicitly(self):
# """Ensure Resource descriptions can be set using the 'description' class attribute."""
# example = 'Some other description'
# class MockView(View):
# """docstring"""
# description = example
# self.assertEquals(get_description(MockView()), example)
#def test_resource_description_does_not_require_docstring(self):
# """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
# example = 'Some other description'
# class MockView(View):
# description = example
# self.assertEquals(get_description(MockView()), example)
# hash style header #"""
self.assertEquals(MockView().get_description(), DESCRIPTION)
def test_resource_description_can_be_set_explicitly(self):
"""Ensure Resource descriptions can be set using the 'get_description' method."""
example = 'Some other description'
class MockView(View):
"""docstring"""
def get_description(self):
return example
self.assertEquals(MockView().get_description(), example)
def test_resource_description_does_not_require_docstring(self):
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
example = 'Some other description'
class MockView(View):
def get_description(self):
return example
self.assertEquals(MockView().get_description(), example)
def test_resource_description_can_be_empty(self):
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
class MockView(View):
pass
self.assertEquals(get_description(MockView()), '')
self.assertEquals(MockView().get_description(), '')
def test_markdown(self):
"""Ensure markdown to HTML works as expected"""
if apply_markdown:
self.assertEquals(apply_markdown(DESCRIPTION), MARKED_DOWN)
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
self.assertTrue(gte_21_match or lt_21_match)

View File

@ -22,7 +22,7 @@ class UploadFilesTests(TestCase):
def post(self, request, *args, **kwargs):
return {'FILE_NAME': self.CONTENT['file'].name,
'FILE_CONTENT': self.CONTENT['file'].read()}
file = StringIO.StringIO('stuff')
file.name = 'stuff.txt'
request = self.factory.post('/', {'file': file})
@ -30,7 +30,3 @@ class UploadFilesTests(TestCase):
response = view(request)
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')

View File

@ -3,7 +3,7 @@ from djangorestframework.compat import RequestFactory
from djangorestframework.mixins import RequestMixin
class TestMethodOverloading(TestCase):
class TestMethodOverloading(TestCase):
def setUp(self):
self.req = RequestFactory()
@ -18,7 +18,7 @@ class TestMethodOverloading(TestCase):
view = RequestMixin()
view.request = self.req.post('/')
self.assertEqual(view.method, 'POST')
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
"""POST requests can be overloaded to another method by setting a reserved form field"""
view = RequestMixin()

View File

@ -1,17 +1,54 @@
"""Tests for the status module"""
"""Tests for the mixin module"""
from django.test import TestCase
from django.utils import simplejson as json
from djangorestframework import status
from djangorestframework.compat import RequestFactory
from django.contrib.auth.models import Group, User
from djangorestframework.mixins import CreateModelMixin
from djangorestframework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin
from djangorestframework.resources import ModelResource
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.tests.models import CustomUser
from djangorestframework.tests.testcases import TestModelsTestCase
from djangorestframework.views import View
class TestModelCreation(TestCase):
class TestModelRead(TestModelsTestCase):
"""Tests on ReadModelMixin"""
def setUp(self):
super(TestModelRead, self).setUp()
self.req = RequestFactory()
def test_read(self):
Group.objects.create(name='other group')
group = Group.objects.create(name='my group')
class GroupResource(ModelResource):
model = Group
request = self.req.get('/groups')
mixin = ReadModelMixin()
mixin.resource = GroupResource
response = mixin.get(request, id=group.id)
self.assertEquals(group.name, response.name)
def test_read_404(self):
class GroupResource(ModelResource):
model = Group
request = self.req.get('/groups')
mixin = ReadModelMixin()
mixin.resource = GroupResource
self.assertRaises(ErrorResponse, mixin.get, request, id=12345)
class TestModelCreation(TestModelsTestCase):
"""Tests on CreateModelMixin"""
def setUp(self):
super(TestModelsTestCase, self).setUp()
self.req = RequestFactory()
def test_creation(self):
@ -25,23 +62,26 @@ class TestModelCreation(TestCase):
mixin = CreateModelMixin()
mixin.resource = GroupResource
mixin.CONTENT = form_data
response = mixin.post(request)
self.assertEquals(1, Group.objects.count())
self.assertEquals('foo', response.cleaned_content.name)
def test_creation_with_m2m_relation(self):
class UserResource(ModelResource):
model = User
def url(self, instance):
return "/users/%i" % instance.id
group = Group(name='foo')
group.save()
form_data = {'username': 'bar', 'password': 'baz', 'groups': [group.id]}
form_data = {
'username': 'bar',
'password': 'baz',
'groups': [group.id]
}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group]
@ -53,18 +93,18 @@ class TestModelCreation(TestCase):
self.assertEquals(1, User.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
def test_creation_with_m2m_relation_through(self):
"""
Tests creation where the m2m relation uses a through table
"""
class UserResource(ModelResource):
model = CustomUser
def url(self, instance):
return "/customusers/%i" % instance.id
form_data = {'username': 'bar0', 'groups': []}
form_data = {'username': 'bar0', 'groups': []}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = []
@ -74,12 +114,12 @@ class TestModelCreation(TestCase):
response = mixin.post(request)
self.assertEquals(1, CustomUser.objects.count())
self.assertEquals(0, response.cleaned_content.groups.count())
self.assertEquals(0, response.cleaned_content.groups.count())
group = Group(name='foo1')
group.save()
form_data = {'username': 'bar1', 'groups': [group.id]}
form_data = {'username': 'bar1', 'groups': [group.id]}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group]
@ -91,12 +131,11 @@ class TestModelCreation(TestCase):
self.assertEquals(2, CustomUser.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
group2 = Group(name='foo2')
group2.save()
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
group2.save()
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data)
cleaned_data['groups'] = [group, group2]
@ -109,5 +148,144 @@ class TestModelCreation(TestCase):
self.assertEquals(2, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].name)
class MockPaginatorView(PaginatorMixin, View):
total = 60
def get(self, request):
return range(0, self.total)
def post(self, request):
return Response(status.HTTP_201_CREATED, {'status': 'OK'})
class TestPagination(TestCase):
def setUp(self):
self.req = RequestFactory()
def test_default_limit(self):
""" Tests if pagination works without overwriting the limit """
request = self.req.get('/paginator')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(MockPaginatorView.total, content['total'])
self.assertEqual(MockPaginatorView.limit, content['per_page'])
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
def test_overwriting_limit(self):
""" Tests if the limit can be overwritten """
limit = 10
request = self.req.get('/paginator')
response = MockPaginatorView.as_view(limit=limit)(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(content['per_page'], limit)
self.assertEqual(range(0, limit), content['results'])
def test_limit_param(self):
""" Tests if the client can set the limit """
from math import ceil
limit = 5
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
request = self.req.get('/paginator/?limit=%d' % limit)
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(MockPaginatorView.total, content['total'])
self.assertEqual(limit, content['per_page'])
self.assertEqual(num_pages, content['pages'])
def test_exceeding_limit(self):
""" Makes sure the client cannot exceed the default limit """
from math import ceil
limit = MockPaginatorView.limit + 10
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
request = self.req.get('/paginator/?limit=%d' % limit)
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(MockPaginatorView.total, content['total'])
self.assertNotEqual(limit, content['per_page'])
self.assertNotEqual(num_pages, content['pages'])
self.assertEqual(MockPaginatorView.limit, content['per_page'])
def test_only_works_for_get(self):
""" Pagination should only work for GET requests """
request = self.req.post('/paginator', data={'content': 'spam'})
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(None, content.get('per_page'))
self.assertEqual('OK', content['status'])
def test_non_int_page(self):
""" Tests that it can handle invalid values """
request = self.req.get('/paginator/?page=spam')
response = MockPaginatorView.as_view()(request)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_page_range(self):
""" Tests that the page range is handle correctly """
request = self.req.get('/paginator/?page=0')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
request = self.req.get('/paginator/')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
num_pages = content['pages']
request = self.req.get('/paginator/?page=%d' % num_pages)
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_existing_query_parameters_are_preserved(self):
""" Tests that existing query parameters are preserved when
generating next/previous page links """
request = self.req.get('/paginator/?foo=bar&another=something')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue('foo=bar' in content['next'])
self.assertTrue('another=something' in content['next'])
self.assertTrue('page=2' in content['next'])
def test_duplicate_parameters_are_not_created(self):
""" Regression: ensure duplicate "page" parameters are not added to
paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
request = self.req.get('/paginator/?page=1')
response = MockPaginatorView.as_view()(request)
content = json.loads(response.content)
self.assertTrue('page=2' in content['next'])
self.assertFalse('page=1' in content['next'])

View File

@ -1,28 +1,28 @@
from django.db import models
from django.contrib.auth.models import Group
class CustomUser(models.Model):
"""
A custom user model, which uses a 'through' table for the foreign key
"""
username = models.CharField(max_length=255, unique=True)
groups = models.ManyToManyField(
to=Group, blank=True, null=True, through='UserGroupMap'
)
@models.permalink
def get_absolute_url(self):
return ('custom_user', (), {
'pk': self.id
})
class UserGroupMap(models.Model):
user = models.ForeignKey(to=CustomUser)
group = models.ForeignKey(to=Group)
@models.permalink
def get_absolute_url(self):
return ('user_group_map', (), {
'pk': self.id
})
from django.db import models
from django.contrib.auth.models import Group
class CustomUser(models.Model):
"""
A custom user model, which uses a 'through' table for the foreign key
"""
username = models.CharField(max_length=255, unique=True)
groups = models.ManyToManyField(
to=Group, blank=True, null=True, through='UserGroupMap'
)
@models.permalink
def get_absolute_url(self):
return ('custom_user', (), {
'pk': self.id
})
class UserGroupMap(models.Model):
user = models.ForeignKey(to=CustomUser)
group = models.ForeignKey(to=Group)
@models.permalink
def get_absolute_url(self):
return ('user_group_map', (), {
'pk': self.id
})

View File

@ -1,25 +1,29 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.forms import ModelForm
from django.contrib.auth.models import Group, User
from djangorestframework.resources import ModelResource
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from djangorestframework.tests.models import CustomUser
from djangorestframework.tests.testcases import TestModelsTestCase
class GroupResource(ModelResource):
model = Group
class UserForm(ModelForm):
class Meta:
model = User
exclude = ('last_login', 'date_joined')
class UserResource(ModelResource):
model = User
form = UserForm
class CustomUserResource(ModelResource):
model = CustomUser
model = CustomUser
urlpatterns = patterns('',
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
@ -31,9 +35,9 @@ urlpatterns = patterns('',
)
class ModelViewTests(TestCase):
class ModelViewTests(TestModelsTestCase):
"""Test the model views djangorestframework provides"""
urls = 'djangorestframework.tests.modelviews'
urls = 'djangorestframework.tests.modelviews'
def test_creation(self):
"""Ensure that a model object can be created"""
@ -52,18 +56,18 @@ class ModelViewTests(TestCase):
self.assertEqual(0, User.objects.count())
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
self.assertEqual(response.status_code, 201)
self.assertEqual(1, User.objects.count())
user = User.objects.all()[0]
self.assertEqual('bar', user.username)
self.assertEqual('baz', user.password)
self.assertEqual(1, user.groups.count())
group = user.groups.all()[0]
self.assertEqual('foo', group.name)
def test_creation_with_m2m_relation_through(self):
"""
Ensure that a model object with a m2m relation can be created where that
@ -74,13 +78,13 @@ class ModelViewTests(TestCase):
self.assertEqual(0, User.objects.count())
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
self.assertEqual(response.status_code, 201)
self.assertEqual(1, CustomUser.objects.count())
user = CustomUser.objects.all()[0]
self.assertEqual('bar', user.username)
self.assertEqual(1, user.groups.count())
group = user.groups.all()[0]
self.assertEqual('foo', group.name)
self.assertEqual('foo', group.name)

View File

@ -23,14 +23,14 @@ else:
class ClientView(View):
def get(self, request):
return {'resource': 'Protected!'}
urlpatterns = patterns('',
url(r'^$', oauth_required(ClientView.as_view())),
url(r'^oauth/', include('oauth_provider.urls')),
url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'),
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
)
class OAuthTests(TestCase):
"""
OAuth authentication:
@ -42,23 +42,23 @@ else:
* the third-party website is able to retrieve data from the API
"""
urls = 'djangorestframework.tests.oauthentication'
def setUp(self):
self.client = Client()
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password)
# OAuth requirements
self.resource = Resource(name='data', url='/')
self.resource.save()
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
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)
self.consumer.save()
def test_oauth_invalid_and_anonymous_access(self):
"""
Verify that the resource is protected and the OAuth authorization view
@ -69,16 +69,16 @@ else:
self.assertEqual(response.status_code, 401)
response = self.client.get('/oauth/authorize/', follow=True)
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
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.
"""
self.client.login(username=self.username, password=self.password)
response = self.client.get('/oauth/authorize/', follow=True)
self.assertEqual(response.content, 'No request token specified.')
def _create_request_token_parameters(self):
"""
A shortcut to create request's token parameters.
@ -93,28 +93,28 @@ else:
'oauth_callback': 'http://api.example.com/request_token_ready',
'scope': 'data',
}
def test_oauth_request_token_retrieval(self):
"""
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.assertEqual(response.status_code, 200)
token = list(Token.objects.all())[-1]
self.failIf(token.key not in response.content)
self.failIf(token.secret not in response.content)
def test_oauth_user_request_authorization(self):
"""
Verify that the user can access the authorization page once logged in
and the request token has been retrieved.
"""
# Setup
response = self.client.get("/oauth/request_token/",
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
# Starting the test here
self.client.login(username=self.username, password=self.password)
parameters = {'oauth_token': token.key,}
@ -129,7 +129,7 @@ else:
token = Token.objects.get(key=token.key)
self.failIf(token.key not in response['Location'])
self.assertEqual(token.is_approved, 1)
def _create_access_token_parameters(self, token):
"""
A shortcut to create access' token parameters.
@ -145,13 +145,13 @@ else:
'oauth_verifier': token.verifier,
'scope': 'data',
}
def test_oauth_access_token_retrieval(self):
"""
Verify that the request token can be retrieved by the server.
"""
# Setup
response = self.client.get("/oauth/request_token/",
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
self.client.login(username=self.username, password=self.password)
@ -160,7 +160,7 @@ else:
parameters['authorize_access'] = 1 # fake authorization by the user
response = self.client.post("/oauth/authorize/", parameters)
token = Token.objects.get(key=token.key)
# Starting the test here
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
self.assertEqual(response.status_code, 200)
@ -169,7 +169,7 @@ else:
self.failIf(access_token.key not in response.content)
self.failIf(access_token.secret not in response.content)
self.assertEqual(access_token.user.username, 'john')
def _create_access_parameters(self, access_token):
"""
A shortcut to create access' parameters.
@ -188,13 +188,13 @@ else:
signature = signature_method.sign(oauth_request, self.consumer, access_token)
parameters['oauth_signature'] = signature
return parameters
def test_oauth_protected_resource_access(self):
"""
Verify that the request token can be retrieved by the server.
"""
# Setup
response = self.client.get("/oauth/request_token/",
response = self.client.get("/oauth/request_token/",
self._create_request_token_parameters())
token = list(Token.objects.all())[-1]
self.client.login(username=self.username, password=self.password)
@ -205,7 +205,7 @@ else:
token = Token.objects.get(key=token.key)
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
# Starting the test here
response = self.client.get("/", self._create_access_token_parameters(access_token))
self.assertEqual(response.status_code, 200)

View File

@ -2,7 +2,7 @@
from django.test import TestCase
import djangorestframework
class TestVersion(TestCase):
class TestVersion(TestCase):
"""Simple sanity test to check the VERSION exists"""
def test_version(self):

View File

@ -8,76 +8,76 @@
# >>> req = RequestFactory().get('/')
# >>> some_view = View()
# >>> some_view.request = req # Make as if this request had been dispatched
#
#
# FormParser
# ============
#
#
# Data flatening
# ----------------
#
#
# Here is some example data, which would eventually be sent along with a post request :
#
#
# >>> inpt = urlencode([
# ... ('key1', 'bla1'),
# ... ('key2', 'blo1'), ('key2', 'blo2'),
# ... ])
#
#
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
#
#
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
# True
#
#
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
#
#
# >>> class MyFormParser(FormParser):
# ...
# ...
# ... def is_a_list(self, key, val_list):
# ... return len(val_list) > 1
#
#
# This new parser only flattens the lists of parameters that contain a single value.
#
#
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
# True
#
#
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
#
#
# Submitting an empty list
# --------------------------
#
#
# When submitting an empty select multiple, like this one ::
#
#
# <select multiple="multiple" name="key2"></select>
#
#
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
#
#
# <select multiple="multiple" name="key2"><option value="_empty"></select>
#
#
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
#
#
# >>> inpt = urlencode([
# ... ('key1', 'blo1'), ('key1', '_empty'),
# ... ('key2', '_empty'),
# ... ])
#
#
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
#
#
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'blo1'}
# True
#
#
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
#
#
# >>> class MyFormParser(FormParser):
# ...
# ...
# ... def is_a_list(self, key, val_list):
# ... return key == 'key2'
# ...
# ...
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
# >>> data == {'key1': 'blo1', 'key2': []}
# True
#
#
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
# """
# import httplib, mimetypes
@ -87,7 +87,7 @@
# from djangorestframework.parsers import MultiPartParser
# from djangorestframework.views import View
# from StringIO import StringIO
#
#
# def encode_multipart_formdata(fields, files):
# """For testing multipart parser.
# fields is a sequence of (name, value) elements for regular form fields.
@ -112,10 +112,10 @@
# body = CRLF.join(L)
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
# return content_type, body
#
#
# def get_content_type(filename):
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
#
#
#class TestMultiPartParser(TestCase):
# def setUp(self):
# self.req = RequestFactory()
@ -136,6 +136,8 @@ from cgi import parse_qs
from django import forms
from django.test import TestCase
from djangorestframework.parsers import FormParser
from djangorestframework.parsers import XMLParser
import datetime
class Form(forms.Form):
field1 = forms.CharField(max_length=3)
@ -143,13 +145,66 @@ class Form(forms.Form):
class TestFormParser(TestCase):
def setUp(self):
self.string = "field1=abc&field2=defghijk"
self.string = "field1=abc&field2=defghijk"
def test_parse(self):
""" Make sure the `QueryDict` works OK """
parser = FormParser(None)
stream = StringIO(self.string)
(data, files) = parser.parse(stream)
self.assertEqual(Form(data).is_valid(), True)
class TestXMLParser(TestCase):
def setUp(self):
self._input = StringIO(
'<?xml version="1.0" encoding="utf-8"?>'
'<root>'
'<field_a>121.0</field_a>'
'<field_b>dasd</field_b>'
'<field_c></field_c>'
'<field_d>2011-12-25 12:45:00</field_d>'
'</root>'
)
self._data = {
'field_a': 121,
'field_b': 'dasd',
'field_c': None,
'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
}
self._complex_data_input = StringIO(
'<?xml version="1.0" encoding="utf-8"?>'
'<root>'
'<creation_date>2011-12-25 12:45:00</creation_date>'
'<sub_data_list>'
'<list-item><sub_id>1</sub_id><sub_name>first</sub_name></list-item>'
'<list-item><sub_id>2</sub_id><sub_name>second</sub_name></list-item>'
'</sub_data_list>'
'<name>name</name>'
'</root>'
)
self._complex_data = {
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
"name": "name",
"sub_data_list": [
{
"sub_id": 1,
"sub_name": "first"
},
{
"sub_id": 2,
"sub_name": "second"
}
]
}
def test_parse(self):
parser = XMLParser(None)
(data, files) = parser.parse(self._input)
self.assertEqual(data, self._data)
def test_complex_data_parse(self):
parser = XMLParser(None)
(data, files) = parser.parse(self._complex_data_input)
self.assertEqual(data, self._complex_data)

View File

@ -1,16 +1,20 @@
from django.conf.urls.defaults import patterns, url
from django import http
import re
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from djangorestframework import status
from djangorestframework.views import View
from djangorestframework.compat import View as DjangoView
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer
from djangorestframework.parsers import JSONParser, YAMLParser
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer
from djangorestframework.parsers import JSONParser, YAMLParser, XMLParser
from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response
from djangorestframework.utils.mediatypes import add_media_type_param
from StringIO import StringIO
import datetime
from decimal import Decimal
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
@ -18,31 +22,58 @@ DUMMYCONTENT = 'dummycontent'
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
class RendererA(BaseRenderer):
media_type = 'mock/renderera'
format="formata"
format = "formata"
def render(self, obj=None, media_type=None):
return RENDERER_A_SERIALIZER(obj)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
format="formatb"
format = "formatb"
def render(self, obj=None, media_type=None):
return RENDERER_B_SERIALIZER(obj)
class MockView(ResponseMixin, DjangoView):
renderers = (RendererA, RendererB)
def get(self, request, **kwargs):
response = Response(DUMMYSTATUS, DUMMYCONTENT)
return self.render(response)
class MockGETView(View):
def get(self, request, **kwargs):
return {'foo': ['bar', 'baz']}
class HTMLView(View):
renderers = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
return 'text'
class HTMLView1(View):
renderers = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
return 'text'
urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', 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])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^api', include('djangorestframework.urls', namespace='djangorestframework'))
)
@ -89,7 +120,7 @@ class RendererIntegrationTests(TestCase):
self.assertEquals(resp['Content-Type'], RendererB.media_type)
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEquals(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
@ -144,14 +175,15 @@ class RendererIntegrationTests(TestCase):
self.assertEquals(resp.status_code, DUMMYSTATUS)
_flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
_indented_repr = """{
"foo": [
"bar",
"baz"
]
}"""
def strip_trailing_whitespace(content):
"""
Seems to be some inconsistencies re. trailing whitespace with
different versions of the json lib.
"""
return re.sub(' +\n', '\n', content)
class JSONRendererTests(TestCase):
"""
@ -162,65 +194,219 @@ class JSONRendererTests(TestCase):
"""
Test basic JSON rendering.
"""
obj = {'foo':['bar','baz']}
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library.
self.assertEquals(content, _flat_repr)
def test_with_content_type_args(self):
"""
Test JSON rendering with additional content type arguments supplied.
Test JSON rendering with additional content type arguments supplied.
"""
obj = {'foo':['bar','baz']}
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None)
content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(content, _indented_repr)
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
def test_render_and_parse(self):
"""
Test rendering and then parsing returns the original object.
IE obj -> render -> parse -> obj.
"""
obj = {'foo':['bar','baz']}
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None)
parser = JSONParser(None)
content = renderer.render(obj, 'application/json')
(data, files) = parser.parse(StringIO(content))
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:
_yaml_repr = 'foo: [bar, baz]\n'
class YAMLRendererTests(TestCase):
"""
Tests specific to the JSON Renderer
"""
def test_render(self):
"""
Test basic YAML rendering.
"""
obj = {'foo':['bar','baz']}
obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None)
content = renderer.render(obj, 'application/yaml')
self.assertEquals(content, _yaml_repr)
def test_render_and_parse(self):
"""
Test rendering and then parsing returns the original object.
IE obj -> render -> parse -> obj.
"""
obj = {'foo':['bar','baz']}
obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None)
parser = YAMLParser(None)
content = renderer.render(obj, 'application/yaml')
(data, files) = parser.parse(StringIO(content))
self.assertEquals(obj, data)
self.assertEquals(obj, data)
class XMLRendererTestCase(TestCase):
"""
Tests specific to the XML Renderer
"""
_complex_data = {
"creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
"name": "name",
"sub_data_list": [
{
"sub_id": 1,
"sub_name": "first"
},
{
"sub_id": 2,
"sub_name": "second"
}
]
}
def test_render_string(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 'astring'}, 'application/xml')
self.assertXMLContains(content, '<field>astring</field>')
def test_render_integer(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 111}, 'application/xml')
self.assertXMLContains(content, '<field>111</field>')
def test_render_datetime(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({
'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
}, 'application/xml')
self.assertXMLContains(content, '<field>2011-12-25 12:45:00</field>')
def test_render_float(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': 123.4}, 'application/xml')
self.assertXMLContains(content, '<field>123.4</field>')
def test_render_decimal(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
self.assertXMLContains(content, '<field>111.2</field>')
def test_render_none(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render({'field': None}, 'application/xml')
self.assertXMLContains(content, '<field></field>')
def test_render_complex_data(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = renderer.render(self._complex_data, 'application/xml')
self.assertXMLContains(content, '<sub_name>first</sub_name>')
self.assertXMLContains(content, '<sub_name>second</sub_name>')
def test_render_and_parse_complex_data(self):
"""
Test XML rendering.
"""
renderer = XMLRenderer(None)
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser(None)
complex_data_out, dummy = parser.parse(content)
error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
self.assertEqual(self._complex_data, complex_data_out, error_msg)
def assertXMLContains(self, xml, string):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>'))
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
class Issue122Tests(TestCase):
"""
Tests that covers #122.
"""
urls = 'djangorestframework.tests.renderers'
def test_only_html_renderer(self):
"""
Test if no infinite recursion occurs.
"""
resp = self.client.get('/html')
def test_html_renderer_is_first(self):
"""
Test if no infinite recursion occurs.
"""
resp = self.client.get('/html1')

View File

@ -5,7 +5,7 @@
#from djangorestframework.response import Response
#
#
#class TestResponse(TestCase):
#class TestResponse(TestCase):
#
# # Interface tests
#

View File

@ -1,28 +1,33 @@
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import simplejson as json
from djangorestframework.renderers import JSONRenderer
from djangorestframework.reverse import reverse
from djangorestframework.views import View
class MockView(View):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
permissions = ()
class MyView(View):
"""
Mock resource which simply returns a URL, so that we can ensure
that reversed URLs are fully qualified.
"""
renderers = (JSONRenderer, )
def get(self, request):
return reverse('another')
return reverse('myview', request=request)
urlpatterns = patterns('',
url(r'^$', MockView.as_view()),
url(r'^another$', MockView.as_view(), name='another'),
url(r'^myview$', MyView.as_view(), name='myview'),
)
class ReverseTests(TestCase):
"""Tests for """
"""
Tests for fully qualifed URLs when using `reverse`.
"""
urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self):
response = self.client.get('/')
self.assertEqual(json.loads(response.content), 'http://testserver/another')
response = self.client.get('/myview')
self.assertEqual(json.loads(response.content), 'http://testserver/myview')

View File

@ -1,13 +1,13 @@
"""Tests for the resource module"""
from django.test import TestCase
from djangorestframework.serializer import Serializer
from django.db import models
from django.test import TestCase
from django.utils.translation import ugettext_lazy
from djangorestframework.serializer import Serializer
import datetime
import decimal
class TestObjectToData(TestCase):
class TestObjectToData(TestCase):
"""
Tests for the Serializer class.
"""
@ -34,9 +34,7 @@ class TestObjectToData(TestCase):
self.assertEquals(self.serialize(Foo().foo), 1)
def test_datetime(self):
"""
datetime objects are left as-is.
"""
"""datetime objects are left as-is."""
now = datetime.datetime.now()
self.assertEquals(self.serialize(now), now)
@ -46,6 +44,9 @@ class TestObjectToData(TestCase):
self.assertEquals(self.serialize({'keys': 'foo'}), {'keys': u'foo'})
self.assertEquals(self.serialize({'values': 'foo'}), {'values': u'foo'})
def test_ugettext_lazy(self):
self.assertEquals(self.serialize(ugettext_lazy('foobar')), u'foobar')
class TestFieldNesting(TestCase):
"""
@ -56,8 +57,8 @@ class TestFieldNesting(TestCase):
self.serialize = self.serializer.serialize
class M1(models.Model):
field1 = models.CharField()
field2 = models.CharField()
field1 = models.CharField(max_length=256)
field2 = models.CharField(max_length=256)
class M2(models.Model):
field = models.OneToOneField(M1)
@ -103,6 +104,27 @@ class TestFieldNesting(TestCase):
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
def test_serializer_no_fields(self):
"""
Test related serializer works when the fields attr isn't present. Fix for
#178.
"""
class NestedM2(Serializer):
fields = ('field1', )
class NestedM3(Serializer):
fields = ('field2', )
class SerializerM2(Serializer):
include = [('field', NestedM2)]
exclude = ('id', )
class SerializerM3(Serializer):
fields = [('field', NestedM3)]
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
def test_serializer_classname_nesting(self):
"""
Test related model serialization
@ -121,3 +143,18 @@ class TestFieldNesting(TestCase):
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
def test_serializer_overridden_hook_method(self):
"""
Test serializing a model instance which overrides a class method on the
serializer. Checks for correct behaviour in odd edge case.
"""
class SerializerM2(Serializer):
fields = ('overridden', )
def overridden(self):
return False
self.m2.overridden = True
self.assertEqual(SerializerM2().serialize_model(self.m2),
{'overridden': True})

View File

@ -3,14 +3,10 @@ from django.test import TestCase
from djangorestframework import status
class TestStatus(TestCase):
class TestStatus(TestCase):
"""Simple sanity test to check the status module"""
def test_status(self):
"""Ensure the status module is present and correct."""
self.assertEquals(200, status.OK)
self.assertEquals(200, status.HTTP_200_OK)
self.assertEquals(404, status.NOT_FOUND)
self.assertEquals(404, status.HTTP_404_NOT_FOUND)

View File

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

View File

@ -21,25 +21,25 @@ class MockView(View):
class MockView_PerViewThrottling(MockView):
permissions = ( PerViewThrottling, )
class MockView_PerResourceThrottling(MockView):
class MockView_PerResourceThrottling(MockView):
permissions = ( PerResourceThrottling, )
resource = FormResource
class MockView_MinuteThrottling(MockView):
throttle = '3/min'
class ThrottlingTests(TestCase):
urls = 'djangorestframework.tests.throttling'
urls = 'djangorestframework.tests.throttling'
def setUp(self):
"""
Reset the cache so that no throttles will be active
"""
cache.clear()
self.factory = RequestFactory()
def test_requests_are_throttled(self):
"""
Ensure request rate is limited
@ -48,7 +48,7 @@ class ThrottlingTests(TestCase):
for dummy in range(4):
response = MockView.as_view()(request)
self.assertEqual(503, response.status_code)
def set_throttle_timer(self, view, value):
"""
Explicitly set the timer, overriding time.time()
@ -71,7 +71,7 @@ class ThrottlingTests(TestCase):
response = MockView.as_view()(request)
self.assertEqual(200, response.status_code)
def ensure_is_throttled(self, view, expect):
request = self.factory.get('/')
request.user = User.objects.create(username='a')
@ -80,27 +80,27 @@ class ThrottlingTests(TestCase):
request.user = User.objects.create(username='b')
response = view.as_view()(request)
self.assertEqual(expect, response.status_code)
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
"""
self.ensure_is_throttled(MockView, 200)
def test_request_throttling_is_per_view(self):
"""
Ensure request rate is limited globally per View for PerViewThrottles
"""
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
def test_request_throttling_is_per_resource(self):
"""
Ensure request rate is limited globally per Resource for PerResourceThrottles
"""
"""
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
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
@ -111,7 +111,7 @@ class ThrottlingTests(TestCase):
self.set_throttle_timer(view, timer)
response = view.as_view()(request)
self.assertEquals(response['X-Throttle'], expect)
def test_seconds_fields(self):
"""
Ensure for second based throttles.
@ -122,7 +122,7 @@ class ThrottlingTests(TestCase):
(0, 'status=SUCCESS; next=1.00 sec'),
(0, 'status=FAILURE; next=1.00 sec')
))
def test_minutes_fields(self):
"""
Ensure for minute based throttles.
@ -133,7 +133,7 @@ class ThrottlingTests(TestCase):
(0, 'status=SUCCESS; next=60.00 sec'),
(0, 'status=FAILURE; next=60.00 sec')
))
def test_next_rate_remains_constant_if_followed(self):
"""
If a client follows the recommended next request rate,

View File

@ -1,12 +1,9 @@
from django import forms
from django.db import models
from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.resources import FormResource, ModelResource
from djangorestframework.response import ErrorResponse
from djangorestframework.views import View
from djangorestframework.resources import Resource
class TestDisabledValidations(TestCase):
@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase):
resource = DisabledFormResource
view = MockView()
content = {'qwerty':'uiop'}
content = {'qwerty': 'uiop'}
self.assertEqual(FormResource(view).validate_request(content, None), content)
def test_disabled_form_validator_get_bound_form_returns_none(self):
@ -33,12 +30,11 @@ class TestDisabledValidations(TestCase):
class MockView(View):
resource = DisabledFormResource
view = MockView()
content = {'qwerty':'uiop'}
content = {'qwerty': 'uiop'}
self.assertEqual(FormResource(view).get_bound_form(content), None)
def test_disabled_model_form_validator_returns_content_unchanged(self):
"""If the view's form is None and does not have a Resource with a model set then
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
@ -47,17 +43,18 @@ class TestDisabledValidations(TestCase):
resource = ModelResource
view = DisabledModelFormView()
content = {'qwerty':'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
content = {'qwerty': 'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None)
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
class DisabledModelFormView(View):
resource = ModelResource
view = DisabledModelFormView()
content = {'qwerty':'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None)
content = {'qwerty': 'uiop'}
self.assertEqual(ModelResource(view).get_bound_form(content), None)
class TestNonFieldErrors(TestCase):
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
@ -68,15 +65,15 @@ class TestNonFieldErrors(TestCase):
field1 = forms.CharField(required=False)
field2 = forms.CharField(required=False)
ERROR_TEXT = 'You may not supply both field1 and field2'
def clean(self):
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
raise forms.ValidationError(self.ERROR_TEXT)
return self.cleaned_data #pragma: no cover
return self.cleaned_data
class MockResource(FormResource):
form = MockForm
class MockView(View):
pass
@ -84,41 +81,40 @@ class TestNonFieldErrors(TestCase):
content = {'field1': 'example1', 'field2': 'example2'}
try:
MockResource(view).validate_request(content, None)
except ErrorResponse, exc:
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else:
self.fail('ErrorResponse was not raised') #pragma: no cover
self.fail('ErrorResponse was not raised')
class TestFormValidation(TestCase):
"""Tests which check basic form validation.
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
(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):
qwerty = forms.CharField(required=True)
class MockFormResource(FormResource):
form = MockForm
class MockModelResource(ModelResource):
form = MockForm
class MockFormView(View):
resource = MockFormResource
class MockModelFormView(View):
resource = MockModelResource
self.MockFormResource = MockFormResource
self.MockModelResource = MockModelResource
self.MockModelResource = MockModelResource
self.MockFormView = MockFormView
self.MockModelFormView = MockModelFormView
self.MockModelFormView = MockModelFormView
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
content = {'qwerty':'uiop'}
content = {'qwerty': 'uiop'}
self.assertEqual(validator.validate_request(content, None), content)
def validation_failure_raises_response_exception(self, validator):
@ -130,59 +126,69 @@ class TestFormValidation(TestCase):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
content = {'qwerty': 'uiop', 'extra': 'extra'}
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
def validation_allows_extra_fields_if_explicitly_set(self, validator):
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
content = {'qwerty': 'uiop', 'extra': 'extra'}
content = {'qwerty': 'uiop', 'extra': 'extra'}
validator._validate(content, None, allowed_extra_fields=('extra',))
def validation_allows_unknown_fields_if_explicitly_allowed(self, validator):
"""If we set ``unknown_form_fields`` on the form resource, then don't
raise errors on unexpected request data"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
validator.allow_unknown_form_fields = True
self.assertEqual({'qwerty': u'uiop'},
validator.validate_request(content, None),
"Resource didn't accept unknown fields.")
validator.allow_unknown_form_fields = False
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
content = {'qwerty': 'uiop'}
content = {'qwerty': 'uiop'}
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
"""If validation fails due to no content, ensure the response contains a single non-field error"""
content = {}
content = {}
try:
validator.validate_request(content, None)
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:
self.fail('ResourceException was not raised') #pragma: no cover
self.fail('ResourceException was not raised')
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
"""If validation fails due to a field error, ensure the response contains a single field error"""
content = {'qwerty': ''}
try:
validator.validate_request(content, None)
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
self.fail('ResourceException was not raised')
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
content = {'qwerty': 'uiop', 'extra': 'extra'}
try:
validator.validate_request(content, None)
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
self.fail('ResourceException was not raised')
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
"""If validation for multiple reasons, ensure the response contains each error"""
content = {'qwerty': '', 'extra': 'extra'}
try:
validator.validate_request(content, None)
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
self.fail('ResourceException was not raised')
# Tests on FormResource
@ -202,6 +208,10 @@ class TestFormValidation(TestCase):
validator = self.MockFormResource(self.MockFormView())
self.validation_allows_extra_fields_if_explicitly_set(validator)
def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
validator = self.MockFormResource(self.MockFormView())
self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
validator = self.MockFormResource(self.MockFormView())
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
@ -263,56 +273,53 @@ class TestFormValidation(TestCase):
class TestModelFormValidator(TestCase):
"""Tests specific to ModelFormValidatorMixin"""
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):
qwerty = models.CharField(max_length=256)
uiop = models.CharField(max_length=256, blank=True)
@property
def readonly(self):
return 'read only'
class MockResource(ModelResource):
model = MockModel
class MockView(View):
resource = MockResource
self.validator = MockResource(MockView)
self.validator = MockResource(MockView)
def test_property_fields_are_allowed_on_model_forms(self):
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example', 'readonly': 'read only'}
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
self.assertEqual(self.validator.validate_request(content, None), content)
def test_property_fields_are_not_required_on_model_forms(self):
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
content = {'qwerty':'example', 'uiop': 'example'}
content = {'qwerty': 'example', 'uiop': 'example'}
self.assertEqual(self.validator.validate_request(content, None), content)
def test_extra_fields_not_allowed_on_model_forms(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
def test_validate_requires_fields_on_model_forms(self):
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
broken clients more easily (eg submitting content with a misnamed field)"""
content = {'readonly': 'read only'}
content = {'readonly': 'read only'}
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
content = {'qwerty':'example', 'readonly': 'read only'}
content = {'qwerty': 'example', 'readonly': 'read only'}
self.validator.validate_request(content, None)
def test_model_form_validator_uses_model_forms(self):
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))

View File

@ -1,43 +1,137 @@
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
from django.conf.urls.defaults import patterns, url, include
from django.http import HttpResponse
from django.test import TestCase
from django.test import Client
from django import forms
from django.db import models
from djangorestframework.views import View
from djangorestframework.parsers import JSONParser
from djangorestframework.resources import ModelResource
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from StringIO import StringIO
urlpatterns = patterns('djangorestframework.utils.staticviews',
url(r'^robots.txt$', 'deny_robots'),
url(r'^favicon.ico$', 'favicon'),
url(r'^accounts/login$', 'api_login'),
url(r'^accounts/logout$', 'api_logout'),
class MockView(View):
"""This is a basic mock view"""
pass
class MockViewFinal(View):
"""View with final() override"""
def final(self, request, response, *args, **kwargs):
return HttpResponse('{"test": "passed"}', content_type="application/json")
class ResourceMockView(View):
"""This is a resource-based mock view"""
class MockForm(forms.Form):
foo = forms.BooleanField(required=False)
bar = forms.IntegerField(help_text='Must be an integer.')
baz = forms.CharField(max_length=32)
form = MockForm
class MockResource(ModelResource):
"""This is a mock model-based resource"""
class MockResourceModel(models.Model):
foo = models.BooleanField()
bar = models.IntegerField(help_text='Must be an integer.')
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
model = MockResourceModel
fields = ('foo', 'bar', 'baz')
urlpatterns = patterns('',
url(r'^mock/$', MockView.as_view()),
url(r'^mock/final/$', MockViewFinal.as_view()),
url(r'^resourcemock/$', ResourceMockView.as_view()),
url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)),
url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)),
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
)
class BaseViewTests(TestCase):
"""Test the base view class of djangorestframework"""
urls = 'djangorestframework.tests.views'
class ViewTests(TestCase):
def test_view_call_final(self):
response = self.client.options('/mock/final/')
self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
parser = JSONParser(None)
(data, files) = parser.parse(StringIO(response.content))
self.assertEqual(data['test'], 'passed')
def test_options_method_simple_view(self):
response = self.client.options('/mock/')
self._verify_options_response(response,
name='Mock',
description='This is a basic mock view')
def test_options_method_resource_view(self):
response = self.client.options('/resourcemock/')
self._verify_options_response(response,
name='Resource Mock',
description='This is a resource-based mock view',
fields={'foo':'BooleanField',
'bar':'IntegerField',
'baz':'CharField',
})
def test_options_method_model_resource_list_view(self):
response = self.client.options('/model/')
self._verify_options_response(response,
name='Mock List',
description='This is a mock model-based resource',
fields={'foo':'BooleanField',
'bar':'IntegerField',
'baz':'CharField',
})
def test_options_method_model_resource_detail_view(self):
response = self.client.options('/model/0/')
self._verify_options_response(response,
name='Mock Instance',
description='This is a mock model-based resource',
fields={'foo':'BooleanField',
'bar':'IntegerField',
'baz':'CharField',
})
def _verify_options_response(self, response, name, description, fields=None, status=200,
mime_type='application/json'):
self.assertEqual(response.status_code, status)
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
parser = JSONParser(None)
(data, files) = parser.parse(StringIO(response.content))
self.assertTrue('application/json' in data['renders'])
self.assertEqual(name, data['name'])
self.assertEqual(description, data['description'])
if fields is None:
self.assertFalse(hasattr(data, 'fields'))
else:
self.assertEqual(data['fields'], fields)
class ExtraViewsTests(TestCase):
"""Test the extra views djangorestframework provides"""
urls = 'djangorestframework.tests.views'
def test_robots_view(self):
"""Ensure the robots view exists"""
response = self.client.get('/robots.txt')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'text/plain')
def test_favicon_view(self):
"""Ensure the favicon view exists"""
response = self.client.get('/favicon.ico')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'image/vnd.microsoft.icon')
urls = 'djangorestframework.tests.views'
def test_login_view(self):
"""Ensure the login view exists"""
response = self.client.get('/accounts/login')
response = self.client.get(reverse('djangorestframework:login'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
def test_logout_view(self):
"""Ensure the logout view exists"""
response = self.client.get('/accounts/logout')
response = self.client.get(reverse('djangorestframework:logout'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
# TODO: Add login/logout behaviour tests

View File

@ -1,16 +1,9 @@
from django.conf.urls.defaults import patterns
from django.conf import settings
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('djangorestframework.utils.staticviews',
(r'robots.txt', 'deny_robots'),
(r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'),
template_name = {'template_name': 'djangorestframework/login.html'}
urlpatterns = patterns('django.contrib.auth.views',
url(r'^login/$', 'login', template_name, name='login'),
url(r'^logout/$', 'logout', template_name, name='logout'),
)
# Only serve favicon in production because otherwise chrome users will pretty much
# permanantly have the django-rest-framework favicon whenever they navigate to
# 127.0.0.1:8000 or whatever, which gets annoying
if not settings.DEBUG:
urlpatterns += patterns('djangorestframework.utils.staticviews',
(r'favicon.ico', 'favicon'),
)

View File

@ -1,3 +1,4 @@
import django
from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator
from django.core.urlresolvers import resolve
@ -8,11 +9,6 @@ from djangorestframework.compat import StringIO
import re
import xml.etree.ElementTree as ET
#def admin_media_prefix(request):
# """Adds the ADMIN_MEDIA_PREFIX to the request context."""
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
from mediatypes import media_type_matches, is_form_media_type
from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence
@ -36,50 +32,18 @@ def as_tuple(obj):
return obj
return (obj,)
def url_resolves(url):
"""
Return True if the given URL is mapped to a view in the urlconf, False otherwise.
"""
try:
resolve(url)
except:
except Exception:
return False
return True
# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
#class object_dict(dict):
# """object view of dict, you can
# >>> a = object_dict()
# >>> a.fish = 'fish'
# >>> a['fish']
# 'fish'
# >>> a['water'] = 'water'
# >>> a.water
# 'water'
# >>> a.test = {'value': 1}
# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
# >>> a.test, a.test2.name, a.test2.value
# (1, 'test2', 2)
# """
# def __init__(self, initd=None):
# if initd is None:
# initd = {}
# dict.__init__(self, initd)
#
# def __getattr__(self, item):
# d = self.__getitem__(item)
# # if value is the only key in object, you can omit it
# if isinstance(d, dict) and 'value' in d and len(d) == 1:
# return d['value']
# else:
# return d
#
# def __setattr__(self, item, value):
# self.__setitem__(item, value)
# From xml2dict
class XML2Dict(object):
@ -103,8 +67,8 @@ class XML2Dict(object):
old = node_tree[tag]
if not isinstance(old, list):
node_tree.pop(tag)
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
return node_tree
@ -117,13 +81,13 @@ class XML2Dict(object):
"""
result = re.compile("\{(.*)\}(.*)").search(tag)
if result:
value.namespace, tag = result.groups()
value.namespace, tag = result.groups()
return (tag, value)
def parse(self, file):
"""parse a xml file to a dict"""
f = open(file, 'r')
return self.fromstring(f.read())
return self.fromstring(f.read())
def fromstring(self, s):
"""parse a string"""
@ -151,11 +115,15 @@ class XMLRenderer():
self._to_xml(xml, value)
xml.endElement(key)
elif data is None:
# Don't output any value
pass
else:
xml.characters(smart_unicode(data))
def dict2xml(self, data):
stream = StringIO.StringIO()
stream = StringIO.StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()

View File

@ -1,31 +1,30 @@
from django.core.urlresolvers import resolve
from djangorestframework.utils.description import get_name
def get_breadcrumbs(url):
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
from djangorestframework.views import View
def breadcrumbs_recursive(url, breadcrumbs_list):
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
try:
(view, unused_args, unused_kwargs) = resolve(url)
except:
except Exception:
pass
else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs
if isinstance(getattr(view, 'cls_instance', None), View):
breadcrumbs_list.insert(0, (get_name(view), url))
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
if url == '':
# All done
return breadcrumbs_list
elif url.endswith('/'):
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)

View File

@ -1,91 +0,0 @@
"""
Get a descriptive name and description for a view.
"""
import re
from djangorestframework.resources import Resource, FormResource, ModelResource
# These a a bit Grungy, but they do the job.
def get_name(view):
"""
Return a name for the view.
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'.
"""
# If we're looking up the name of a view callable, as found by reverse,
# grok the class instance that we stored when as_view was called.
if getattr(view, 'cls_instance', None):
view = view.cls_instance
# If this view has a resource that's been overridden, then use that resource for the name
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
name = view.resource.__name__
# Chomp of any non-descriptive trailing part of the resource class name
if name.endswith('Resource') and name != 'Resource':
name = name[:-len('Resource')]
# If the view has a descriptive suffix, eg '*** List', '*** Instance'
if getattr(view, '_suffix', None):
name += view._suffix
# Otherwise if it's a function view use the function's name
elif getattr(view, '__name__', None) is not None:
name = view.__name__
# If it's a view class with no resource then grok the name from the class name
elif getattr(view, '__class__', None) is not None:
name = view.__class__.__name__
# Chomp of any non-descriptive trailing part of the view class name
if name.endswith('View') and name != 'View':
name = name[:-len('View')]
# I ain't got nuthin fo' ya
else:
return ''
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()
def get_description(view):
"""
Provide a description for the view.
By default this is the view's docstring with nice unindention applied.
"""
# If we're looking up the name of a view callable, as found by reverse,
# grok the class instance that we stored when as_view was called.
if getattr(view, 'cls_instance', None):
view = view.cls_instance
# If this view has a resource that's been overridden, then use the resource's doctring
if getattr(view, 'resource', None) not in (None, Resource, FormResource, ModelResource):
doc = view.resource.__doc__
# Otherwise use the view doctring
elif getattr(view, '__doc__', None):
doc = view.__doc__
# I ain't got nuthin fo' ya
else:
return ''
if not doc:
return ''
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in doc.splitlines()[1:] if line.lstrip()]
# unindent the docstring if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', doc)
# otherwise return it as-is
return doc

View File

@ -61,10 +61,10 @@ def order_by_precedence(media_type_lst):
1. 'type/*'
0. '*/*'
"""
ret = [[],[],[],[]]
ret = [[], [], [], []]
for media_type in media_type_lst:
precedence = _MediaType(media_type).precedence
ret[3-precedence].append(media_type)
ret[3 - precedence].append(media_type)
return ret
@ -103,29 +103,6 @@ class _MediaType(object):
return 2
return 3
#def quality(self):
# """
# Return a quality level for the media type.
# """
# try:
# return Decimal(self.params.get('q', '1.0'))
# except:
# return Decimal(0)
#def score(self):
# """
# Return an overall score for a given media type given it's quality and precedence.
# """
# # NB. quality values should only have up to 3 decimal points
# # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
# return self.quality * 10000 + self.precedence
#def as_tuple(self):
# return (self.main_type, self.sub_type, self.params)
#def __repr__(self):
# return "<MediaType %s>" % (self.as_tuple(),)
def __str__(self):
return unicode(self).encode('utf-8')
@ -134,4 +111,3 @@ class _MediaType(object):
for key, val in self.params.items():
ret += "; %s=%s" % (key, val)
return ret

View File

@ -1,65 +0,0 @@
from django.contrib.auth.views import *
from django.conf import settings
from django.http import HttpResponse
import base64
def deny_robots(request):
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
def favicon(request):
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
# BLERGH
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
# be making settings changes in order to accomodate django-rest-framework
@csrf_protect
@never_cache
def api_login(request, template_name='api_login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm):
"""Displays the login form and handles the login action."""
redirect_to = request.REQUEST.get(redirect_field_name, '')
if request.method == "POST":
form = authentication_form(data=request.POST)
if form.is_valid():
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
# Heavier security check -- redirects to http://example.com should
# not be allowed, but things like /view/?param=http://example.com
# should be allowed. This regex checks if there is a '//' *before* a
# question mark.
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
redirect_to = settings.LOGIN_REDIRECT_URL
# Okay, security checks complete. Log the user in.
auth_login(request, form.get_user())
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to)
else:
form = authentication_form(request)
request.session.set_test_cookie()
#current_site = get_current_site(request)
return render_to_response(template_name, {
'form': form,
redirect_field_name: redirect_to,
#'site': current_site,
#'site_name': current_site.name,
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
}, context_instance=RequestContext(request))
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
return logout(request, next_page, template_name, redirect_field_name)

View File

@ -5,11 +5,13 @@ be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour.
"""
from django.core.urlresolvers import set_script_prefix
import re
from django.http import HttpResponse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View as DjangoView
from djangorestframework.compat import View as DjangoView, apply_markdown
from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import *
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
@ -24,6 +26,47 @@ __all__ = (
)
def _remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view/resource classes.
"""
if content.endswith(trailing) and content != trailing:
return content[:-len(trailing)]
return content
def _remove_leading_indent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
"""
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content
def _camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view/resource classes.
"""
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
return re.sub(camelcase_boundry, ' \\1', content).strip()
_resource_classes = (
None,
resources.Resource,
resources.FormResource,
resources.ModelResource
)
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
"""
@ -31,46 +74,44 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
Performs request deserialization, response serialization, authentication and input validation.
"""
resource = None
"""
The resource to use when validating requests and filtering responses,
or `None` to use default behaviour.
"""
resource = None
renderers = renderers.DEFAULT_RENDERERS
"""
List of renderers the resource can serialize the response with, ordered by preference.
"""
renderers = renderers.DEFAULT_RENDERERS
parsers = parsers.DEFAULT_PARSERS
"""
List of parsers the resource can parse the request with.
"""
parsers = parsers.DEFAULT_PARSERS
authentication = (authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication)
"""
List of all authenticating methods to attempt.
"""
authentication = ( authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication )
permissions = (permissions.FullAnonAccess,)
"""
List of all permissions that must be checked.
"""
permissions = ( permissions.FullAnonAccess, )
@classmethod
def as_view(cls, **initkwargs):
"""
Override the default :meth:`as_view` to store an instance of the view
as an attribute on the callable function. This allows us to discover
information about the view when we do URL reverse lookups.
information about the view when we do URL reverse lookups.
"""
view = super(View, cls).as_view(**initkwargs)
view.cls_instance = cls(**initkwargs)
return view
@property
def allowed_methods(self):
"""
@ -78,15 +119,64 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
"""
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def get_name(self):
"""
Return the resource or view class name for use as this view's name.
Override to customize.
"""
# If this view has a resource that's been overridden, then use that resource for the name
if getattr(self, 'resource', None) not in _resource_classes:
name = self.resource.__name__
name = _remove_trailing_string(name, 'Resource')
name += getattr(self, '_suffix', '')
# If it's a view class with no resource then grok the name from the class name
else:
name = self.__class__.__name__
name = _remove_trailing_string(name, 'View')
return _camelcase_to_spaces(name)
def get_description(self, html=False):
"""
Return the resource or view docstring for use as this view's description.
Override to customize.
"""
description = None
# If this view has a resource that's been overridden,
# then try to use the resource's docstring
if getattr(self, 'resource', None) not in _resource_classes:
description = self.resource.__doc__
# Otherwise use the view docstring
if not description:
description = self.__doc__ or ''
description = _remove_leading_indent(description)
if not isinstance(description, unicode):
description = description.decode('UTF-8')
if html:
return self.markup_description(description)
return description
def markup_description(self, description):
if apply_markdown:
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '<br />')
return mark_safe(description)
def http_method_not_allowed(self, request, *args, **kwargs):
"""
Return an HTTP 405 error if an operation is called which does not have a handler method.
Return an HTTP 405 error if an operation is called which does not have a handler method.
"""
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
def initial(self, request, *args, **kargs):
"""
Hook for any code that needs to run prior to anything else.
@ -95,14 +185,25 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
"""
pass
def final(self, request, response, *args, **kargs):
"""
Hook for any code that needs to run after everything else in the view.
"""
# Always add these headers.
response.headers['Allow'] = ', '.join(self.allowed_methods)
# sample to allow caching using Vary http header
response.headers['Vary'] = 'Authenticate, Accept'
# merge with headers possibly set at some point in the view
response.headers.update(self.headers)
return self.render(response)
def add_header(self, field, value):
"""
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
"""
self.headers[field] = value
# Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt.
@csrf_exempt
@ -112,13 +213,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.kwargs = kwargs
self.headers = {}
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
try:
self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions
self._check_permissions()
@ -142,21 +239,29 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.filter_response(response.raw_content)
except ErrorResponse, exc:
response = exc.response
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
# merge with headers possibly set at some point in the view
response.headers.update(self.headers)
return self.render(response)
return self.final(request, response, *args, **kwargs)
def options(self, request, *args, **kwargs):
response_obj = {
'name': self.get_name(),
'description': self.get_description(),
'renders': self._rendered_media_types,
'parses': self._parsed_media_types,
}
form = self.get_bound_form()
if form is not None:
field_name_types = {}
for name, field in form.fields.iteritems():
field_name_types[name] = field.__class__.__name__
response_obj['fields'] = field_name_types
# Note 'ErrorResponse' is misleading, it's just any response
# that should be rendered and returned immediately, without any
# response filtering.
raise ErrorResponse(status.HTTP_200_OK, response_obj)
class ModelView(View):
@ -165,20 +270,23 @@ class ModelView(View):
"""
resource = resources.ModelResource
class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
"""
A view which provides default operations for read/update/delete against a model instance.
"""
_suffix = 'Instance'
class ListModelView(ListModelMixin, ModelView):
"""
A view which provides default operations for list, against a model in the database.
"""
"""
_suffix = 'List'
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
"""
A view which provides default operations for list and create, against a model in the database.
"""
"""
_suffix = 'List'

9
docs/check_sphinx.py Normal file
View File

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

View File

@ -14,8 +14,8 @@
import sys, os
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__)), 'examples'))
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')) # for importing settings
import settings
from django.core.management import setup_environ
setup_environ(settings)
@ -55,9 +55,13 @@ copyright = u'2011, Tom Christie'
# built documents.
#
# The short X.Y version.
version = '0.1'
import djangorestframework
version = djangorestframework.__version__
# The full version, including alpha/beta/rc tags.
release = '0.1'
release = version
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
# 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
# further. For a list of options available for each theme, see the
@ -220,3 +224,5 @@ html_static_path = []
#man_pages = [
# ()
#]
linkcheck_timeout = 120 # seconds, set to extra large value for link_checks

10
docs/contents.rst Normal file
View File

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

23
docs/examples.rst Normal file
View File

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

View File

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

View File

@ -1,5 +1,3 @@
.. _modelviews:
Getting Started - Model Views
-----------------------------
@ -7,11 +5,11 @@ Getting Started - Model Views
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::
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:
@ -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:
* 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:
.. code-block:: bash
# 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."]}}
# 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."]}}

View File

@ -1,9 +1,7 @@
.. _objectstore:
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.

View File

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

View File

@ -1,5 +1,3 @@
.. _codehighlighting:
Code Highlighting API
=====================
@ -8,11 +6,11 @@ We're going to provide a simple wrapper around the awesome `pygments <http://pyg
.. 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::
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
@ -79,13 +77,13 @@ For example if we make a POST request using form input:
.. 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."]}}
Or if we make the same request using JSON:
.. 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."]}}

View File

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

View File

@ -1,5 +1,3 @@
.. _views:
Getting Started - Views
-----------------------
@ -7,11 +5,11 @@ Getting Started - Views
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::
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:
@ -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:
* http://api.django-rest-framework.org/resource-example/
* http://rest.ep.io/resource-example/
And from the command line:
.. code-block:: bash
# 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."]}}
# 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."]}}

8
docs/howto.rst Normal file
View File

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

View File

@ -7,7 +7,7 @@ Alternative frameworks
There are a number of alternative REST frameworks for Django:
* `django-piston <https://bitbucket.org/jespern/django-piston/wiki/Home>`_ is very mature, and has a large community behind it. This project was originally based on piston code in parts.
* `django-tasypie <https://github.com/toastdriven/django-tastypie>`_ is also very good, and has a very active and helpful developer community and maintainers.
* `django-tastypie <https://github.com/toastdriven/django-tastypie>`_ is also very good, and has a very active and helpful developer community and maintainers.
* Other interesting projects include `dagny <https://github.com/zacharyvoase/dagny>`_ and `dj-webmachine <http://benoitc.github.com/dj-webmachine/>`_

View File

@ -1,19 +1,18 @@
Using Django REST framework Mixin classes
=========================================
This example demonstrates creating a REST API **without** using Django REST framework's :class:`.Resource` or :class:`.ModelResource`,
but instead using Django :class:`View` class, and adding the :class:`EmitterMixin` class to provide full HTTP Accept header content negotiation,
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,
a browseable Web API, and much of the other goodness that Django REST framework gives you for free.
.. note::
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::
curl -X GET http://api.django-rest-framework.org/mixin/
curl -X GET http://rest.ep.io/mixin/
URL configuration
@ -26,5 +25,6 @@ Everything we need for this example can go straight into the URL conf...
.. include:: ../../examples/mixin/urls.py
: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.

39
docs/howto/reverse.rst Normal file
View File

@ -0,0 +1,39 @@
Returning URIs from your Web APIs
=================================
As a rule, it's probably better practice to return absolute URIs from you web APIs, e.g. "http://example.com/foobar", rather than returning relative URIs, e.g. "/foobar".
The advantages of doing so are:
* It's more explicit.
* It leaves less work for your API clients.
* There's no ambiguity about the meaning of the string when it's found in representations such as JSON that do not have a native URI type.
* It allows us to easily do things like markup HTML representations with hyperlinks.
Django REST framework provides two utility functions to make it simpler to return absolute URIs from your Web API.
There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier.
reverse(viewname, request, ...)
-------------------------------
The :py:func:`~reverse.reverse` function has the same behavior as `django.core.urlresolvers.reverse`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port::
from djangorestframework.reverse import reverse
from djangorestframework.views import View
class MyView(View):
def get(self, request):
context = {
'url': reverse('year-summary', request, args=[1945])
}
return Response(context)
reverse_lazy(viewname, request, ...)
------------------------------------
The :py:func:`~reverse.reverse_lazy` function has the same behavior as `django.core.urlresolvers.reverse_lazy`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port.
.. _django.core.urlresolvers.reverse: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse
.. _django.core.urlresolvers.reverse_lazy: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy

View File

@ -3,48 +3,71 @@
Setup
=====
Installing into site-packages
-----------------------------
Templates
---------
If you need to manually install Django REST framework to your ``site-packages`` directory, run the ``setup.py`` script::
Django REST framework uses a few templates for the HTML and plain text
documenting renderers. You'll need to ensure ``TEMPLATE_LOADERS`` setting
contains ``'django.template.loaders.app_directories.Loader'``.
This will already be the case by default.
python setup.py install
You may customize the templates by creating a new template called
``djangorestframework/api.html`` in your project, which should extend
``djangorestframework/base.html`` and override the appropriate
block tags. For example::
Template Loaders
----------------
{% extends "djangorestframework/base.html" %}
Django REST framework uses a few templates for the HTML and plain text documenting emitters.
{% block title %}My API{% endblock %}
* Ensure ``TEMPLATE_LOADERS`` setting contains ``'django.template.loaders.app_directories.Loader'``.
{% block branding %}
<h1 id="site-name">My API</h1>
{% endblock %}
This will be the case by default so you shouldn't normally need to do anything here.
Admin Styling
-------------
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 requires `django.contrib.staticfiles`_ to serve it's css.
If you're using Django 1.2 you'll need to use the seperate
`django-staticfiles`_ package instead.
You can override the styling by creating a file in your top-level static
directory named ``djangorestframework/css/style.css``
* 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
--------
The Python `markdown library <http://www.freewisdom.org/projects/python-markdown/>`_ is not required but comes recommended.
`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 formatting`_ which will be rendered by the self-documenting API.
robots.txt, favicon, login/logout
---------------------------------
YAML
----
Django REST framework comes with a few views that can be useful including a deny robots view, a favicon view, and api login and logout views::
YAML support is optional, and requires `PyYAML`_.
from django.conf.urls.defaults import patterns
urlpatterns = patterns('djangorestframework.views',
(r'robots.txt', 'deny_robots'),
(r'favicon.ico', 'favicon'),
# Add your resources here
(r'^accounts/login/$', 'api_login'),
(r'^accounts/logout/$', 'api_logout'),
)
Login / Logout
--------------
Django REST framework includes login and logout views that are needed if
you're using the self-documenting API.
Make sure you include the following in your `urlconf`::
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('',
...
url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework'))
)
.. _django.contrib.staticfiles: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
.. _URLObject: http://pypi.python.org/pypi/URLObject/
.. _Python markdown: http://www.freewisdom.org/projects/python-markdown/
.. _markdown formatting: http://daringfireball.net/projects/markdown/syntax
.. _PyYAML: http://pypi.python.org/pypi/PyYAML

View File

@ -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::
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/

View File

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

View File

@ -11,11 +11,12 @@ Introduction
Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs.
**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:
---------
* 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/>`_.
* 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.
@ -26,10 +27,10 @@ Features:
Resources
---------
**Project hosting:** `Bitbucket <https://bitbucket.org/tomchristie/django-rest-framework>`_ and `GitHub <https://github.com/tomchristie/django-rest-framework>`_.
**Project hosting:** `GitHub <https://github.com/tomchristie/django-rest-framework>`_.
* The ``djangorestframework`` package is `available on PyPI <http://pypi.python.org/pypi/djangorestframework>`_.
* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_ and a `project blog <http://blog.django-rest-framework.org>`_.
* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_.
* Bug reports are handled on the `issue tracker <https://github.com/tomchristie/django-rest-framework/issues>`_.
* There is a `Jenkins CI server <http://jenkins.tibold.nl/job/djangorestframework/>`_ which tracks test status and coverage reporting. (Thanks Marko!)
@ -39,24 +40,23 @@ Requirements
------------
* Python (2.5, 2.6, 2.7 supported)
* Django (1.2, 1.3 supported)
* Django (1.2, 1.3, 1.4 supported)
* `django.contrib.staticfiles`_ (or `django-staticfiles`_ for Django 1.2)
* `URLObject`_ >= 2.0.0
* `Markdown`_ >= 2.1.0 (Optional)
* `PyYAML`_ >= 3.10 (Optional)
Installation
------------
You can install Django REST framework using ``pip`` or ``easy_install``::
pip install djangorestframework
Or get the latest development version using mercurial or git::
Or get the latest development version using git::
hg clone https://bitbucket.org/tomchristie/django-rest-framework
git clone git@github.com:tomchristie/django-rest-framework.git
Or you can `download the current release <http://pypi.python.org/pypi/djangorestframework>`_.
Setup
-----
@ -64,6 +64,12 @@ To add Django REST framework to a Django project:
* Ensure that the ``djangorestframework`` directory is on your ``PYTHONPATH``.
* Add ``djangorestframework`` to your ``INSTALLED_APPS``.
* Add the following to your URLconf. (To include the REST framework Login/Logout views.)::
urlpatterns = patterns('',
...
url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework'))
)
For more information on settings take a look at the :ref:`setup` section.
@ -72,13 +78,20 @@ Getting Started
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``::
from django.conf.urls.defaults import patterns, url
from djangorestframework.resources import ModelResource
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from myapp.models import MyModel
class MyResource(ModelResource):
model = MyModel
@ -87,70 +100,19 @@ 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)),
)
Django REST framework comes with two "getting started" examples.
.. include:: howto.rst
#. :ref:`views`
#. :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:: library.rst
How Tos, FAQs & Notes
---------------------
.. include:: examples.rst
.. toctree::
:maxdepth: 1
:hidden:
howto/setup
howto/usingcurl
howto/alternativeframeworks
howto/mixin
contents
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
.. include:: ../CHANGELOG.rst
Indices and tables
------------------
@ -159,3 +121,8 @@ Indices and tables
* :ref:`modindex`
* :ref:`search`
.. _django.contrib.staticfiles: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
.. _URLObject: http://pypi.python.org/pypi/URLObject/
.. _Markdown: http://pypi.python.org/pypi/Markdown/
.. _PyYAML: http://pypi.python.org/pypi/PyYAML

8
docs/library.rst Normal file
View File

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

5
docs/library/reverse.rst Normal file
View File

@ -0,0 +1,5 @@
:mod:`reverse`
================
.. automodule:: reverse
:members:

View File

@ -1,6 +1,6 @@
# Documentation requires Django & Sphinx, and their dependencies...
Django==1.2.4
Django>=1.2.4
Jinja2==2.5.5
Pygments==1.4
Sphinx==1.0.7

View File

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

1
examples/.epio-app Normal file
View File

@ -0,0 +1 @@
rest

View File

@ -2,6 +2,7 @@ from django.db import models
from django.template.defaultfilters import slugify
import uuid
def uuid_str():
return str(uuid.uuid1())
@ -14,6 +15,7 @@ RATING_CHOICES = ((0, 'Awful'),
MAX_POSTS = 10
class BlogPost(models.Model):
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
title = models.CharField(max_length=128)
@ -37,4 +39,3 @@ class Comment(models.Model):
comment = models.TextField()
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
created = models.DateTimeField(auto_now_add=True)

View File

@ -1,5 +1,5 @@
from django.core.urlresolvers import reverse
from djangorestframework.resources import ModelResource
from djangorestframework.reverse import reverse
from blogpost.models import BlogPost, Comment
@ -11,17 +11,26 @@ class BlogPostResource(ModelResource):
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
ordering = ('-created',)
def url(self, instance):
return reverse('blog-post',
kwargs={'key': instance.key},
request=self.request)
def comments(self, instance):
return reverse('comments', kwargs={'blogpost': instance.key})
return reverse('comments',
kwargs={'blogpost': instance.key},
request=self.request)
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
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
ordering = ('-created',)
def blogpost(self, instance):
return reverse('blog-post', kwargs={'key': instance.blogpost.key})
return reverse('blog-post',
kwargs={'key': instance.blogpost.key},
request=self.request)

View File

@ -1,12 +1,11 @@
"""Test a range of REST API usage of the example application.
"""
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.utils import simplejson as json
from djangorestframework.compat import RequestFactory
from djangorestframework.reverse import reverse
from djangorestframework.views import InstanceModelView, ListOrCreateModelView
from blogpost import models, urls
@ -15,68 +14,68 @@ from blogpost import models, urls
# class AcceptHeaderTests(TestCase):
# """Test correct behaviour of the Accept header as specified by RFC 2616:
#
#
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
#
#
# def assert_accept_mimetype(self, mimetype, expect=None):
# """Assert that a request with given mimetype in the accept header,
# gives a response with the appropriate content-type."""
# if expect is None:
# expect = mimetype
#
#
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
#
#
# self.assertEquals(resp['content-type'], expect)
#
#
#
#
# def dont_test_accept_json(self):
# """Ensure server responds with Content-Type of JSON when requested."""
# self.assert_accept_mimetype('application/json')
#
#
# def dont_test_accept_xml(self):
# """Ensure server responds with Content-Type of XML when requested."""
# self.assert_accept_mimetype('application/xml')
#
#
# 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."""
# 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):
# """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')
#
#
# def dont_test_default_json_prefered(self):
# """Ensure server responds with JSON in preference to XML."""
# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
#
#
# def dont_test_accept_generic_subtype_format(self):
# """Ensure server responds with an appropriate type, when the subtype is left generic."""
# self.assert_accept_mimetype('text/*', expect='text/html')
#
#
# def dont_test_accept_generic_type_format(self):
# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
# self.assert_accept_mimetype('*/*', expect='application/json')
#
#
# 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."""
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
# self.assertEquals(resp.status_code, 406)
#
#
# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
# """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')
#
#
#
#
# class AllowedMethodsTests(TestCase):
# """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):
# """GET requests on a read only resource should default to a 200 (OK) response"""
# resp = self.client.get(reverse(views.RootResource))
# self.assertEquals(resp.status_code, 200)
#
#
# 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"""
# resp = self.client.put(reverse(views.RootResource), {})
@ -171,7 +170,7 @@ from blogpost import models, urls
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."""
def setUp(self):
@ -193,7 +192,7 @@ class TestRotation(TestCase):
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
view(request)
self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
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.'''
for post in range(15):
@ -201,11 +200,11 @@ class TestRotation(TestCase):
request = self.factory.post('/blog-post', data=form_data)
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
view(request)
request = self.factory.get('/blog-post')
request = self.factory.get('/blog-post')
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
response = view(request)
response_posts = json.loads(response.content)
response_titles = [d['title'] for d in response_posts]
response_titles.reverse()
self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])

62
examples/epio.ini Normal file
View File

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

View File

@ -1,24 +1,25 @@
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.renderers import DEFAULT_RENDERERS
from djangorestframework.response import Response
from djangorestframework.reverse import reverse
from django.conf.urls.defaults import patterns, url
from django.core.urlresolvers import reverse
class ExampleView(ResponseMixin, View):
"""An example view using Django 1.3's class based views.
Uses djangorestframework's RendererMixin to provide support for multiple output formats."""
Uses djangorestframework's RendererMixin to provide support for multiple
output formats."""
renderers = DEFAULT_RENDERERS
def get(self, request):
url = reverse('mixin-view', request=request)
response = Response(200, {'description': 'Some example content',
'url': reverse('mixin-view')})
'url': url})
return self.render(response)
urlpatterns = patterns('',
url(r'^$', ExampleView.as_view(), name='mixin-view'),
)

View File

@ -2,6 +2,7 @@ from django.db import models
MAX_INSTANCES = 10
class MyModel(models.Model):
foo = models.BooleanField()
bar = models.IntegerField(help_text='Must be an integer.')
@ -15,5 +16,3 @@ class MyModel(models.Model):
super(MyModel, self).save(*args, **kwargs)
while MyModel.objects.all().count() > MAX_INSTANCES:
MyModel.objects.all().order_by('-created')[0].delete()

View File

@ -1,7 +1,14 @@
from djangorestframework.resources import ModelResource
from djangorestframework.reverse import reverse
from modelresourceexample.models import MyModel
class MyModelResource(ModelResource):
model = MyModel
fields = ('foo', 'bar', 'baz', 'url')
ordering = ('created',)
def url(self, instance):
return reverse('model-resource-instance',
kwargs={'id': instance.id},
request=self.request)

View File

@ -2,7 +2,10 @@ from django.conf.urls.defaults import patterns, url
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from modelresourceexample.resources import MyModelResource
my_model_list = ListOrCreateModelView.as_view(resource=MyModelResource)
my_model_instance = InstanceModelView.as_view(resource=MyModelResource)
urlpatterns = patterns('',
url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'),
url(r'^(?P<pk>[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)),
url(r'^$', my_model_list, name='model-resource-root'),
url(r'^(?P<id>[0-9]+)/$', my_model_instance, name='model-resource-instance'),
)

View File

@ -1,7 +1,7 @@
from django.conf.urls.defaults import patterns, url
from objectstore.views import ObjectStoreRoot, StoredObject
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'),
)

View File

@ -1,6 +1,6 @@
from django.conf import settings
from django.core.urlresolvers import reverse
from djangorestframework.reverse import reverse
from djangorestframework.views import View
from djangorestframework.response import Response
from djangorestframework import status
@ -13,6 +13,9 @@ import operator
OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
MAX_FILES = 10
if not os.path.exists(OBJECT_STORE_DIR):
os.makedirs(OBJECT_STORE_DIR)
def remove_oldest_files(dir, max_files):
"""
@ -25,6 +28,20 @@ def remove_oldest_files(dir, max_files):
[os.remove(path) for path in ctime_sorted_paths[max_files:]]
def get_filename(key):
"""
Given a stored object's key returns the file's path.
"""
return os.path.join(OBJECT_STORE_DIR, key)
def get_file_url(key, request):
"""
Given a stored object's key returns the URL for the object.
"""
return reverse('stored-object', kwargs={'key': key}, request=request)
class ObjectStoreRoot(View):
"""
Root of the Object Store API.
@ -35,20 +52,24 @@ class ObjectStoreRoot(View):
"""
Return a list of all the stored object URLs. (Ordered by creation time, newest first)
"""
filepaths = [os.path.join(OBJECT_STORE_DIR, file) for file in os.listdir(OBJECT_STORE_DIR) if not file.startswith('.')]
filepaths = [os.path.join(OBJECT_STORE_DIR, file)
for file in os.listdir(OBJECT_STORE_DIR)
if not file.startswith('.')]
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
key=operator.itemgetter(1), reverse=True)]
return [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]
return [get_file_url(key, request) for key in ctime_sorted_basenames]
def post(self, request):
"""
Create a new stored object, with a unique key.
"""
key = str(uuid.uuid1())
pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(self.CONTENT, open(pathname, 'wb'))
filename = get_filename(key)
pickle.dump(self.CONTENT, open(filename, 'wb'))
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': reverse('stored-object', kwargs={'key':key})})
url = get_file_url(key, request)
return Response(status.HTTP_201_CREATED, self.CONTENT, {'Location': url})
class StoredObject(View):
@ -56,29 +77,30 @@ class StoredObject(View):
Represents a stored object.
The object may be any picklable content.
"""
def get(self, request, key):
"""
Return a stored object, by unpickling the contents of a locally stored file.
Return a stored object, by unpickling the contents of a locally
stored file.
"""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):
filename = get_filename(key)
if not os.path.exists(filename):
return Response(status.HTTP_404_NOT_FOUND)
return pickle.load(open(pathname, 'rb'))
return pickle.load(open(filename, 'rb'))
def put(self, request, key):
"""
Update/create a stored object, by pickling the request content to a locally stored file.
Update/create a stored object, by pickling the request content to a
locally stored file.
"""
pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(self.CONTENT, open(pathname, 'wb'))
filename = get_filename(key)
pickle.dump(self.CONTENT, open(filename, 'wb'))
return self.CONTENT
def delete(self, request):
def delete(self, request, key):
"""
Delete a stored object, by removing it's pickled file.
"""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):
filename = get_filename(key)
if not os.path.exists(filename):
return Response(status.HTTP_404_NOT_FOUND)
os.remove(pathname)
os.remove(filename)

View File

@ -0,0 +1,18 @@
[
{
"pk": 2,
"model": "auth.user",
"fields": {
"username": "test",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"groups": [],
"user_permissions": [],
"password": "sha1$b3dff$671b4ab97f2714446da32670d27576614e176758",
"email": ""
}
}
]

View File

@ -1,12 +0,0 @@
- fields:
first_name: ''
groups: []
is_active: true
is_staff: true
is_superuser: true
last_name: ''
password: sha1$b3dff$671b4ab97f2714446da32670d27576614e176758
user_permissions: []
username: test
model: auth.user
pk: 2

View File

@ -1 +1 @@
#for fixture loading
#for fixture loading

Some files were not shown because too many files have changed in this diff Show More