mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-05 04:50:12 +03:00
commit
bacedef4da
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,7 +1,9 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.db
|
*.db
|
||||||
assetplatform.egg-info/*
|
|
||||||
*~
|
*~
|
||||||
|
.*
|
||||||
|
|
||||||
|
assetplatform.egg-info/*
|
||||||
coverage.xml
|
coverage.xml
|
||||||
env
|
env
|
||||||
docs/build
|
docs/build
|
||||||
|
@ -14,11 +16,5 @@ dist/*
|
||||||
xmlrunner/*
|
xmlrunner/*
|
||||||
djangorestframework.egg-info/*
|
djangorestframework.egg-info/*
|
||||||
MANIFEST
|
MANIFEST
|
||||||
.project
|
|
||||||
.pydevproject
|
!.gitignore
|
||||||
.settings
|
|
||||||
.cache
|
|
||||||
.coverage
|
|
||||||
.tox
|
|
||||||
.DS_Store
|
|
||||||
.idea/*
|
|
||||||
|
|
25
.hgignore
25
.hgignore
|
@ -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
|
|
23
AUTHORS
23
AUTHORS
|
@ -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)
|
Marko Tibold <markotibold> (Additional thanks for providing & managing the Jenkins CI Server)
|
||||||
Paul Bagwell <pbgwl>
|
Paul Bagwell <pbgwl>
|
||||||
Sébastien Piquemal <sebpiq>
|
Sébastien Piquemal <sebpiq>
|
||||||
|
@ -12,12 +12,31 @@ Andrew Straw <astraw>
|
||||||
Zeth <zeth>
|
Zeth <zeth>
|
||||||
Fernando Zunino <fzunino>
|
Fernando Zunino <fzunino>
|
||||||
Jens Alm <ulmus>
|
Jens Alm <ulmus>
|
||||||
Craig Blaszczyk <jakul>
|
Craig Blaszczyk <jakul>
|
||||||
Garcia Solero <garciasolero>
|
Garcia Solero <garciasolero>
|
||||||
Tom Drummond <devioustree>
|
Tom Drummond <devioustree>
|
||||||
Danilo Bargen <gwrtheyrn>
|
Danilo Bargen <gwrtheyrn>
|
||||||
Andrew McCloud <amccloud>
|
Andrew McCloud <amccloud>
|
||||||
Thomas Steinacher <thomasst>
|
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:
|
THANKS TO:
|
||||||
|
|
||||||
|
|
107
CHANGELOG.rst
Normal file
107
CHANGELOG.rst
Normal 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
21
LICENSE
|
@ -1,9 +1,22 @@
|
||||||
Copyright (c) 2011, Tom Christie
|
Copyright (c) 2011, Tom Christie
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
Redistributions of source code must retain the above copyright notice, this
|
||||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
list of conditions and the following disclaimer.
|
||||||
|
Redistributions in binary form must reproduce the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer in the documentation and/or
|
||||||
|
other materials provided with the distribution.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
|
@ -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 djangorestframework/templates *.txt *.html
|
||||||
recursive-include examples .keep *.py *.txt
|
recursive-include examples .keep *.py *.txt
|
||||||
recursive-include docs *.py *.rst *.html *.txt
|
recursive-include docs *.py *.rst *.html *.txt
|
||||||
include AUTHORS LICENSE requirements.txt tox.ini
|
include AUTHORS LICENSE CHANGELOG.rst requirements.txt tox.ini
|
||||||
|
|
71
README.rst
71
README.rst
|
@ -1,13 +1,18 @@
|
||||||
Django REST framework
|
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:
|
Features:
|
||||||
|
|
||||||
* Creates awesome self-describing *web browse-able* APIs.
|
* Creates awesome self-describing *web browse-able* APIs.
|
||||||
* Clean, modular design, using Django's class based views.
|
* 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.
|
* Stable, well tested code-base.
|
||||||
* Active developer community.
|
* 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>`_.
|
Issue tracking is on `GitHub <https://github.com/tomchristie/django-rest-framework/issues>`_.
|
||||||
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
General questions should be taken to the `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
||||||
|
|
||||||
|
We also have a `Jenkins service <http://jenkins.tibold.nl/job/djangorestframework1/>`_ which runs our test suite.
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
* Python (2.5, 2.6, 2.7 supported)
|
* Python (2.5, 2.6, 2.7 supported)
|
||||||
* Django (1.2, 1.3 supported)
|
* Django (1.2, 1.3, 1.4 supported)
|
||||||
|
|
||||||
|
|
||||||
Installation Notes
|
Installation Notes
|
||||||
|
@ -30,15 +37,10 @@ To clone the project from GitHub using git::
|
||||||
git clone git@github.com:tomchristie/django-rest-framework.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::
|
To install django-rest-framework in a virtualenv environment::
|
||||||
|
|
||||||
cd django-rest-framework
|
cd django-rest-framework
|
||||||
virtualenv --no-site-packages --distribute --python=python2.6 env
|
virtualenv --no-site-packages --distribute env
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
pip install -r requirements.txt # django, coverage
|
pip install -r requirements.txt # django, coverage
|
||||||
|
|
||||||
|
@ -79,54 +81,3 @@ To run the tests against the full set of supported configurations::
|
||||||
To create the sdist packages::
|
To create the sdist packages::
|
||||||
|
|
||||||
python setup.py sdist --formats=gztar,zip
|
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.
|
|
|
@ -1,3 +1,3 @@
|
||||||
__version__ = '0.2.3'
|
__version__ = '0.4.0-dev'
|
||||||
|
|
||||||
VERSION = __version__ # synonym
|
VERSION = __version__ # synonym
|
||||||
|
|
|
@ -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.contrib.auth import authenticate
|
||||||
from django.middleware.csrf import CsrfViewMiddleware
|
from djangorestframework.compat import CsrfViewMiddleware
|
||||||
from djangorestframework.utils import as_tuple
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -33,13 +32,13 @@ class BaseAuthentication(object):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
|
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
|
||||||
|
|
||||||
.. [*] The authentication context *will* typically be a :obj:`User`,
|
.. [*] 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
|
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
|
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
|
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
|
This can be an important distinction if you're implementing some token
|
||||||
based authentication mechanism, where the authentication context
|
based authentication mechanism, where the authentication context
|
||||||
may be more involved than simply mapping to a :obj:`User`.
|
may be more involved than simply mapping to a :obj:`User`.
|
||||||
|
@ -55,10 +54,10 @@ class BasicAuthentication(BaseAuthentication):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
Returns a :obj:`User` if a correct username and password have been supplied
|
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
|
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
|
||||||
|
|
||||||
if 'HTTP_AUTHORIZATION' in request.META:
|
if 'HTTP_AUTHORIZATION' in request.META:
|
||||||
auth = request.META['HTTP_AUTHORIZATION'].split()
|
auth = request.META['HTTP_AUTHORIZATION'].split()
|
||||||
if len(auth) == 2 and auth[0].lower() == "basic":
|
if len(auth) == 2 and auth[0].lower() == "basic":
|
||||||
|
@ -66,17 +65,17 @@ class BasicAuthentication(BaseAuthentication):
|
||||||
auth_parts = base64.b64decode(auth[1]).partition(':')
|
auth_parts = base64.b64decode(auth[1]).partition(':')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
|
uname, passwd = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
|
||||||
except DjangoUnicodeDecodeError:
|
except DjangoUnicodeDecodeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = authenticate(username=uname, password=passwd)
|
user = authenticate(username=uname, password=passwd)
|
||||||
if user is not None and user.is_active:
|
if user is not None and user.is_active:
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class UserLoggedInAuthentication(BaseAuthentication):
|
class UserLoggedInAuthentication(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
|
@ -88,18 +87,14 @@ class UserLoggedInAuthentication(BaseAuthentication):
|
||||||
Returns a :obj:`User` if the request session currently has a logged in user.
|
Returns a :obj:`User` if the request session currently has a logged in user.
|
||||||
Otherwise returns :const:`None`.
|
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 getattr(request, 'user', None) and request.user.is_active:
|
||||||
# If this is a POST request we enforce CSRF validation.
|
# Enforce CSRF validation for session based authentication.
|
||||||
if request.method.upper() == 'POST':
|
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
||||||
# Temporarily replace request.POST with .DATA,
|
|
||||||
# so that we use our more generic request parsing
|
if resp is None: # csrf passed
|
||||||
request._post = self.view.DATA
|
return request.user
|
||||||
resp = CsrfViewMiddleware().process_view(request, None, (), {})
|
|
||||||
del(request._post)
|
|
||||||
if resp is not None: # csrf failed
|
|
||||||
return None
|
|
||||||
return request.user
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
try:
|
||||||
import cStringIO as StringIO
|
import cStringIO as StringIO
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
|
|
||||||
# parse_qs
|
# parse_qs from 'urlparse' module unless python 2.5, in which case from 'cgi'
|
||||||
try:
|
try:
|
||||||
# python >= ?
|
# python >= 2.6
|
||||||
from urlparse import parse_qs
|
from urlparse import parse_qs
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# python <= ?
|
# python < 2.6
|
||||||
from cgi import parse_qs
|
from cgi import parse_qs
|
||||||
|
|
||||||
|
|
||||||
# django.test.client.RequestFactory (Django >= 1.3)
|
# django.test.client.RequestFactory (Required for Django < 1.3)
|
||||||
try:
|
try:
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
# From: http://djangosnippets.org/snippets/963/
|
# From: http://djangosnippets.org/snippets/963/
|
||||||
# Lovely stuff
|
# Lovely stuff
|
||||||
class RequestFactory(Client):
|
class RequestFactory(Client):
|
||||||
"""
|
"""
|
||||||
Class that lets you create mock :obj:`Request` objects for use in testing.
|
Class that lets you create mock :obj:`Request` objects for use in testing.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
get_request = rf.get('/hello/')
|
get_request = rf.get('/hello/')
|
||||||
post_request = rf.post('/submit/', {'foo': 'bar'})
|
post_request = rf.post('/submit/', {'foo': 'bar'})
|
||||||
|
|
||||||
This class re-uses the :class:`django.test.client.Client` interface. Of which
|
This class re-uses the :class:`django.test.client.Client` interface. Of which
|
||||||
you can find the docs here__.
|
you can find the docs here__.
|
||||||
|
|
||||||
__ http://www.djangoproject.com/documentation/testing/#the-test-client
|
__ http://www.djangoproject.com/documentation/testing/#the-test-client
|
||||||
|
|
||||||
Once you have a `request` object you can pass it to any :func:`view` function,
|
Once you have a `request` object you can pass it to any :func:`view` function,
|
||||||
just as if that :func:`view` had been hooked up using a URLconf.
|
just as if that :func:`view` had been hooked up using a URLconf.
|
||||||
"""
|
"""
|
||||||
def request(self, **request):
|
def request(self, **request):
|
||||||
|
@ -68,30 +69,30 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
if not hasattr(View, 'head'):
|
if not hasattr(View, 'head'):
|
||||||
# First implementation of Django class-based views did not include head method
|
# First implementation of Django class-based views did not include head method
|
||||||
# in base View class - https://code.djangoproject.com/ticket/15668
|
# in base View class - https://code.djangoproject.com/ticket/15668
|
||||||
class ViewPlusHead(View):
|
class ViewPlusHead(View):
|
||||||
def head(self, request, *args, **kwargs):
|
def head(self, request, *args, **kwargs):
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
View = ViewPlusHead
|
View = ViewPlusHead
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from django import http
|
from django import http
|
||||||
from django.utils.functional import update_wrapper
|
from django.utils.functional import update_wrapper
|
||||||
# from django.utils.log import getLogger
|
# from django.utils.log import getLogger
|
||||||
# from django.utils.decorators import classonlymethod
|
# from django.utils.decorators import classonlymethod
|
||||||
|
|
||||||
# logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
|
# logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
|
||||||
# Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely
|
# Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely
|
||||||
|
|
||||||
class View(object):
|
class View(object):
|
||||||
"""
|
"""
|
||||||
Intentionally simple parent class for all views. Only implements
|
Intentionally simple parent class for all views. Only implements
|
||||||
dispatch-by-method and simple sanity checking.
|
dispatch-by-method and simple sanity checking.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
|
http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Constructor. Called in the URLconf; can contain helpful extra
|
Constructor. Called in the URLconf; can contain helpful extra
|
||||||
|
@ -101,7 +102,7 @@ except ImportError:
|
||||||
# instance, or raise an error.
|
# instance, or raise an error.
|
||||||
for key, value in kwargs.iteritems():
|
for key, value in kwargs.iteritems():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
|
# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
|
@ -117,19 +118,19 @@ except ImportError:
|
||||||
if not hasattr(cls, key):
|
if not hasattr(cls, key):
|
||||||
raise TypeError(u"%s() received an invalid keyword %r" % (
|
raise TypeError(u"%s() received an invalid keyword %r" % (
|
||||||
cls.__name__, key))
|
cls.__name__, key))
|
||||||
|
|
||||||
def view(request, *args, **kwargs):
|
def view(request, *args, **kwargs):
|
||||||
self = cls(**initkwargs)
|
self = cls(**initkwargs)
|
||||||
return self.dispatch(request, *args, **kwargs)
|
return self.dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
# take name and docstring from class
|
# take name and docstring from class
|
||||||
update_wrapper(view, cls, updated=())
|
update_wrapper(view, cls, updated=())
|
||||||
|
|
||||||
# and possible attributes set by decorators
|
# and possible attributes set by decorators
|
||||||
# like csrf_exempt from dispatch
|
# like csrf_exempt from dispatch
|
||||||
update_wrapper(view, cls.dispatch, assigned=())
|
update_wrapper(view, cls.dispatch, assigned=())
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# Try to dispatch to the right method; if a method doesn't exist,
|
# Try to dispatch to the right method; if a method doesn't exist,
|
||||||
# defer to the error handler. Also defer to the error handler if the
|
# defer to the error handler. Also defer to the error handler if the
|
||||||
|
@ -142,7 +143,7 @@ except ImportError:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
return handler(request, *args, **kwargs)
|
return handler(request, *args, **kwargs)
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
|
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
|
||||||
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
|
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
|
||||||
|
@ -156,24 +157,237 @@ except ImportError:
|
||||||
def head(self, request, *args, **kwargs):
|
def head(self, request, *args, **kwargs):
|
||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
|
||||||
|
if django.VERSION >= (1, 4):
|
||||||
|
from django.middleware.csrf import CsrfViewMiddleware
|
||||||
|
else:
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import get_callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from logging import NullHandler
|
||||||
|
except ImportError:
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def emit(self, record):
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger = logging.getLogger('django.request')
|
||||||
|
|
||||||
|
if not logger.handlers:
|
||||||
|
logger.addHandler(NullHandler())
|
||||||
|
|
||||||
|
def same_origin(url1, url2):
|
||||||
|
"""
|
||||||
|
Checks if two URLs are 'same-origin'
|
||||||
|
"""
|
||||||
|
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
|
||||||
|
return p1[0:2] == p2[0:2]
|
||||||
|
|
||||||
|
def constant_time_compare(val1, val2):
|
||||||
|
"""
|
||||||
|
Returns True if the two strings are equal, False otherwise.
|
||||||
|
|
||||||
|
The time taken is independent of the number of characters that match.
|
||||||
|
"""
|
||||||
|
if len(val1) != len(val2):
|
||||||
|
return False
|
||||||
|
result = 0
|
||||||
|
for x, y in zip(val1, val2):
|
||||||
|
result |= ord(x) ^ ord(y)
|
||||||
|
return result == 0
|
||||||
|
|
||||||
|
# Use the system (hardware-based) random number generator if it exists.
|
||||||
|
if hasattr(random, 'SystemRandom'):
|
||||||
|
randrange = random.SystemRandom().randrange
|
||||||
|
else:
|
||||||
|
randrange = random.randrange
|
||||||
|
_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63
|
||||||
|
|
||||||
|
REASON_NO_REFERER = "Referer checking failed - no Referer."
|
||||||
|
REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
|
||||||
|
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
|
||||||
|
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
|
||||||
|
|
||||||
|
def _get_failure_view():
|
||||||
|
"""
|
||||||
|
Returns the view to be used for CSRF rejections
|
||||||
|
"""
|
||||||
|
return get_callable(settings.CSRF_FAILURE_VIEW)
|
||||||
|
|
||||||
|
def _get_new_csrf_key():
|
||||||
|
return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
|
||||||
|
|
||||||
|
def get_token(request):
|
||||||
|
"""
|
||||||
|
Returns the the CSRF token required for a POST form. The token is an
|
||||||
|
alphanumeric value.
|
||||||
|
|
||||||
|
A side effect of calling this function is to make the the csrf_protect
|
||||||
|
decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
|
||||||
|
header to the outgoing response. For this reason, you may need to use this
|
||||||
|
function lazily, as is done by the csrf context processor.
|
||||||
|
"""
|
||||||
|
request.META["CSRF_COOKIE_USED"] = True
|
||||||
|
return request.META.get("CSRF_COOKIE", None)
|
||||||
|
|
||||||
|
def _sanitize_token(token):
|
||||||
|
# Allow only alphanum, and ensure we return a 'str' for the sake of the post
|
||||||
|
# processing middleware.
|
||||||
|
token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
|
||||||
|
if token == "":
|
||||||
|
# In case the cookie has been truncated to nothing at some point.
|
||||||
|
return _get_new_csrf_key()
|
||||||
|
else:
|
||||||
|
return token
|
||||||
|
|
||||||
|
class CsrfViewMiddleware(object):
|
||||||
|
"""
|
||||||
|
Middleware that requires a present and correct csrfmiddlewaretoken
|
||||||
|
for POST requests that have a CSRF cookie, and sets an outgoing
|
||||||
|
CSRF cookie.
|
||||||
|
|
||||||
|
This middleware should be used in conjunction with the csrf_token template
|
||||||
|
tag.
|
||||||
|
"""
|
||||||
|
# The _accept and _reject methods currently only exist for the sake of the
|
||||||
|
# requires_csrf_token decorator.
|
||||||
|
def _accept(self, request):
|
||||||
|
# Avoid checking the request twice by adding a custom attribute to
|
||||||
|
# request. This will be relevant when both decorator and middleware
|
||||||
|
# are used.
|
||||||
|
request.csrf_processing_done = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _reject(self, request, reason):
|
||||||
|
return _get_failure_view()(request, reason=reason)
|
||||||
|
|
||||||
|
def process_view(self, request, callback, callback_args, callback_kwargs):
|
||||||
|
|
||||||
|
if getattr(request, 'csrf_processing_done', False):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
|
||||||
|
# Use same token next time
|
||||||
|
request.META['CSRF_COOKIE'] = csrf_token
|
||||||
|
except KeyError:
|
||||||
|
csrf_token = None
|
||||||
|
# Generate token and store it in the request, so it's available to the view.
|
||||||
|
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
|
||||||
|
|
||||||
|
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
|
||||||
|
# bailing out, so that get_token still works
|
||||||
|
if getattr(callback, 'csrf_exempt', False):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Assume that anything not defined as 'safe' by RC2616 needs protection.
|
||||||
|
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
|
||||||
|
if getattr(request, '_dont_enforce_csrf_checks', False):
|
||||||
|
# Mechanism to turn off CSRF checks for test suite. It comes after
|
||||||
|
# the creation of CSRF cookies, so that everything else continues to
|
||||||
|
# work exactly the same (e.g. cookies are sent etc), but before the
|
||||||
|
# any branches that call reject()
|
||||||
|
return self._accept(request)
|
||||||
|
|
||||||
|
if request.is_secure():
|
||||||
|
# Suppose user visits http://example.com/
|
||||||
|
# An active network attacker,(man-in-the-middle, MITM) sends a
|
||||||
|
# POST form which targets https://example.com/detonate-bomb/ and
|
||||||
|
# submits it via javascript.
|
||||||
|
#
|
||||||
|
# The attacker will need to provide a CSRF cookie and token, but
|
||||||
|
# that is no problem for a MITM and the session independent
|
||||||
|
# nonce we are using. So the MITM can circumvent the CSRF
|
||||||
|
# protection. This is true for any HTTP connection, but anyone
|
||||||
|
# using HTTPS expects better! For this reason, for
|
||||||
|
# https://example.com/ we need additional protection that treats
|
||||||
|
# http://example.com/ as completely untrusted. Under HTTPS,
|
||||||
|
# Barth et al. found that the Referer header is missing for
|
||||||
|
# same-domain requests in only about 0.2% of cases or less, so
|
||||||
|
# we can use strict Referer checking.
|
||||||
|
referer = request.META.get('HTTP_REFERER')
|
||||||
|
if referer is None:
|
||||||
|
logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
|
||||||
|
extra={
|
||||||
|
'status_code': 403,
|
||||||
|
'request': request,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._reject(request, REASON_NO_REFERER)
|
||||||
|
|
||||||
|
# Note that request.get_host() includes the port
|
||||||
|
good_referer = 'https://%s/' % request.get_host()
|
||||||
|
if not same_origin(referer, good_referer):
|
||||||
|
reason = REASON_BAD_REFERER % (referer, good_referer)
|
||||||
|
logger.warning('Forbidden (%s): %s' % (reason, request.path),
|
||||||
|
extra={
|
||||||
|
'status_code': 403,
|
||||||
|
'request': request,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._reject(request, reason)
|
||||||
|
|
||||||
|
if csrf_token is None:
|
||||||
|
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
|
||||||
|
# and in this way we can avoid all CSRF attacks, including login
|
||||||
|
# CSRF.
|
||||||
|
logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
|
||||||
|
extra={
|
||||||
|
'status_code': 403,
|
||||||
|
'request': request,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._reject(request, REASON_NO_CSRF_COOKIE)
|
||||||
|
|
||||||
|
# check non-cookie token for match
|
||||||
|
request_csrf_token = ""
|
||||||
|
if request.method == "POST":
|
||||||
|
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
|
||||||
|
|
||||||
|
if request_csrf_token == "":
|
||||||
|
# Fall back to X-CSRFToken, to make things easier for AJAX,
|
||||||
|
# and possible for PUT/DELETE
|
||||||
|
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
|
||||||
|
|
||||||
|
if not constant_time_compare(request_csrf_token, csrf_token):
|
||||||
|
logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
|
||||||
|
extra={
|
||||||
|
'status_code': 403,
|
||||||
|
'request': request,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._reject(request, REASON_BAD_TOKEN)
|
||||||
|
|
||||||
|
return self._accept(request)
|
||||||
|
|
||||||
|
|
||||||
# Markdown is optional
|
# Markdown is optional
|
||||||
try:
|
try:
|
||||||
import markdown
|
import markdown
|
||||||
import re
|
if markdown.version_info < (2, 0):
|
||||||
|
raise ImportError('Markdown < 2.0 is not supported.')
|
||||||
|
|
||||||
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
|
class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor):
|
||||||
"""
|
"""
|
||||||
Override `markdown`'s :class:`SetextHeaderProcessor`, so that ==== headers are <h2> and ---- headers are <h3>.
|
Class for markdown < 2.1
|
||||||
|
|
||||||
|
Override `markdown`'s :class:`SetextHeaderProcessor`, so that ==== headers are <h2> and ---- heade
|
||||||
|
|
||||||
We use <h1> for the resource name.
|
We use <h1> for the resource name.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
# Detect Setext-style header. Must be first 2 lines of block.
|
# Detect Setext-style header. Must be first 2 lines of block.
|
||||||
RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
|
RE = re.compile(r'^.*?\n[=-]{3,}', re.MULTILINE)
|
||||||
|
|
||||||
def test(self, parent, block):
|
def test(self, parent, block):
|
||||||
return bool(self.RE.match(block))
|
return bool(self.RE.match(block))
|
||||||
|
|
||||||
def run(self, parent, blocks):
|
def run(self, parent, blocks):
|
||||||
lines = blocks.pop(0).split('\n')
|
lines = blocks.pop(0).split('\n')
|
||||||
# Determine level. ``=`` is 1 and ``-`` is 2.
|
# Determine level. ``=`` is 1 and ``-`` is 2.
|
||||||
|
@ -186,21 +400,25 @@ try:
|
||||||
if len(lines) > 2:
|
if len(lines) > 2:
|
||||||
# Block contains additional lines. Add to master blocks for later.
|
# Block contains additional lines. Add to master blocks for later.
|
||||||
blocks.insert(0, '\n'.join(lines[2:]))
|
blocks.insert(0, '\n'.join(lines[2:]))
|
||||||
|
|
||||||
def apply_markdown(text):
|
def apply_markdown(text):
|
||||||
"""
|
"""
|
||||||
Simple wrapper around :func:`markdown.markdown` to apply our :class:`CustomSetextHeaderProcessor`,
|
Simple wrapper around :func:`markdown.markdown` to set the base level
|
||||||
and also set the base level of '#' style headers to <h2>.
|
of '#' style headers to <h2>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
extensions = ['headerid(level=2)']
|
extensions = ['headerid(level=2)']
|
||||||
safe_mode = False,
|
safe_mode = False,
|
||||||
output_format = markdown.DEFAULT_OUTPUT_FORMAT
|
|
||||||
|
|
||||||
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
|
if markdown.version_info < (2, 1):
|
||||||
safe_mode=safe_mode,
|
output_format = markdown.DEFAULT_OUTPUT_FORMAT
|
||||||
|
|
||||||
|
md = markdown.Markdown(extensions=markdown.load_extensions(extensions),
|
||||||
|
safe_mode=safe_mode,
|
||||||
output_format=output_format)
|
output_format=output_format)
|
||||||
md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
|
md.parser.blockprocessors['setextheader'] = CustomSetextHeaderProcessor(md.parser)
|
||||||
|
else:
|
||||||
|
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
|
||||||
return md.convert(text)
|
return md.convert(text)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -211,3 +429,37 @@ try:
|
||||||
import yaml
|
import yaml
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
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
|
||||||
|
|
|
@ -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`.
|
classes that can be added to a `View`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
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.db.models.fields.related import ForeignKey
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from urlobject import URLObject
|
||||||
|
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.parsers import FormParser, MultiPartParser
|
|
||||||
from djangorestframework.renderers import BaseRenderer
|
from djangorestframework.renderers import BaseRenderer
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
import re
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,14 +25,13 @@ __all__ = (
|
||||||
'ResponseMixin',
|
'ResponseMixin',
|
||||||
'AuthMixin',
|
'AuthMixin',
|
||||||
'ResourceMixin',
|
'ResourceMixin',
|
||||||
# Reverse URL lookup behavior
|
|
||||||
'InstanceMixin',
|
|
||||||
# Model behavior mixins
|
# Model behavior mixins
|
||||||
'ReadModelMixin',
|
'ReadModelMixin',
|
||||||
'CreateModelMixin',
|
'CreateModelMixin',
|
||||||
'UpdateModelMixin',
|
'UpdateModelMixin',
|
||||||
'DeleteModelMixin',
|
'DeleteModelMixin',
|
||||||
'ListModelMixin'
|
'ListModelMixin',
|
||||||
|
'PaginatorMixin'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,12 +47,12 @@ class RequestMixin(object):
|
||||||
_CONTENTTYPE_PARAM = '_content_type'
|
_CONTENTTYPE_PARAM = '_content_type'
|
||||||
_CONTENT_PARAM = '_content'
|
_CONTENT_PARAM = '_content'
|
||||||
|
|
||||||
|
parsers = ()
|
||||||
"""
|
"""
|
||||||
The set of request parsers that the view can handle.
|
The set of request parsers that the view can handle.
|
||||||
|
|
||||||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
||||||
"""
|
"""
|
||||||
parsers = ()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method(self):
|
def method(self):
|
||||||
|
@ -69,7 +66,6 @@ class RequestMixin(object):
|
||||||
self._load_method_and_content_type()
|
self._load_method_and_content_type()
|
||||||
return self._method
|
return self._method
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_type(self):
|
def content_type(self):
|
||||||
"""
|
"""
|
||||||
|
@ -83,7 +79,6 @@ class RequestMixin(object):
|
||||||
self._load_method_and_content_type()
|
self._load_method_and_content_type()
|
||||||
return self._content_type
|
return self._content_type
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DATA(self):
|
def DATA(self):
|
||||||
"""
|
"""
|
||||||
|
@ -96,7 +91,6 @@ class RequestMixin(object):
|
||||||
self._load_data_and_files()
|
self._load_data_and_files()
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def FILES(self):
|
def FILES(self):
|
||||||
"""
|
"""
|
||||||
|
@ -108,7 +102,6 @@ class RequestMixin(object):
|
||||||
self._load_data_and_files()
|
self._load_data_and_files()
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
def _load_data_and_files(self):
|
||||||
"""
|
"""
|
||||||
Parse the request content into self.DATA and self.FILES.
|
Parse the request content into self.DATA and self.FILES.
|
||||||
|
@ -119,7 +112,6 @@ class RequestMixin(object):
|
||||||
if not hasattr(self, '_data'):
|
if not hasattr(self, '_data'):
|
||||||
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
|
(self._data, self._files) = self._parse(self._get_stream(), self._content_type)
|
||||||
|
|
||||||
|
|
||||||
def _load_method_and_content_type(self):
|
def _load_method_and_content_type(self):
|
||||||
"""
|
"""
|
||||||
Set the method and content_type, and then check if they've been overridden.
|
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._content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
|
||||||
self._perform_form_overloading()
|
self._perform_form_overloading()
|
||||||
|
|
||||||
|
|
||||||
def _get_stream(self):
|
def _get_stream(self):
|
||||||
"""
|
"""
|
||||||
Returns an object that may be used to stream the request content.
|
Returns an object that may be used to stream the request content.
|
||||||
|
@ -145,10 +136,9 @@ class RequestMixin(object):
|
||||||
if content_length == 0:
|
if content_length == 0:
|
||||||
return None
|
return None
|
||||||
elif hasattr(request, 'read'):
|
elif hasattr(request, 'read'):
|
||||||
return request
|
return request
|
||||||
return StringIO(request.raw_post_data)
|
return StringIO(request.raw_post_data)
|
||||||
|
|
||||||
|
|
||||||
def _perform_form_overloading(self):
|
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
|
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.
|
# 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):
|
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
|
||||||
return
|
return
|
||||||
|
|
||||||
# At this point we're committed to parsing the request as form data.
|
# At this point we're committed to parsing the request as form data.
|
||||||
self._data = data = self.request.POST.copy()
|
self._data = data = self.request.POST.copy()
|
||||||
self._files = self.request.FILES
|
self._files = self.request.FILES
|
||||||
|
@ -174,7 +164,6 @@ class RequestMixin(object):
|
||||||
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
|
||||||
(self._data, self._files) = self._parse(stream, self._content_type)
|
(self._data, self._files) = self._parse(stream, self._content_type)
|
||||||
|
|
||||||
|
|
||||||
def _parse(self, stream, content_type):
|
def _parse(self, stream, content_type):
|
||||||
"""
|
"""
|
||||||
Parse the request content.
|
Parse the request content.
|
||||||
|
@ -192,10 +181,9 @@ class RequestMixin(object):
|
||||||
return parser.parse(stream)
|
return parser.parse(stream)
|
||||||
|
|
||||||
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||||
{'error': 'Unsupported media type in request \'%s\'.' %
|
{'error': 'Unsupported media type in request \'%s\'.' %
|
||||||
content_type})
|
content_type})
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _parsed_media_types(self):
|
def _parsed_media_types(self):
|
||||||
"""
|
"""
|
||||||
|
@ -203,22 +191,20 @@ class RequestMixin(object):
|
||||||
"""
|
"""
|
||||||
return [parser.media_type for parser in self.parsers]
|
return [parser.media_type for parser in self.parsers]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _default_parser(self):
|
def _default_parser(self):
|
||||||
"""
|
"""
|
||||||
Return the view's default parser class.
|
Return the view's default parser class.
|
||||||
"""
|
"""
|
||||||
return self.parsers[0]
|
return self.parsers[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
########## ResponseMixin ##########
|
########## ResponseMixin ##########
|
||||||
|
|
||||||
class ResponseMixin(object):
|
class ResponseMixin(object):
|
||||||
"""
|
"""
|
||||||
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
||||||
|
|
||||||
Default behavior is to use standard HTTP Accept header content negotiation.
|
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.
|
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.
|
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
|
_ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||||
_IGNORE_IE_ACCEPT_HEADER = True
|
_IGNORE_IE_ACCEPT_HEADER = True
|
||||||
|
|
||||||
|
renderers = ()
|
||||||
"""
|
"""
|
||||||
The set of response renderers that the view can handle.
|
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
|
# TODO: wrap this behavior around dispatch(), ensuring it works
|
||||||
# out of the box with existing Django classes that use render_to_response.
|
# 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
|
# Set the media type of the response
|
||||||
# Note that the renderer *could* override it in .render() if required.
|
# Note that the renderer *could* override it in .render() if required.
|
||||||
response.media_type = renderer.media_type
|
response.media_type = renderer.media_type
|
||||||
|
|
||||||
# Serialize the response content
|
# Serialize the response content
|
||||||
if response.has_content_body:
|
if response.has_content_body:
|
||||||
content = renderer.render(response.cleaned_content, media_type)
|
content = renderer.render(response.cleaned_content, media_type)
|
||||||
|
@ -267,7 +259,6 @@ class ResponseMixin(object):
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def _determine_renderer(self, request):
|
def _determine_renderer(self, request):
|
||||||
"""
|
"""
|
||||||
Determines the appropriate renderer for the output, given the client's 'Accept' header,
|
Determines the appropriate renderer for the output, given the client's 'Accept' header,
|
||||||
|
@ -282,13 +273,14 @@ class ResponseMixin(object):
|
||||||
# Use _accept parameter override
|
# Use _accept parameter override
|
||||||
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
|
||||||
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
elif (self._IGNORE_IE_ACCEPT_HEADER and
|
||||||
request.META.has_key('HTTP_USER_AGENT') and
|
'HTTP_USER_AGENT' in request.META and
|
||||||
MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])):
|
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
|
# Ignore MSIE's broken accept behavior and do something sensible instead
|
||||||
accept_list = ['text/html', '*/*']
|
accept_list = ['text/html', '*/*']
|
||||||
elif request.META.has_key('HTTP_ACCEPT'):
|
elif 'HTTP_ACCEPT' in request.META:
|
||||||
# Use standard HTTP Accept negotiation
|
# 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:
|
else:
|
||||||
# No accept header specified
|
# No accept header specified
|
||||||
accept_list = ['*/*']
|
accept_list = ['*/*']
|
||||||
|
@ -297,7 +289,7 @@ class ResponseMixin(object):
|
||||||
# attempting more specific media types first
|
# attempting more specific media types first
|
||||||
# NB. The inner loop here isn't as bad as it first looks :)
|
# 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)
|
# 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 accepted_media_type_lst in order_by_precedence(accept_list):
|
||||||
for renderer in renderers:
|
for renderer in renderers:
|
||||||
|
@ -310,14 +302,13 @@ class ResponseMixin(object):
|
||||||
{'detail': 'Could not satisfy the client\'s Accept header',
|
{'detail': 'Could not satisfy the client\'s Accept header',
|
||||||
'available_types': self._rendered_media_types})
|
'available_types': self._rendered_media_types})
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rendered_media_types(self):
|
def _rendered_media_types(self):
|
||||||
"""
|
"""
|
||||||
Return an list of all the media types that this view can render.
|
Return an list of all the media types that this view can render.
|
||||||
"""
|
"""
|
||||||
return [renderer.media_type for renderer in self.renderers]
|
return [renderer.media_type for renderer in self.renderers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rendered_formats(self):
|
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.
|
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.
|
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.
|
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
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
"""
|
"""
|
||||||
Returns the :obj:`user` for the current request, as determined by the set of
|
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'):
|
if not hasattr(self, '_user'):
|
||||||
self._user = self._authenticate()
|
self._user = self._authenticate()
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
|
|
||||||
def _authenticate(self):
|
def _authenticate(self):
|
||||||
"""
|
"""
|
||||||
Attempt to authenticate the request using each authentication class in turn.
|
Attempt to authenticate the request using each authentication class in turn.
|
||||||
|
@ -378,7 +367,6 @@ class AuthMixin(object):
|
||||||
return user
|
return user
|
||||||
return AnonymousUser()
|
return AnonymousUser()
|
||||||
|
|
||||||
|
|
||||||
# TODO: wrap this behavior around dispatch()
|
# TODO: wrap this behavior around dispatch()
|
||||||
def _check_permissions(self):
|
def _check_permissions(self):
|
||||||
"""
|
"""
|
||||||
|
@ -451,60 +439,108 @@ class ResourceMixin(object):
|
||||||
return self._resource.filter_response(obj)
|
return self._resource.filter_response(obj)
|
||||||
|
|
||||||
def get_bound_form(self, content=None, method=None):
|
def get_bound_form(self, content=None, method=None):
|
||||||
return self._resource.get_bound_form(content, method=method)
|
if hasattr(self._resource, 'get_bound_form'):
|
||||||
|
return self._resource.get_bound_form(content, method=method)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
##########
|
|
||||||
|
|
||||||
class InstanceMixin(object):
|
|
||||||
"""
|
|
||||||
`Mixin` class that is used to identify a `View` class as being the canonical identifier
|
|
||||||
for the resources it is mapped to.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def as_view(cls, **initkwargs):
|
|
||||||
"""
|
|
||||||
Store the callable object on the resource class that has been associated with this view.
|
|
||||||
"""
|
|
||||||
view = super(InstanceMixin, cls).as_view(**initkwargs)
|
|
||||||
resource = getattr(cls(**initkwargs), 'resource', None)
|
|
||||||
if resource:
|
|
||||||
# We do a little dance when we store the view callable...
|
|
||||||
# we need to store it wrapped in a 1-tuple, so that inspect will treat it
|
|
||||||
# as a function when we later look it up (rather than turning it into a method).
|
|
||||||
# This makes sure our URL reversing works ok.
|
|
||||||
resource.view_callable = (view,)
|
|
||||||
return view
|
|
||||||
|
|
||||||
|
|
||||||
########## Model Mixins ##########
|
########## 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
|
Behavior to read a `model` instance on GET requests
|
||||||
"""
|
"""
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
model = self.resource.model
|
model = self.resource.model
|
||||||
|
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args:
|
self.model_instance = self.get_instance(**query_kwargs)
|
||||||
# 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)
|
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
return self.model_instance
|
return self.model_instance
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(object):
|
class CreateModelMixin(ModelMixin):
|
||||||
"""
|
"""
|
||||||
Behavior to create a `model` instance on POST requests
|
Behavior to create a `model` instance on POST requests
|
||||||
"""
|
"""
|
||||||
|
@ -515,86 +551,66 @@ class CreateModelMixin(object):
|
||||||
content = dict(self.CONTENT)
|
content = dict(self.CONTENT)
|
||||||
m2m_data = {}
|
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:
|
for field in model._meta.many_to_many:
|
||||||
if content.has_key(field.name):
|
if field.name in content:
|
||||||
m2m_data[field.name] = (
|
m2m_data[field.name] = (
|
||||||
field.m2m_reverse_field_name(), content[field.name]
|
field.m2m_reverse_field_name(), content[field.name]
|
||||||
)
|
)
|
||||||
del content[field.name]
|
del content[field.name]
|
||||||
|
|
||||||
all_kw_args = dict(content.items() + kwargs.items())
|
instance = model(**self.get_instance_data(model, content, *args, **kwargs))
|
||||||
|
|
||||||
if args:
|
|
||||||
instance = model(pk=args[-1], **all_kw_args)
|
|
||||||
else:
|
|
||||||
instance = model(**all_kw_args)
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
for fieldname in m2m_data:
|
for fieldname in m2m_data:
|
||||||
manager = getattr(instance, fieldname)
|
manager = getattr(instance, fieldname)
|
||||||
|
|
||||||
if hasattr(manager, 'add'):
|
if hasattr(manager, 'add'):
|
||||||
manager.add(*m2m_data[fieldname][1])
|
manager.add(*m2m_data[fieldname][1])
|
||||||
else:
|
else:
|
||||||
data = {}
|
data = {}
|
||||||
data[manager.source_field_name] = instance
|
data[manager.source_field_name] = instance
|
||||||
|
|
||||||
for related_item in m2m_data[fieldname][1]:
|
for related_item in m2m_data[fieldname][1]:
|
||||||
data[m2m_data[fieldname][0]] = related_item
|
data[m2m_data[fieldname][0]] = related_item
|
||||||
manager.through(**data).save()
|
manager.through(**data).save()
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
if hasattr(instance, 'get_absolute_url'):
|
if hasattr(self.resource, 'url'):
|
||||||
headers['Location'] = self.resource(self).url(instance)
|
headers['Location'] = self.resource(self).url(instance)
|
||||||
return Response(status.HTTP_201_CREATED, instance, headers)
|
return Response(status.HTTP_201_CREATED, instance, headers)
|
||||||
|
|
||||||
|
|
||||||
class UpdateModelMixin(object):
|
class UpdateModelMixin(ModelMixin):
|
||||||
"""
|
"""
|
||||||
Behavior to update a `model` instance on PUT requests
|
Behavior to update a `model` instance on PUT requests
|
||||||
"""
|
"""
|
||||||
def put(self, request, *args, **kwargs):
|
def put(self, request, *args, **kwargs):
|
||||||
model = self.resource.model
|
model = self.resource.model
|
||||||
|
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
|
|
||||||
|
# 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:
|
try:
|
||||||
if args:
|
self.model_instance = self.get_instance(**query_kwargs)
|
||||||
# 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)
|
|
||||||
|
|
||||||
for (key, val) in self.CONTENT.items():
|
for (key, val) in self.CONTENT.items():
|
||||||
setattr(self.model_instance, key, val)
|
setattr(self.model_instance, key, val)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
self.model_instance = model(**self.CONTENT)
|
self.model_instance = model(**self.get_instance_data(model, self.CONTENT, *args, **kwargs))
|
||||||
self.model_instance.save()
|
|
||||||
|
|
||||||
self.model_instance.save()
|
self.model_instance.save()
|
||||||
return self.model_instance
|
return self.model_instance
|
||||||
|
|
||||||
|
|
||||||
class DeleteModelMixin(object):
|
class DeleteModelMixin(ModelMixin):
|
||||||
"""
|
"""
|
||||||
Behavior to delete a `model` instance on DELETE requests
|
Behavior to delete a `model` instance on DELETE requests
|
||||||
"""
|
"""
|
||||||
def delete(self, request, *args, **kwargs):
|
def delete(self, request, *args, **kwargs):
|
||||||
model = self.resource.model
|
model = self.resource.model
|
||||||
|
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args:
|
instance = self.get_instance(**query_kwargs)
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
|
||||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
|
||||||
else:
|
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
|
||||||
instance = model.objects.get(**kwargs)
|
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||||
|
|
||||||
|
@ -602,38 +618,119 @@ class DeleteModelMixin(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class ListModelMixin(object):
|
class ListModelMixin(ModelMixin):
|
||||||
"""
|
"""
|
||||||
Behavior to list a set of `model` instances on GET requests
|
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):
|
def get(self, request, *args, **kwargs):
|
||||||
model = self.resource.model
|
queryset = self.get_queryset()
|
||||||
|
ordering = self.get_ordering()
|
||||||
queryset = self.queryset if self.queryset is not None else model.objects.all()
|
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
||||||
|
|
||||||
if hasattr(self, 'resource'):
|
|
||||||
ordering = getattr(self.resource, 'ordering', None)
|
|
||||||
else:
|
|
||||||
ordering = None
|
|
||||||
|
|
||||||
|
queryset = queryset.filter(**query_kwargs)
|
||||||
if ordering:
|
if ordering:
|
||||||
args = as_tuple(ordering)
|
queryset = queryset.order_by(*ordering)
|
||||||
queryset = queryset.order_by(*args)
|
|
||||||
return queryset.filter(**kwargs)
|
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
|
||||||
|
|
|
@ -19,6 +19,11 @@ from djangorestframework import status
|
||||||
from djangorestframework.compat import yaml
|
from djangorestframework.compat import yaml
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
from djangorestframework.utils.mediatypes import media_type_matches
|
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__ = (
|
__all__ = (
|
||||||
|
@ -28,6 +33,7 @@ __all__ = (
|
||||||
'FormParser',
|
'FormParser',
|
||||||
'MultiPartParser',
|
'MultiPartParser',
|
||||||
'YAMLParser',
|
'YAMLParser',
|
||||||
|
'XMLParser'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,15 +51,15 @@ class BaseParser(object):
|
||||||
in case the parser needs to access any metadata on the :obj:`View` object.
|
in case the parser needs to access any metadata on the :obj:`View` object.
|
||||||
"""
|
"""
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def can_handle_request(self, content_type):
|
def can_handle_request(self, content_type):
|
||||||
"""
|
"""
|
||||||
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
Returns :const:`True` if this parser is able to deal with the given *content_type*.
|
||||||
|
|
||||||
The default implementation for this function is to check the *content_type*
|
The default implementation for this function is to check the *content_type*
|
||||||
argument against the :attr:`media_type` attribute set on the class to see if
|
argument against the :attr:`media_type` attribute set on the class to see if
|
||||||
they match.
|
they match.
|
||||||
|
|
||||||
This may be overridden to provide for other behavior, but typically you'll
|
This may be overridden to provide for other behavior, but typically you'll
|
||||||
instead want to just set the :attr:`media_type` attribute on the class.
|
instead want to just set the :attr:`media_type` attribute on the class.
|
||||||
"""
|
"""
|
||||||
|
@ -88,28 +94,26 @@ class JSONParser(BaseParser):
|
||||||
{'detail': 'JSON parse error - %s' % unicode(exc)})
|
{'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`.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
media_type = 'application/yaml'
|
return (yaml.safe_load(stream), None)
|
||||||
|
except (ValueError, yaml.parser.ParserError), exc:
|
||||||
def parse(self, stream):
|
content = {'detail': 'YAML parse error - %s' % unicode(exc)}
|
||||||
"""
|
raise ErrorResponse(status.HTTP_400_BAD_REQUEST, content)
|
||||||
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
|
|
||||||
|
|
||||||
class PlainTextParser(BaseParser):
|
class PlainTextParser(BaseParser):
|
||||||
"""
|
"""
|
||||||
|
@ -121,7 +125,7 @@ class PlainTextParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will simply be a string representing the body of the request.
|
`data` will simply be a string representing the body of the request.
|
||||||
`files` will always be `None`.
|
`files` will always be `None`.
|
||||||
"""
|
"""
|
||||||
|
@ -138,7 +142,7 @@ class FormParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||||
`files` will always be :const:`None`.
|
`files` will always be :const:`None`.
|
||||||
"""
|
"""
|
||||||
|
@ -156,21 +160,99 @@ class MultiPartParser(BaseParser):
|
||||||
def parse(self, stream):
|
def parse(self, stream):
|
||||||
"""
|
"""
|
||||||
Returns a 2-tuple of `(data, files)`.
|
Returns a 2-tuple of `(data, files)`.
|
||||||
|
|
||||||
`data` will be a :class:`QueryDict` containing all the form parameters.
|
`data` will be a :class:`QueryDict` containing all the form parameters.
|
||||||
`files` will be a :class:`QueryDict` containing all the form files.
|
`files` will be a :class:`QueryDict` containing all the form files.
|
||||||
"""
|
"""
|
||||||
upload_handlers = self.view.request._get_upload_handlers()
|
upload_handlers = self.view.request._get_upload_handlers()
|
||||||
try:
|
try:
|
||||||
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
|
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
|
||||||
|
return django_parser.parse()
|
||||||
except MultiPartParserError, exc:
|
except MultiPartParserError, exc:
|
||||||
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
raise ErrorResponse(status.HTTP_400_BAD_REQUEST,
|
||||||
{'detail': 'multipart parse error - %s' % unicode(exc)})
|
{'detail': 'multipart parse error - %s' % unicode(exc)})
|
||||||
return django_parser.parse()
|
|
||||||
|
|
||||||
DEFAULT_PARSERS = ( JSONParser,
|
|
||||||
FormParser,
|
|
||||||
MultiPartParser )
|
|
||||||
|
|
||||||
if YAMLParser:
|
class XMLParser(BaseParser):
|
||||||
DEFAULT_PARSERS += ( YAMLParser, )
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`permissions` module bundles a set of permission classes that are used
|
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
|
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.
|
class to your view by setting your View's :attr:`permissions` class attribute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ __all__ = (
|
||||||
'PerResourceThrottling'
|
'PerResourceThrottling'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
||||||
|
|
||||||
|
|
||||||
_403_FORBIDDEN_RESPONSE = ErrorResponse(
|
_403_FORBIDDEN_RESPONSE = ErrorResponse(
|
||||||
status.HTTP_403_FORBIDDEN,
|
status.HTTP_403_FORBIDDEN,
|
||||||
|
@ -40,7 +42,7 @@ class BasePermission(object):
|
||||||
Permission classes are always passed the current view on creation.
|
Permission classes are always passed the current view on creation.
|
||||||
"""
|
"""
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
def check_permission(self, auth):
|
def check_permission(self, auth):
|
||||||
"""
|
"""
|
||||||
Should simply return, or raise an :exc:`response.ErrorResponse`.
|
Should simply return, or raise an :exc:`response.ErrorResponse`.
|
||||||
|
@ -64,7 +66,7 @@ class IsAuthenticated(BasePermission):
|
||||||
|
|
||||||
def check_permission(self, user):
|
def check_permission(self, user):
|
||||||
if not user.is_authenticated():
|
if not user.is_authenticated():
|
||||||
raise _403_FORBIDDEN_RESPONSE
|
raise _403_FORBIDDEN_RESPONSE
|
||||||
|
|
||||||
|
|
||||||
class IsAdminUser(BasePermission):
|
class IsAdminUser(BasePermission):
|
||||||
|
@ -82,10 +84,56 @@ class IsUserOrIsAnonReadOnly(BasePermission):
|
||||||
The request is authenticated as a user, or is a read-only request.
|
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
|
if (not user.is_authenticated() and
|
||||||
self.view.method != 'GET' and
|
self.view.method not in SAFE_METHODS):
|
||||||
self.view.method != 'HEAD'):
|
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
|
raise _403_FORBIDDEN_RESPONSE
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,7 +148,7 @@ class BaseThrottle(BasePermission):
|
||||||
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
|
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
|
||||||
|
|
||||||
Previous request information used for throttling is stored in the cache.
|
Previous request information used for throttling is stored in the cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
attr_name = 'throttle'
|
attr_name = 'throttle'
|
||||||
default = '0/sec'
|
default = '0/sec'
|
||||||
|
@ -109,7 +157,7 @@ class BaseThrottle(BasePermission):
|
||||||
def get_cache_key(self):
|
def get_cache_key(self):
|
||||||
"""
|
"""
|
||||||
Should return a unique cache-key which can be used for throttling.
|
Should return a unique cache-key which can be used for throttling.
|
||||||
Muse be overridden.
|
Must be overridden.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -123,7 +171,7 @@ class BaseThrottle(BasePermission):
|
||||||
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
|
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.check_throttle()
|
self.check_throttle()
|
||||||
|
|
||||||
def check_throttle(self):
|
def check_throttle(self):
|
||||||
"""
|
"""
|
||||||
Implement the check to see if the request should be throttled.
|
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.key = self.get_cache_key()
|
||||||
self.history = cache.get(self.key, [])
|
self.history = cache.get(self.key, [])
|
||||||
self.now = self.timer()
|
self.now = self.timer()
|
||||||
|
|
||||||
# Drop any requests from the history which have now passed the
|
# Drop any requests from the history which have now passed the
|
||||||
# throttle duration
|
# throttle duration
|
||||||
while self.history and self.history[-1] <= self.now - self.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)
|
cache.set(self.key, self.history, self.duration)
|
||||||
header = 'status=SUCCESS; next=%s sec' % self.next()
|
header = 'status=SUCCESS; next=%s sec' % self.next()
|
||||||
self.view.add_header('X-Throttle', header)
|
self.view.add_header('X-Throttle', header)
|
||||||
|
|
||||||
def throttle_failure(self):
|
def throttle_failure(self):
|
||||||
"""
|
"""
|
||||||
Called when a request to the API has failed due to throttling.
|
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()
|
header = 'status=FAILURE; next=%s sec' % self.next()
|
||||||
self.view.add_header('X-Throttle', header)
|
self.view.add_header('X-Throttle', header)
|
||||||
raise _503_SERVICE_UNAVAILABLE
|
raise _503_SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
"""
|
"""
|
||||||
Returns the recommended next request time in seconds.
|
Returns the recommended next request time in seconds.
|
||||||
|
@ -188,7 +236,7 @@ class PerUserThrottling(BaseThrottle):
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self):
|
||||||
if self.auth.is_authenticated():
|
if self.auth.is_authenticated():
|
||||||
ident = str(self.auth)
|
ident = self.auth.id
|
||||||
else:
|
else:
|
||||||
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
ident = self.view.request.META.get('REMOTE_ADDR', None)
|
||||||
return 'throttle_user_%s' % ident
|
return 'throttle_user_%s' % ident
|
||||||
|
@ -205,7 +253,7 @@ class PerViewThrottling(BaseThrottle):
|
||||||
def get_cache_key(self):
|
def get_cache_key(self):
|
||||||
return 'throttle_view_%s' % self.view.__class__.__name__
|
return 'throttle_view_%s' % self.view.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
class PerResourceThrottling(BaseThrottle):
|
class PerResourceThrottling(BaseThrottle):
|
||||||
"""
|
"""
|
||||||
Limits the rate of API calls that may be used against all views on
|
Limits the rate of API calls that may be used against all views on
|
||||||
|
|
|
@ -3,7 +3,7 @@ Renderers are used to serialize a View's output into specific media types.
|
||||||
|
|
||||||
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
|
Django REST framework also provides HTML and PlainText renderers that help self-document the API,
|
||||||
by serializing the output along with documentation regarding the View, output status and headers,
|
by serializing the output along with documentation regarding the View, output status and headers,
|
||||||
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||||
"""
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -12,10 +12,9 @@ from django.template import RequestContext, loader
|
||||||
from django.utils import simplejson as json
|
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 import dict2xml, url_resolves
|
||||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
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.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
|
||||||
from djangorestframework import VERSION
|
from djangorestframework import VERSION
|
||||||
|
|
||||||
|
@ -26,6 +25,7 @@ __all__ = (
|
||||||
'BaseRenderer',
|
'BaseRenderer',
|
||||||
'TemplateRenderer',
|
'TemplateRenderer',
|
||||||
'JSONRenderer',
|
'JSONRenderer',
|
||||||
|
'JSONPRenderer',
|
||||||
'DocumentingHTMLRenderer',
|
'DocumentingHTMLRenderer',
|
||||||
'DocumentingXHTMLRenderer',
|
'DocumentingXHTMLRenderer',
|
||||||
'DocumentingPlainTextRenderer',
|
'DocumentingPlainTextRenderer',
|
||||||
|
@ -39,7 +39,7 @@ class BaseRenderer(object):
|
||||||
All renderers must extend this class, set the :attr:`media_type` attribute,
|
All renderers must extend this class, set the :attr:`media_type` attribute,
|
||||||
and override the :meth:`render` method.
|
and override the :meth:`render` method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_FORMAT_QUERY_PARAM = 'format'
|
_FORMAT_QUERY_PARAM = 'format'
|
||||||
|
|
||||||
media_type = None
|
media_type = None
|
||||||
|
@ -81,7 +81,7 @@ class BaseRenderer(object):
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,6 +113,28 @@ class JSONRenderer(BaseRenderer):
|
||||||
return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys)
|
return json.dumps(obj, cls=DateTimeAwareJSONEncoder, indent=indent, sort_keys=sort_keys)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONPRenderer(JSONRenderer):
|
||||||
|
"""
|
||||||
|
Renderer which serializes to JSONP
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = 'application/json-p'
|
||||||
|
format = 'json-p'
|
||||||
|
renderer_class = JSONRenderer
|
||||||
|
callback_parameter = 'callback'
|
||||||
|
|
||||||
|
def _get_callback(self):
|
||||||
|
return self.view.request.GET.get(self.callback_parameter, self.callback_parameter)
|
||||||
|
|
||||||
|
def _get_renderer(self):
|
||||||
|
return self.renderer_class(self.view)
|
||||||
|
|
||||||
|
def render(self, obj=None, media_type=None):
|
||||||
|
callback = self._get_callback()
|
||||||
|
json = self._get_renderer().render(obj, media_type)
|
||||||
|
return "%s(%s);" % (callback, json)
|
||||||
|
|
||||||
|
|
||||||
class XMLRenderer(BaseRenderer):
|
class XMLRenderer(BaseRenderer):
|
||||||
"""
|
"""
|
||||||
Renderer which serializes to XML.
|
Renderer which serializes to XML.
|
||||||
|
@ -130,25 +152,22 @@ class XMLRenderer(BaseRenderer):
|
||||||
return dict2xml(obj)
|
return dict2xml(obj)
|
||||||
|
|
||||||
|
|
||||||
if yaml:
|
class YAMLRenderer(BaseRenderer):
|
||||||
class YAMLRenderer(BaseRenderer):
|
"""
|
||||||
"""
|
Renderer which serializes to YAML.
|
||||||
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 ''
|
|
||||||
|
|
||||||
return yaml.dump(obj)
|
media_type = 'application/yaml'
|
||||||
else:
|
format = 'yaml'
|
||||||
YAMLRenderer = None
|
|
||||||
|
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):
|
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.)
|
# 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:
|
if not renderers:
|
||||||
return '[No renderers were found]'
|
return '[No renderers were found]'
|
||||||
|
|
||||||
|
@ -200,9 +219,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
content = renderers[0](view).render(obj, media_type)
|
content = renderers[0](view).render(obj, media_type)
|
||||||
if not all(char in string.printable for char in content):
|
if not all(char in string.printable for char in content):
|
||||||
return '[%d bytes of binary content]'
|
return '[%d bytes of binary content]'
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
def _get_form_instance(self, view, method):
|
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)
|
form_instance = view.get_bound_form(view.response.cleaned_content, method=method)
|
||||||
if form_instance and not form_instance.is_valid():
|
if form_instance and not form_instance.is_valid():
|
||||||
form_instance = None
|
form_instance = None
|
||||||
except:
|
except Exception:
|
||||||
form_instance = None
|
form_instance = None
|
||||||
|
|
||||||
# If we still don't have a form instance then try to get an unbound form
|
# If we still don't have a form instance then try to get an unbound form
|
||||||
if not form_instance:
|
if not form_instance:
|
||||||
try:
|
try:
|
||||||
form_instance = view.get_bound_form(method=method)
|
form_instance = view.get_bound_form(method=method)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
|
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
|
||||||
if not form_instance:
|
if not form_instance:
|
||||||
form_instance = self._get_generic_content_form(view)
|
form_instance = self._get_generic_content_form(view)
|
||||||
|
|
||||||
return form_instance
|
|
||||||
|
|
||||||
|
return form_instance
|
||||||
|
|
||||||
def _get_generic_content_form(self, view):
|
def _get_generic_content_form(self, view):
|
||||||
"""
|
"""
|
||||||
|
@ -275,6 +292,19 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
# Okey doke, let's do it
|
# Okey doke, let's do it
|
||||||
return GenericContentForm(view)
|
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):
|
def render(self, obj=None, media_type=None):
|
||||||
"""
|
"""
|
||||||
|
@ -296,15 +326,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
login_url = None
|
login_url = None
|
||||||
logout_url = None
|
logout_url = None
|
||||||
|
|
||||||
name = get_name(self.view)
|
name = self.get_name()
|
||||||
description = get_description(self.view)
|
description = self.get_description()
|
||||||
|
|
||||||
markeddown = None
|
|
||||||
if apply_markdown:
|
|
||||||
try:
|
|
||||||
markeddown = apply_markdown(description)
|
|
||||||
except AttributeError:
|
|
||||||
markeddown = None
|
|
||||||
|
|
||||||
breadcrumb_list = get_breadcrumbs(self.view.request.path)
|
breadcrumb_list = get_breadcrumbs(self.view.request.path)
|
||||||
|
|
||||||
|
@ -312,23 +335,19 @@ class DocumentingTemplateRenderer(BaseRenderer):
|
||||||
context = RequestContext(self.view.request, {
|
context = RequestContext(self.view.request, {
|
||||||
'content': content,
|
'content': content,
|
||||||
'view': self.view,
|
'view': self.view,
|
||||||
'request': self.view.request, # TODO: remove
|
'request': self.view.request,
|
||||||
'response': self.view.response,
|
'response': self.view.response,
|
||||||
'description': description,
|
'description': description,
|
||||||
'name': name,
|
'name': name,
|
||||||
'version': VERSION,
|
'version': VERSION,
|
||||||
'markeddown': markeddown,
|
|
||||||
'breadcrumblist': breadcrumb_list,
|
'breadcrumblist': breadcrumb_list,
|
||||||
'available_formats': self.view._rendered_formats,
|
'available_formats': self.view._rendered_formats,
|
||||||
'put_form': put_form_instance,
|
'put_form': put_form_instance,
|
||||||
'post_form': post_form_instance,
|
'post_form': post_form_instance,
|
||||||
'login_url': login_url,
|
|
||||||
'logout_url': logout_url,
|
|
||||||
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
|
'FORMAT_PARAM': self._FORMAT_QUERY_PARAM,
|
||||||
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
|
'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None),
|
||||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ret = template.render(context)
|
ret = template.render(context)
|
||||||
|
|
||||||
# Munge DELETE Response code to allow us to return content
|
# Munge DELETE Response code to allow us to return content
|
||||||
|
@ -348,7 +367,7 @@ class DocumentingHTMLRenderer(DocumentingTemplateRenderer):
|
||||||
|
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
format = 'html'
|
format = 'html'
|
||||||
template = 'renderer.html'
|
template = 'djangorestframework/api.html'
|
||||||
|
|
||||||
|
|
||||||
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
|
class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
|
||||||
|
@ -360,7 +379,7 @@ class DocumentingXHTMLRenderer(DocumentingTemplateRenderer):
|
||||||
|
|
||||||
media_type = 'application/xhtml+xml'
|
media_type = 'application/xhtml+xml'
|
||||||
format = 'xhtml'
|
format = 'xhtml'
|
||||||
template = 'renderer.html'
|
template = 'djangorestframework/api.html'
|
||||||
|
|
||||||
|
|
||||||
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
||||||
|
@ -372,14 +391,19 @@ class DocumentingPlainTextRenderer(DocumentingTemplateRenderer):
|
||||||
|
|
||||||
media_type = 'text/plain'
|
media_type = 'text/plain'
|
||||||
format = 'txt'
|
format = 'txt'
|
||||||
template = 'renderer.txt'
|
template = 'djangorestframework/api.txt'
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_RENDERERS = ( JSONRenderer,
|
DEFAULT_RENDERERS = (
|
||||||
DocumentingHTMLRenderer,
|
JSONRenderer,
|
||||||
DocumentingXHTMLRenderer,
|
JSONPRenderer,
|
||||||
DocumentingPlainTextRenderer,
|
DocumentingHTMLRenderer,
|
||||||
XMLRenderer )
|
DocumentingXHTMLRenderer,
|
||||||
|
DocumentingPlainTextRenderer,
|
||||||
|
XMLRenderer
|
||||||
|
)
|
||||||
|
|
||||||
if YAMLRenderer:
|
if yaml:
|
||||||
DEFAULT_RENDERERS += (YAMLRenderer,)
|
DEFAULT_RENDERERS += (YAMLRenderer, )
|
||||||
|
else:
|
||||||
|
YAMLRenderer = None
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
from django import forms
|
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.response import ErrorResponse
|
||||||
from djangorestframework.serializer import Serializer, _SkipField
|
from djangorestframework.serializer import Serializer
|
||||||
from djangorestframework.utils import as_tuple
|
from djangorestframework.utils import as_tuple
|
||||||
|
|
||||||
import decimal
|
|
||||||
import inspect
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseResource(Serializer):
|
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
|
fields = None
|
||||||
include = None
|
include = None
|
||||||
|
@ -27,14 +17,16 @@ class BaseResource(Serializer):
|
||||||
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
||||||
super(BaseResource, self).__init__(depth, stack, **kwargs)
|
super(BaseResource, self).__init__(depth, stack, **kwargs)
|
||||||
self.view = view
|
self.view = view
|
||||||
|
self.request = getattr(view, 'request', None)
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given the request content return the cleaned, validated content.
|
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
|
return data
|
||||||
|
|
||||||
def filter_response(self, obj):
|
def filter_response(self, obj):
|
||||||
"""
|
"""
|
||||||
Given the response content, filter it into a serializable object.
|
Given the response content, filter it into a serializable object.
|
||||||
|
@ -45,13 +37,14 @@ class BaseResource(Serializer):
|
||||||
class Resource(BaseResource):
|
class Resource(BaseResource):
|
||||||
"""
|
"""
|
||||||
A Resource determines how a python object maps to some serializable data.
|
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 attribute refers to the Django Model which this Resource maps to.
|
||||||
# (The Model's class, rather than an instance of the Model)
|
# (The Model's class, rather than an instance of the Model)
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
# By default the set of returned fields will be the set of:
|
# By default the set of returned fields will be the set of:
|
||||||
#
|
#
|
||||||
# 0. All the fields on the model, excluding 'id'.
|
# 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`.
|
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):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given some content as input return some cleaned, validated content.
|
Given some content as input return some cleaned, validated content.
|
||||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
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.
|
On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
|
||||||
If the :obj:`'errors'` key exists it is a list of strings of non-field errors.
|
If the :obj:`'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)
|
return self._validate(data, files)
|
||||||
|
|
||||||
|
|
||||||
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None):
|
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.
|
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
|
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.
|
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
|
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`
|
to be populated when an empty dict is supplied in `data`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We'd like nice error messages even if no content is supplied.
|
# We'd like nice error messages even if no content is supplied.
|
||||||
# Typically if an empty dict is given to a form Django will
|
# Typically if an empty dict is given to a form Django will
|
||||||
# return .is_valid() == False, but .errors == {}
|
# 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:
|
if fake_data:
|
||||||
data[fake_data] = '_fake_data'
|
data[fake_data] = '_fake_data'
|
||||||
allowed_extra_fields = tuple(allowed_extra_fields) + ('_fake_data',)
|
allowed_extra_fields = tuple(allowed_extra_fields) + ('_fake_data',)
|
||||||
|
|
||||||
bound_form = self.get_bound_form(data, files)
|
bound_form = self.get_bound_form(data, files)
|
||||||
|
|
||||||
if bound_form is None:
|
if bound_form is None:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
self.view.bound_form_instance = bound_form
|
self.view.bound_form_instance = bound_form
|
||||||
|
|
||||||
data = data and data or {}
|
data = data and data or {}
|
||||||
files = files and files 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...
|
# 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 = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
|
||||||
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
|
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
|
||||||
|
|
||||||
# Check using both regular validation, and our stricter no additional fields rule
|
# Check using both regular validation, and our stricter no additional fields rule
|
||||||
if bound_form.is_valid() and not unknown_fields:
|
if bound_form.is_valid() and (self.allow_unknown_form_fields or not unknown_fields):
|
||||||
# Validation succeeded...
|
# Validation succeeded...
|
||||||
cleaned_data = bound_form.cleaned_data
|
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.
|
# If we've already set fake_dict and we're still here, fallback gracefully.
|
||||||
detail = {u'errors': [u'No content was supplied.']}
|
detail = {u'errors': [u'No content was supplied.']}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Add any non-field errors
|
# Add any non-field errors
|
||||||
if bound_form.non_field_errors():
|
if bound_form.non_field_errors():
|
||||||
detail[u'errors'] = 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
|
# Add any unknown field errors
|
||||||
for key in unknown_fields:
|
for key in unknown_fields:
|
||||||
field_errors[key] = [u'This field does not exist.']
|
field_errors[key] = [u'This field does not exist.']
|
||||||
|
|
||||||
if field_errors:
|
if field_errors:
|
||||||
detail[u'field-errors'] = field_errors
|
detail[u'field_errors'] = field_errors
|
||||||
|
|
||||||
# Return HTTP 400 response (BAD REQUEST)
|
# Return HTTP 400 response (BAD REQUEST)
|
||||||
raise ErrorResponse(400, detail)
|
raise ErrorResponse(400, detail)
|
||||||
|
|
||||||
|
|
||||||
def get_form_class(self, method=None):
|
def get_form_class(self, method=None):
|
||||||
"""
|
"""
|
||||||
Returns the form class used to validate this resource.
|
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)
|
form = getattr(self.view, '%s_form' % method.lower(), form)
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, files=None, method=None):
|
def get_bound_form(self, data=None, files=None, method=None):
|
||||||
"""
|
"""
|
||||||
|
@ -217,29 +215,12 @@ class FormResource(Resource):
|
||||||
return form()
|
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):
|
class ModelResource(FormResource):
|
||||||
"""
|
"""
|
||||||
Resource class that uses forms for validation and otherwise falls back to a model form if no form is set.
|
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.
|
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
|
form = None
|
||||||
"""
|
"""
|
||||||
The form class that should be used for request validation.
|
The form class that should be used for request validation.
|
||||||
|
@ -258,23 +239,22 @@ class ModelResource(FormResource):
|
||||||
fields = None
|
fields = None
|
||||||
"""
|
"""
|
||||||
The list of fields to use on the output.
|
The list of fields to use on the output.
|
||||||
|
|
||||||
May be any of:
|
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 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 model.
|
||||||
The name of an attribute on the resource.
|
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 model, with a signature like ``func(self)``.
|
||||||
The name of a method on the resource, with a signature like ``func(self, instance)``.
|
The name of a method on the resource, with a signature like ``func(self, instance)``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
exclude = ('id', 'pk')
|
exclude = ('id', 'pk')
|
||||||
"""
|
"""
|
||||||
The list of fields to exclude. This is only used if :attr:`fields` is not set.
|
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.
|
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
|
self.model = getattr(view, 'model', None) or self.model
|
||||||
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
def validate_request(self, data, files=None):
|
||||||
"""
|
"""
|
||||||
Given some content as input return some cleaned, validated content.
|
Given some content as input return some cleaned, validated content.
|
||||||
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure.
|
||||||
|
|
||||||
Validation is standard form or model form validation,
|
Validation is standard form or model form validation,
|
||||||
with an additional constraint that no extra unknown fields may be supplied,
|
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,
|
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)
|
return self._validate(data, files, allowed_extra_fields=self._property_fields_set)
|
||||||
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, files=None, method=None):
|
def get_bound_form(self, data=None, files=None, method=None):
|
||||||
"""
|
"""
|
||||||
Given some content return a ``Form`` instance bound to that content.
|
Given some content return a ``Form`` instance bound to that content.
|
||||||
|
@ -339,49 +317,6 @@ class ModelResource(FormResource):
|
||||||
|
|
||||||
return form()
|
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
|
@property
|
||||||
def _model_fields_set(self):
|
def _model_fields_set(self):
|
||||||
"""
|
"""
|
||||||
|
@ -389,11 +324,11 @@ class ModelResource(FormResource):
|
||||||
"""
|
"""
|
||||||
model_fields = set(field.name for field in self.model._meta.fields)
|
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.fields))
|
||||||
|
|
||||||
return model_fields - set(as_tuple(self.exclude))
|
return model_fields - set(as_tuple(self.exclude))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _property_fields_set(self):
|
def _property_fields_set(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""
|
"""
|
||||||
The :mod:`response` module provides Response classes you can use in your
|
The :mod:`response` module provides Response classes you can use in your
|
||||||
views to return a certain HTTP response. Typically a response is *rendered*
|
views to return a certain HTTP response. Typically a response is *rendered*
|
||||||
into a HTTP response depending on what renderers are set on your view and
|
into a HTTP response depending on what renderers are set on your view and
|
||||||
als depending on the accept header of the request.
|
als depending on the accept header of the request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
@ -11,6 +11,7 @@ __all__ = ('Response', 'ErrorResponse')
|
||||||
|
|
||||||
# TODO: remove raw_content/cleaned_content and just use content?
|
# TODO: remove raw_content/cleaned_content and just use content?
|
||||||
|
|
||||||
|
|
||||||
class Response(object):
|
class Response(object):
|
||||||
"""
|
"""
|
||||||
An HttpResponse that may include content that hasn't yet been serialized.
|
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.raw_content = content # content prior to filtering
|
||||||
self.cleaned_content = content # content after filtering
|
self.cleaned_content = content # content after filtering
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
"""
|
"""
|
||||||
|
@ -33,7 +34,7 @@ class Response(object):
|
||||||
return STATUS_CODE_TEXT.get(self.status, '')
|
return STATUS_CODE_TEXT.get(self.status, '')
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseException):
|
class ErrorResponse(Exception):
|
||||||
"""
|
"""
|
||||||
An exception representing an Response that should be returned immediately.
|
An exception representing an Response that should be returned immediately.
|
||||||
Any content should be serialized as-is, without being filtered.
|
Any content should be serialized as-is, without being filtered.
|
||||||
|
|
20
djangorestframework/reverse.py
Normal file
20
djangorestframework/reverse.py
Normal 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)
|
|
@ -35,7 +35,6 @@ def main():
|
||||||
else:
|
else:
|
||||||
test_runner = TestRunner()
|
test_runner = TestRunner()
|
||||||
failures = test_runner.run_tests(['djangorestframework'])
|
failures = test_runner.run_tests(['djangorestframework'])
|
||||||
|
|
||||||
cov.stop()
|
cov.stop()
|
||||||
|
|
||||||
# Discover the list of all modules that we should test coverage for
|
# 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
|
# Drop the compat module from coverage, since we're not interested in the coverage
|
||||||
# of a module which is specifically for resolving environment dependant imports.
|
# of a module which is specifically for resolving environment dependant imports.
|
||||||
# (Because we'll end up getting different coverage reports for it for each environment)
|
# (Because we'll end up getting different coverage reports for it for each environment)
|
||||||
if 'compat.py' in files:
|
if 'compat.py' in files:
|
||||||
files.remove('compat.py')
|
files.remove('compat.py')
|
||||||
|
|
||||||
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
|
||||||
|
|
||||||
cov.report(cov_files)
|
cov.report(cov_files)
|
||||||
|
|
|
@ -16,12 +16,12 @@ from django.test.utils import get_runner
|
||||||
def usage():
|
def usage():
|
||||||
return """
|
return """
|
||||||
Usage: python runtests.py [UnitTestClass].[method]
|
Usage: python runtests.py [UnitTestClass].[method]
|
||||||
|
|
||||||
You can pass the Class name of the `UnitTestClass` you want to test.
|
You can pass the Class name of the `UnitTestClass` you want to test.
|
||||||
|
|
||||||
Append a method name if you only want to test a specific method of that class.
|
Append a method name if you only want to test a specific method of that class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
TestRunner = get_runner(settings)
|
TestRunner = get_runner(settings)
|
||||||
|
|
||||||
|
|
|
@ -53,11 +53,6 @@ MEDIA_ROOT = ''
|
||||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||||
MEDIA_URL = ''
|
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.
|
# Make this unique, and don't share it with anybody.
|
||||||
SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
|
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:
|
# Uncomment the next line to enable admin documentation:
|
||||||
# 'django.contrib.admindocs',
|
# 'django.contrib.admindocs',
|
||||||
'djangorestframework',
|
'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.
|
# OAuth support is optional, so we only test oauth if it's installed.
|
||||||
try:
|
try:
|
||||||
import oauth_provider
|
import oauth_provider
|
||||||
|
|
|
@ -4,4 +4,4 @@ Blank URLConf just to keep runtests.py happy.
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
Customizable serialization.
|
Customizable serialization.
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet, RawQuerySet
|
||||||
from django.db.models.fields.related import RelatedField
|
|
||||||
from django.utils.encoding import smart_unicode, is_protected_type, smart_str
|
from django.utils.encoding import smart_unicode, is_protected_type, smart_str
|
||||||
|
|
||||||
import decimal
|
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
@ -18,23 +16,18 @@ _serializers = {}
|
||||||
|
|
||||||
def _field_to_tuple(field):
|
def _field_to_tuple(field):
|
||||||
"""
|
"""
|
||||||
Convert an item in the `fields` attribute into a 2-tuple.
|
Convert an item in the `fields` attribute into a 2-tuple.
|
||||||
"""
|
"""
|
||||||
if isinstance(field, (tuple, list)):
|
if isinstance(field, (tuple, list)):
|
||||||
return (field[0], field[1])
|
return (field[0], field[1])
|
||||||
return (field, None)
|
return (field, None)
|
||||||
|
|
||||||
|
|
||||||
def _fields_to_list(fields):
|
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 ()]
|
return [_field_to_tuple(field) 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 ()])
|
|
||||||
|
|
||||||
|
|
||||||
class _SkipField(Exception):
|
class _SkipField(Exception):
|
||||||
|
@ -52,7 +45,7 @@ class _RegisterSerializer(type):
|
||||||
"""
|
"""
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
# Build the class and register it.
|
# Build the class and register it.
|
||||||
ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
|
ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
|
||||||
_serializers[name] = ret
|
_serializers[name] = ret
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -61,19 +54,19 @@ class Serializer(object):
|
||||||
"""
|
"""
|
||||||
Converts python objects into plain old native types suitable for
|
Converts python objects into plain old native types suitable for
|
||||||
serialization. In particular it handles models and querysets.
|
serialization. In particular it handles models and querysets.
|
||||||
|
|
||||||
The output format is specified by setting a number of attributes
|
The output format is specified by setting a number of attributes
|
||||||
on the class.
|
on the class.
|
||||||
|
|
||||||
You may also override any of the serialization methods, to provide
|
You may also override any of the serialization methods, to provide
|
||||||
for more flexible behavior.
|
for more flexible behavior.
|
||||||
|
|
||||||
Valid output types include anything that may be directly rendered into
|
Valid output types include anything that may be directly rendered into
|
||||||
json, xml etc...
|
json, xml etc...
|
||||||
"""
|
"""
|
||||||
__metaclass__ = _RegisterSerializer
|
__metaclass__ = _RegisterSerializer
|
||||||
|
|
||||||
fields = ()
|
fields = ()
|
||||||
"""
|
"""
|
||||||
Specify the fields to be serialized on a model or dict.
|
Specify the fields to be serialized on a model or dict.
|
||||||
Overrides `include` and `exclude`.
|
Overrides `include` and `exclude`.
|
||||||
|
@ -104,17 +97,12 @@ class Serializer(object):
|
||||||
The maximum depth to serialize to, or `None`.
|
The maximum depth to serialize to, or `None`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, depth=None, stack=[], **kwargs):
|
def __init__(self, depth=None, stack=[], **kwargs):
|
||||||
if depth is not None:
|
if depth is not None:
|
||||||
self.depth = depth
|
self.depth = depth
|
||||||
self.stack = stack
|
self.stack = stack
|
||||||
|
|
||||||
|
|
||||||
def get_fields(self, obj):
|
def get_fields(self, obj):
|
||||||
"""
|
|
||||||
Return the set of field names/keys to use for a model instance/dict.
|
|
||||||
"""
|
|
||||||
fields = self.fields
|
fields = self.fields
|
||||||
|
|
||||||
# If `fields` is not set, we use the default fields and modify
|
# If `fields` is not set, we use the default fields and modify
|
||||||
|
@ -125,12 +113,8 @@ class Serializer(object):
|
||||||
exclude = self.exclude or ()
|
exclude = self.exclude or ()
|
||||||
fields = set(default + list(include)) - set(exclude)
|
fields = set(default + list(include)) - set(exclude)
|
||||||
|
|
||||||
else:
|
|
||||||
fields = _fields_to_list(self.fields)
|
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def get_default_fields(self, obj):
|
def get_default_fields(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the default list of field names/keys for a model instance/dict.
|
Return the default list of field names/keys for a model instance/dict.
|
||||||
|
@ -142,15 +126,12 @@ class Serializer(object):
|
||||||
else:
|
else:
|
||||||
return obj.keys()
|
return obj.keys()
|
||||||
|
|
||||||
|
def get_related_serializer(self, info):
|
||||||
def get_related_serializer(self, key):
|
|
||||||
info = _fields_to_dict(self.fields).get(key, None)
|
|
||||||
|
|
||||||
# If an element in `fields` is a 2-tuple of (str, tuple)
|
# If an element in `fields` is a 2-tuple of (str, tuple)
|
||||||
# then the second element of the tuple is the fields to
|
# then the second element of the tuple is the fields to
|
||||||
# set on the related serializer
|
# set on the related serializer
|
||||||
if isinstance(info, (list, tuple)):
|
if isinstance(info, (list, tuple)):
|
||||||
class OnTheFlySerializer(Serializer):
|
class OnTheFlySerializer(self.__class__):
|
||||||
fields = info
|
fields = info
|
||||||
return OnTheFlySerializer
|
return OnTheFlySerializer
|
||||||
|
|
||||||
|
@ -168,11 +149,10 @@ class Serializer(object):
|
||||||
# Similar to what Django does for cyclically related models.
|
# Similar to what Django does for cyclically related models.
|
||||||
elif isinstance(info, str) and info in _serializers:
|
elif isinstance(info, str) and info in _serializers:
|
||||||
return _serializers[info]
|
return _serializers[info]
|
||||||
|
|
||||||
# Otherwise use `related_serializer` or fall back to `Serializer`
|
# Otherwise use `related_serializer` or fall back to `Serializer`
|
||||||
return getattr(self, 'related_serializer') or Serializer
|
return getattr(self, 'related_serializer') or Serializer
|
||||||
|
|
||||||
|
|
||||||
def serialize_key(self, key):
|
def serialize_key(self, key):
|
||||||
"""
|
"""
|
||||||
Keys serialize to their string value,
|
Keys serialize to their string value,
|
||||||
|
@ -180,13 +160,12 @@ class Serializer(object):
|
||||||
"""
|
"""
|
||||||
return self.rename.get(smart_str(key), smart_str(key))
|
return self.rename.get(smart_str(key), smart_str(key))
|
||||||
|
|
||||||
|
def serialize_val(self, key, obj, related_info):
|
||||||
def serialize_val(self, key, obj):
|
|
||||||
"""
|
"""
|
||||||
Convert a model field or dict value into a serializable representation.
|
Convert a model field or dict value into a serializable representation.
|
||||||
"""
|
"""
|
||||||
related_serializer = self.get_related_serializer(key)
|
related_serializer = self.get_related_serializer(related_info)
|
||||||
|
|
||||||
if self.depth is None:
|
if self.depth is None:
|
||||||
depth = None
|
depth = None
|
||||||
elif self.depth <= 0:
|
elif self.depth <= 0:
|
||||||
|
@ -200,8 +179,8 @@ class Serializer(object):
|
||||||
stack = self.stack[:]
|
stack = self.stack[:]
|
||||||
stack.append(obj)
|
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):
|
def serialize_max_depth(self, obj):
|
||||||
"""
|
"""
|
||||||
|
@ -210,7 +189,6 @@ class Serializer(object):
|
||||||
"""
|
"""
|
||||||
raise _SkipField
|
raise _SkipField
|
||||||
|
|
||||||
|
|
||||||
def serialize_recursion(self, obj):
|
def serialize_recursion(self, obj):
|
||||||
"""
|
"""
|
||||||
Determine how objects should be serialized if recursion occurs.
|
Determine how objects should be serialized if recursion occurs.
|
||||||
|
@ -218,7 +196,6 @@ class Serializer(object):
|
||||||
"""
|
"""
|
||||||
raise _SkipField
|
raise _SkipField
|
||||||
|
|
||||||
|
|
||||||
def serialize_model(self, instance):
|
def serialize_model(self, instance):
|
||||||
"""
|
"""
|
||||||
Given a model instance or dict, serialize it to a dict..
|
Given a model instance or dict, serialize it to a dict..
|
||||||
|
@ -227,69 +204,69 @@ class Serializer(object):
|
||||||
|
|
||||||
fields = self.get_fields(instance)
|
fields = self.get_fields(instance)
|
||||||
|
|
||||||
# serialize each required field
|
# serialize each required field
|
||||||
for fname in fields:
|
for fname, related_info in _fields_to_list(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
|
|
||||||
|
|
||||||
try:
|
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)
|
key = self.serialize_key(fname)
|
||||||
val = self.serialize_val(fname, obj)
|
val = self.serialize_val(fname, obj, related_info)
|
||||||
data[key] = val
|
data[key] = val
|
||||||
except _SkipField:
|
except _SkipField:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def serialize_iter(self, obj):
|
def serialize_iter(self, obj):
|
||||||
"""
|
"""
|
||||||
Convert iterables into a serializable representation.
|
Convert iterables into a serializable representation.
|
||||||
"""
|
"""
|
||||||
return [self.serialize(item) for item in obj]
|
return [self.serialize(item) for item in obj]
|
||||||
|
|
||||||
|
|
||||||
def serialize_func(self, obj):
|
def serialize_func(self, obj):
|
||||||
"""
|
"""
|
||||||
Convert no-arg methods and functions into a serializable representation.
|
Convert no-arg methods and functions into a serializable representation.
|
||||||
"""
|
"""
|
||||||
return self.serialize(obj())
|
return self.serialize(obj())
|
||||||
|
|
||||||
|
|
||||||
def serialize_manager(self, obj):
|
def serialize_manager(self, obj):
|
||||||
"""
|
"""
|
||||||
Convert a model manager into a serializable representation.
|
Convert a model manager into a serializable representation.
|
||||||
"""
|
"""
|
||||||
return self.serialize_iter(obj.all())
|
return self.serialize_iter(obj.all())
|
||||||
|
|
||||||
|
|
||||||
def serialize_fallback(self, obj):
|
def serialize_fallback(self, obj):
|
||||||
"""
|
"""
|
||||||
Convert any unhandled object into a serializable representation.
|
Convert any unhandled object into a serializable representation.
|
||||||
"""
|
"""
|
||||||
return smart_unicode(obj, strings_only=True)
|
return smart_unicode(obj, strings_only=True)
|
||||||
|
|
||||||
|
def serialize(self, obj, request=None):
|
||||||
def serialize(self, obj):
|
|
||||||
"""
|
"""
|
||||||
Convert any object into a serializable representation.
|
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)):
|
if isinstance(obj, (dict, models.Model)):
|
||||||
# Model instances & dictionaries
|
# Model instances & dictionaries
|
||||||
return self.serialize_model(obj)
|
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
|
# basic iterables
|
||||||
return self.serialize_iter(obj)
|
return self.serialize_iter(obj)
|
||||||
elif isinstance(obj, models.Manager):
|
elif isinstance(obj, models.Manager):
|
||||||
|
|
1209
djangorestframework/static/djangorestframework/css/style.css
Normal file
1209
djangorestframework/static/djangorestframework/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
|
@ -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
|
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Verbose format
|
|
||||||
HTTP_100_CONTINUE = 100
|
HTTP_100_CONTINUE = 100
|
||||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||||
HTTP_200_OK = 200
|
HTTP_200_OK = 200
|
||||||
|
@ -47,46 +46,3 @@ HTTP_502_BAD_GATEWAY = 502
|
||||||
HTTP_503_SERVICE_UNAVAILABLE = 503
|
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||||
HTTP_504_GATEWAY_TIMEOUT = 504
|
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
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
|
|
|
@ -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> </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>
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "djangorestframework/base.html" %}
|
||||||
|
|
||||||
|
{# Override this template in your own templates directory to customize #}
|
|
@ -1,8 +1,8 @@
|
||||||
{{ name }}
|
{% autoescape off %}{{ name }}
|
||||||
|
|
||||||
{{ description }}
|
{{ 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 }}
|
{% for key, val in response.headers.items %}{{ key }}: {{ val }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ content }}{% endautoescape %}
|
{{ content }}{% endautoescape %}
|
|
@ -1,43 +1,60 @@
|
||||||
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
"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">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}djangorestframework/css/style.css'/>
|
||||||
/* Override some of the Django admin styling */
|
{% block extrastyle %}{% endblock %}
|
||||||
#site-name a {color: #F4F379 !important;}
|
<title>{% block title %}Django REST framework - {{ name }}{% endblock %}</title>
|
||||||
.errorlist {display: inline !important}
|
{% block extrahead %}{% endblock %}
|
||||||
.errorlist li {display: inline !important; background: white !important; color: black !important; border: 0 !important;}
|
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
|
||||||
/* Custom styles */
|
</head>
|
||||||
.version{font-size:8px;}
|
<body class="{% block bodyclass %}{% endblock %}">
|
||||||
</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>
|
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div id="branding">
|
<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>
|
||||||
<div id="user-tools">
|
<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>
|
</div>
|
||||||
|
{% block nav-global %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
|
{% block breadcrumbs %}
|
||||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||||
<a href="{{breadcrumb_url}}">{{breadcrumb_name}}</a> {% if not forloop.last %}›{% endif %}
|
<a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a> {% if not forloop.last %}›{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
<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'>
|
<div class='content-main'>
|
||||||
<h1>{{ name }}</h1>
|
<h1>{{ name }}</h1>
|
||||||
<p>{% if markeddown %}{% autoescape off %}{{ markeddown }}{% endautoescape %}{% else %}{{ description|linebreaksbr }}{% endif %}</p>
|
<p>{{ description }}</p>
|
||||||
<div class='module'>
|
<div class='module'>
|
||||||
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
<pre><b>{{ response.status }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
{% for key, val in response.headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||||
|
@ -59,8 +76,8 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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 METHOD_PARAM and response.status != 403 %}
|
||||||
|
|
||||||
{% if 'POST' in view.allowed_methods %}
|
{% if 'POST' in view.allowed_methods %}
|
||||||
|
@ -83,7 +100,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if 'PUT' in view.allowed_methods %}
|
{% if 'PUT' in view.allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
<form action="{{ request.get_full_path }}" method="post" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
|
@ -96,23 +113,23 @@
|
||||||
{{ field.label_tag }}
|
{{ field.label_tag }}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
<span class='help'>{{ field.help_text }}</span>
|
<span class='help'>{{ field.help_text }}</span>
|
||||||
{{ field.errors }}
|
{{ field.errors }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% 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" />
|
<input type="submit" value="PUT" class="default" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if 'DELETE' in view.allowed_methods %}
|
{% if 'DELETE' in view.allowed_methods %}
|
||||||
<form action="{{ request.get_full_path }}" method="post">
|
<form action="{{ request.get_full_path }}" method="post">
|
||||||
<fieldset class='module aligned'>
|
<fieldset class='module aligned'>
|
||||||
<h2>DELETE {{ name }}</h2>
|
<h2>DELETE {{ name }}</h2>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="{{ METHOD_PARAM }}" value="DELETE" />
|
<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" />
|
<input type="submit" value="DELETE" class="default" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -121,7 +138,12 @@
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- END content-main -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- END Content -->
|
||||||
|
|
||||||
|
{% block footer %}<div id="footer"></div>{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
44
djangorestframework/templates/djangorestframework/login.html
Normal file
44
djangorestframework/templates/djangorestframework/login.html
Normal 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> </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>
|
|
@ -1,17 +1,10 @@
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
from urlparse import urlparse, urlunparse
|
from urlobject import URLObject
|
||||||
from urllib import quote
|
|
||||||
register = Library()
|
register = Library()
|
||||||
|
|
||||||
|
|
||||||
def add_query_param(url, param):
|
def add_query_param(url, param):
|
||||||
(key, sep, val) = param.partition('=')
|
return unicode(URLObject(url).add_query_param(*param.split('=')))
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
register.filter('add_query_param', add_query_param)
|
register.filter('add_query_param', add_query_param)
|
|
@ -1,6 +1,6 @@
|
||||||
"""Adds the custom filter 'urlize_quoted_links'
|
"""Adds the custom filter 'urlize_quoted_links'
|
||||||
|
|
||||||
This is identical to the built-in filter 'urlize' with the exception that
|
This is identical to the built-in filter 'urlize' with the exception that
|
||||||
single and double quotes are permitted as leading or trailing punctuation.
|
single and double quotes are permitted as leading or trailing punctuation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
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_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'
|
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):
|
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):
|
def setUp(self):
|
||||||
|
|
||||||
|
@ -39,6 +50,16 @@ class UserAgentMungingTest(TestCase):
|
||||||
resp = self.view(req)
|
resp = self.view(req)
|
||||||
self.assertEqual(resp['Content-Type'], 'text/html')
|
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):
|
def test_dont_rewrite_msie_accept_header(self):
|
||||||
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
"""Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||||
that we get a JSON response if we set a */* accept header."""
|
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)
|
req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent)
|
||||||
resp = view(req)
|
resp = view(req)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
def test_dont_munge_nice_browsers_accept_header(self):
|
def test_dont_munge_nice_browsers_accept_header(self):
|
||||||
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
|
"""Send Non-MSIE user agent strings and ensure that we get a JSON response,
|
||||||
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
|
if we set a */* Accept header. (Other browsers will correctly set the Accept header)"""
|
||||||
|
@ -63,6 +84,3 @@ class UserAgentMungingTest(TestCase):
|
||||||
resp = self.view(req)
|
resp = self.view(req)
|
||||||
self.assertEqual(resp['Content-Type'], 'application/json')
|
self.assertEqual(resp['Content-Type'], 'application/json')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
from django.conf.urls.defaults import patterns
|
from django.conf.urls.defaults import patterns
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework import permissions
|
from djangorestframework import permissions
|
||||||
|
|
||||||
|
@ -13,9 +11,13 @@ import base64
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
permissions = ( permissions.IsAuthenticated, )
|
permissions = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
def post(self, request):
|
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('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockView.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
|
@ -31,7 +33,7 @@ class BasicAuthTests(TestCase):
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
def test_post_form_passing_basic_auth(self):
|
def test_post_form_passing_basic_auth(self):
|
||||||
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
|
"""Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
|
||||||
|
@ -66,25 +68,38 @@ class SessionAuthTests(TestCase):
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.csrf_client.logout()
|
self.csrf_client.logout()
|
||||||
|
|
||||||
def test_post_form_session_auth_failing_csrf(self):
|
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)
|
self.csrf_client.login(username=self.username, password=self.password)
|
||||||
response = self.csrf_client.post('/', {'example': 'example'})
|
response = self.csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_post_form_session_auth_passing(self):
|
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)
|
self.non_csrf_client.login(username=self.username, password=self.password)
|
||||||
response = self.non_csrf_client.post('/', {'example': 'example'})
|
response = self.non_csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 200)
|
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):
|
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'})
|
response = self.csrf_client.post('/', {'example': 'example'})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
|
@ -64,4 +64,4 @@ class BreadcrumbTests(TestCase):
|
||||||
|
|
||||||
def test_broken_url_breadcrumbs_handled_gracefully(self):
|
def test_broken_url_breadcrumbs_handled_gracefully(self):
|
||||||
url = '/foobar'
|
url = '/foobar'
|
||||||
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
|
self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.contrib.auth.models import User
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.authentication import UserLoggedInAuthentication
|
from djangorestframework.authentication import UserLoggedInAuthentication
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory, unittest
|
||||||
from djangorestframework.mixins import RequestMixin
|
from djangorestframework.mixins import RequestMixin
|
||||||
from djangorestframework.parsers import FormParser, MultiPartParser, \
|
from djangorestframework.parsers import FormParser, MultiPartParser, \
|
||||||
PlainTextParser, JSONParser
|
PlainTextParser, JSONParser
|
||||||
|
@ -17,8 +17,8 @@ class MockView(View):
|
||||||
authentication = (UserLoggedInAuthentication,)
|
authentication = (UserLoggedInAuthentication,)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
if request.POST.get('example') is not None:
|
if request.POST.get('example') is not None:
|
||||||
return Response(status.OK)
|
return Response(status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response(status.INTERNAL_SERVER_ERROR)
|
return Response(status.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
@ -103,104 +103,105 @@ class TestContentParsing(TestCase):
|
||||||
view.request = self.req.post('/', form_data)
|
view.request = self.req.post('/', form_data)
|
||||||
view.parsers = (PlainTextParser,)
|
view.parsers = (PlainTextParser,)
|
||||||
self.assertEqual(view.DATA, content)
|
self.assertEqual(view.DATA, content)
|
||||||
|
|
||||||
def test_accessing_post_after_data_form(self):
|
def test_accessing_post_after_data_form(self):
|
||||||
"""Ensures request.POST can be accessed after request.DATA in form request"""
|
"""Ensures request.POST can be accessed after request.DATA in form request"""
|
||||||
form_data = {'qwerty': 'uiop'}
|
form_data = {'qwerty': 'uiop'}
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
view.parsers = (FormParser, MultiPartParser)
|
||||||
view.request = self.req.post('/', data=form_data)
|
view.request = self.req.post('/', data=form_data)
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
self.assertEqual(view.DATA.items(), form_data.items())
|
||||||
self.assertEqual(view.request.POST.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):
|
def test_accessing_post_after_data_for_json(self):
|
||||||
"""Ensures request.POST can be accessed after request.DATA in json request"""
|
"""Ensures request.POST can be accessed after request.DATA in json request"""
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
content = json.dumps(data)
|
content = json.dumps(data)
|
||||||
content_type = 'application/json'
|
content_type = 'application/json'
|
||||||
|
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (JSONParser,)
|
view.parsers = (JSONParser,)
|
||||||
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
view.request = self.req.post('/', content, content_type=content_type)
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
self.assertEqual(view.DATA.items(), data.items())
|
||||||
self.assertEqual(view.request.POST.items(), [])
|
self.assertEqual(view.request.POST.items(), [])
|
||||||
|
|
||||||
def test_accessing_post_after_data_for_overloaded_json(self):
|
def test_accessing_post_after_data_for_overloaded_json(self):
|
||||||
"""Ensures request.POST can be accessed after request.DATA in overloaded json request"""
|
"""Ensures request.POST can be accessed after request.DATA in overloaded json request"""
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
content = json.dumps(data)
|
content = json.dumps(data)
|
||||||
content_type = 'application/json'
|
content_type = 'application/json'
|
||||||
|
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (JSONParser,)
|
view.parsers = (JSONParser,)
|
||||||
|
|
||||||
form_data = {view._CONTENT_PARAM: content,
|
form_data = {view._CONTENT_PARAM: content,
|
||||||
view._CONTENTTYPE_PARAM: content_type}
|
view._CONTENTTYPE_PARAM: content_type}
|
||||||
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
view.request = self.req.post('/', data=form_data)
|
||||||
|
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
self.assertEqual(view.DATA.items(), data.items())
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||||
|
|
||||||
def test_accessing_data_after_post_form(self):
|
def test_accessing_data_after_post_form(self):
|
||||||
"""Ensures request.DATA can be accessed after request.POST in form request"""
|
"""Ensures request.DATA can be accessed after request.POST in form request"""
|
||||||
form_data = {'qwerty': 'uiop'}
|
form_data = {'qwerty': 'uiop'}
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (FormParser, MultiPartParser)
|
view.parsers = (FormParser, MultiPartParser)
|
||||||
view.request = self.req.post('/', data=form_data)
|
view.request = self.req.post('/', data=form_data)
|
||||||
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||||
self.assertEqual(view.DATA.items(), form_data.items())
|
self.assertEqual(view.DATA.items(), form_data.items())
|
||||||
|
|
||||||
def test_accessing_data_after_post_for_json(self):
|
def test_accessing_data_after_post_for_json(self):
|
||||||
"""Ensures request.DATA can be accessed after request.POST in json request"""
|
"""Ensures request.DATA can be accessed after request.POST in json request"""
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
content = json.dumps(data)
|
content = json.dumps(data)
|
||||||
content_type = 'application/json'
|
content_type = 'application/json'
|
||||||
|
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (JSONParser,)
|
view.parsers = (JSONParser,)
|
||||||
|
|
||||||
view.request = self.req.post('/', content, content_type=content_type)
|
view.request = self.req.post('/', content, content_type=content_type)
|
||||||
|
|
||||||
post_items = view.request.POST.items()
|
post_items = view.request.POST.items()
|
||||||
|
|
||||||
self.assertEqual(len(post_items), 1)
|
self.assertEqual(len(post_items), 1)
|
||||||
self.assertEqual(len(post_items[0]), 2)
|
self.assertEqual(len(post_items[0]), 2)
|
||||||
self.assertEqual(post_items[0][0], content)
|
self.assertEqual(post_items[0][0], content)
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
self.assertEqual(view.DATA.items(), data.items())
|
||||||
|
|
||||||
def test_accessing_data_after_post_for_overloaded_json(self):
|
def test_accessing_data_after_post_for_overloaded_json(self):
|
||||||
"""Ensures request.DATA can be accessed after request.POST in overloaded json request"""
|
"""Ensures request.DATA can be accessed after request.POST in overloaded json request"""
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
data = {'qwerty': 'uiop'}
|
data = {'qwerty': 'uiop'}
|
||||||
content = json.dumps(data)
|
content = json.dumps(data)
|
||||||
content_type = 'application/json'
|
content_type = 'application/json'
|
||||||
|
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.parsers = (JSONParser,)
|
view.parsers = (JSONParser,)
|
||||||
|
|
||||||
form_data = {view._CONTENT_PARAM: content,
|
form_data = {view._CONTENT_PARAM: content,
|
||||||
view._CONTENTTYPE_PARAM: content_type}
|
view._CONTENTTYPE_PARAM: content_type}
|
||||||
|
|
||||||
view.request = self.req.post('/', data=form_data)
|
view.request = self.req.post('/', data=form_data)
|
||||||
|
|
||||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||||
self.assertEqual(view.DATA.items(), data.items())
|
self.assertEqual(view.DATA.items(), data.items())
|
||||||
|
|
||||||
class TestContentParsingWithAuthentication(TestCase):
|
class TestContentParsingWithAuthentication(TestCase):
|
||||||
urls = 'djangorestframework.tests.content'
|
urls = 'djangorestframework.tests.content'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
|
@ -208,25 +209,25 @@ class TestContentParsingWithAuthentication(TestCase):
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
|
|
||||||
def test_user_logged_in_authentication_has_post_when_not_logged_in(self):
|
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"""
|
"""Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in"""
|
||||||
content = {'example': 'example'}
|
content = {'example': 'example'}
|
||||||
|
|
||||||
response = self.client.post('/', content)
|
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)
|
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):
|
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
|
||||||
"""Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
||||||
self.client.login(username='john', password='password')
|
# self.client.login(username='john', password='password')
|
||||||
self.csrf_client.login(username='john', password='password')
|
# self.csrf_client.login(username='john', password='password')
|
||||||
content = {'example': 'example'}
|
# content = {'example': 'example'}
|
||||||
|
|
||||||
response = self.client.post('/', content)
|
# response = self.client.post('/', content)
|
||||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||||
|
|
||||||
response = self.csrf_client.post('/', content)
|
# response = self.csrf_client.post('/', content)
|
||||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.compat import apply_markdown
|
from djangorestframework.compat import apply_markdown
|
||||||
from djangorestframework.utils.description import get_name, get_description
|
|
||||||
|
|
||||||
# We check that docstrings get nicely un-indented.
|
# We check that docstrings get nicely un-indented.
|
||||||
DESCRIPTION = """an example docstring
|
DESCRIPTION = """an example docstring
|
||||||
|
@ -19,8 +18,11 @@ indented
|
||||||
|
|
||||||
# hash style header #"""
|
# hash style header #"""
|
||||||
|
|
||||||
# If markdown is installed we also test it's working (and that our wrapped forces '=' to h2 and '-' to h3)
|
# If markdown is installed we also test it's working
|
||||||
MARKED_DOWN = """<h2>an example docstring</h2>
|
# (and that our wrapped forces '=' to h2 and '-' to h3)
|
||||||
|
|
||||||
|
# We support markdown < 2.1 and markdown >= 2.1
|
||||||
|
MARKED_DOWN_lt_21 = """<h2>an example docstring</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>list</li>
|
<li>list</li>
|
||||||
<li>list</li>
|
<li>list</li>
|
||||||
|
@ -31,21 +33,32 @@ MARKED_DOWN = """<h2>an example docstring</h2>
|
||||||
<p>indented</p>
|
<p>indented</p>
|
||||||
<h2 id="hash_style_header">hash style header</h2>"""
|
<h2 id="hash_style_header">hash style header</h2>"""
|
||||||
|
|
||||||
|
MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2>
|
||||||
|
<ul>
|
||||||
|
<li>list</li>
|
||||||
|
<li>list</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="another-header">another header</h3>
|
||||||
|
<pre><code>code block
|
||||||
|
</code></pre>
|
||||||
|
<p>indented</p>
|
||||||
|
<h2 id="hash-style-header">hash style header</h2>"""
|
||||||
|
|
||||||
|
|
||||||
class TestViewNamesAndDescriptions(TestCase):
|
class TestViewNamesAndDescriptions(TestCase):
|
||||||
def test_resource_name_uses_classname_by_default(self):
|
def test_resource_name_uses_classname_by_default(self):
|
||||||
"""Ensure Resource names are based on the classname by default."""
|
"""Ensure Resource names are based on the classname by default."""
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
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):
|
||||||
#def test_resource_name_can_be_set_explicitly(self):
|
"""Ensure Resource names can be set using the 'get_name' method."""
|
||||||
# """Ensure Resource names can be set using the 'name' class attribute."""
|
example = 'Some Other Name'
|
||||||
# example = 'Some Other Name'
|
class MockView(View):
|
||||||
# class MockView(View):
|
def get_name(self):
|
||||||
# name = example
|
return example
|
||||||
# self.assertEquals(get_name(MockView()), example)
|
self.assertEquals(MockView().get_name(), example)
|
||||||
|
|
||||||
def test_resource_description_uses_docstring_by_default(self):
|
def test_resource_description_uses_docstring_by_default(self):
|
||||||
"""Ensure Resource names are based on the docstring by default."""
|
"""Ensure Resource names are based on the docstring by default."""
|
||||||
|
@ -55,41 +68,44 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
* list
|
* list
|
||||||
* list
|
* list
|
||||||
|
|
||||||
another header
|
another header
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
code block
|
code block
|
||||||
|
|
||||||
indented
|
indented
|
||||||
|
|
||||||
# hash style header #"""
|
|
||||||
|
|
||||||
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
|
||||||
|
|
||||||
# This has been turned off now
|
# hash style header #"""
|
||||||
#def test_resource_description_can_be_set_explicitly(self):
|
|
||||||
# """Ensure Resource descriptions can be set using the 'description' class attribute."""
|
self.assertEquals(MockView().get_description(), DESCRIPTION)
|
||||||
# example = 'Some other description'
|
|
||||||
# class MockView(View):
|
def test_resource_description_can_be_set_explicitly(self):
|
||||||
# """docstring"""
|
"""Ensure Resource descriptions can be set using the 'get_description' method."""
|
||||||
# description = example
|
example = 'Some other description'
|
||||||
# self.assertEquals(get_description(MockView()), example)
|
class MockView(View):
|
||||||
|
"""docstring"""
|
||||||
#def test_resource_description_does_not_require_docstring(self):
|
def get_description(self):
|
||||||
# """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
return example
|
||||||
# example = 'Some other description'
|
self.assertEquals(MockView().get_description(), example)
|
||||||
# class MockView(View):
|
|
||||||
# description = example
|
def test_resource_description_does_not_require_docstring(self):
|
||||||
# self.assertEquals(get_description(MockView()), example)
|
"""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):
|
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):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_description(MockView()), '')
|
self.assertEquals(MockView().get_description(), '')
|
||||||
|
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""Ensure markdown to HTML works as expected"""
|
"""Ensure markdown to HTML works as expected"""
|
||||||
if apply_markdown:
|
if apply_markdown:
|
||||||
self.assertEquals(apply_markdown(DESCRIPTION), MARKED_DOWN)
|
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
|
||||||
|
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
|
||||||
|
self.assertTrue(gte_21_match or lt_21_match)
|
||||||
|
|
|
@ -22,7 +22,7 @@ class UploadFilesTests(TestCase):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return {'FILE_NAME': self.CONTENT['file'].name,
|
return {'FILE_NAME': self.CONTENT['file'].name,
|
||||||
'FILE_CONTENT': self.CONTENT['file'].read()}
|
'FILE_CONTENT': self.CONTENT['file'].read()}
|
||||||
|
|
||||||
file = StringIO.StringIO('stuff')
|
file = StringIO.StringIO('stuff')
|
||||||
file.name = 'stuff.txt'
|
file.name = 'stuff.txt'
|
||||||
request = self.factory.post('/', {'file': file})
|
request = self.factory.post('/', {'file': file})
|
||||||
|
@ -30,7 +30,3 @@ class UploadFilesTests(TestCase):
|
||||||
response = view(request)
|
response = view(request)
|
||||||
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.mixins import RequestMixin
|
from djangorestframework.mixins import RequestMixin
|
||||||
|
|
||||||
|
|
||||||
class TestMethodOverloading(TestCase):
|
class TestMethodOverloading(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class TestMethodOverloading(TestCase):
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
view.request = self.req.post('/')
|
view.request = self.req.post('/')
|
||||||
self.assertEqual(view.method, 'POST')
|
self.assertEqual(view.method, 'POST')
|
||||||
|
|
||||||
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
def test_overloaded_POST_behaviour_determines_overloaded_method(self):
|
||||||
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
"""POST requests can be overloaded to another method by setting a reserved form field"""
|
||||||
view = RequestMixin()
|
view = RequestMixin()
|
||||||
|
|
|
@ -1,17 +1,54 @@
|
||||||
"""Tests for the status module"""
|
"""Tests for the mixin module"""
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils import simplejson as json
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from django.contrib.auth.models import Group, User
|
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.resources import ModelResource
|
||||||
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
from djangorestframework.tests.models import CustomUser
|
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"""
|
"""Tests on CreateModelMixin"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super(TestModelsTestCase, self).setUp()
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
|
@ -25,23 +62,26 @@ class TestModelCreation(TestCase):
|
||||||
mixin = CreateModelMixin()
|
mixin = CreateModelMixin()
|
||||||
mixin.resource = GroupResource
|
mixin.resource = GroupResource
|
||||||
mixin.CONTENT = form_data
|
mixin.CONTENT = form_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(request)
|
||||||
self.assertEquals(1, Group.objects.count())
|
self.assertEquals(1, Group.objects.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.name)
|
self.assertEquals('foo', response.cleaned_content.name)
|
||||||
|
|
||||||
|
|
||||||
def test_creation_with_m2m_relation(self):
|
def test_creation_with_m2m_relation(self):
|
||||||
class UserResource(ModelResource):
|
class UserResource(ModelResource):
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
def url(self, instance):
|
def url(self, instance):
|
||||||
return "/users/%i" % instance.id
|
return "/users/%i" % instance.id
|
||||||
|
|
||||||
group = Group(name='foo')
|
group = Group(name='foo')
|
||||||
group.save()
|
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)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group]
|
cleaned_data['groups'] = [group]
|
||||||
|
@ -53,18 +93,18 @@ class TestModelCreation(TestCase):
|
||||||
self.assertEquals(1, User.objects.count())
|
self.assertEquals(1, User.objects.count())
|
||||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
"""
|
||||||
Tests creation where the m2m relation uses a through table
|
Tests creation where the m2m relation uses a through table
|
||||||
"""
|
"""
|
||||||
class UserResource(ModelResource):
|
class UserResource(ModelResource):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
|
|
||||||
def url(self, instance):
|
def url(self, instance):
|
||||||
return "/customusers/%i" % instance.id
|
return "/customusers/%i" % instance.id
|
||||||
|
|
||||||
form_data = {'username': 'bar0', 'groups': []}
|
form_data = {'username': 'bar0', 'groups': []}
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = []
|
cleaned_data['groups'] = []
|
||||||
|
@ -74,12 +114,12 @@ class TestModelCreation(TestCase):
|
||||||
|
|
||||||
response = mixin.post(request)
|
response = mixin.post(request)
|
||||||
self.assertEquals(1, CustomUser.objects.count())
|
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 = Group(name='foo1')
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
form_data = {'username': 'bar1', 'groups': [group.id]}
|
form_data = {'username': 'bar1', 'groups': [group.id]}
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group]
|
cleaned_data['groups'] = [group]
|
||||||
|
@ -91,12 +131,11 @@ class TestModelCreation(TestCase):
|
||||||
self.assertEquals(2, CustomUser.objects.count())
|
self.assertEquals(2, CustomUser.objects.count())
|
||||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||||
|
|
||||||
|
|
||||||
group2 = Group(name='foo2')
|
group2 = Group(name='foo2')
|
||||||
group2.save()
|
group2.save()
|
||||||
|
|
||||||
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
||||||
request = self.req.post('/groups', data=form_data)
|
request = self.req.post('/groups', data=form_data)
|
||||||
cleaned_data = dict(form_data)
|
cleaned_data = dict(form_data)
|
||||||
cleaned_data['groups'] = [group, group2]
|
cleaned_data['groups'] = [group, group2]
|
||||||
|
@ -109,5 +148,144 @@ class TestModelCreation(TestCase):
|
||||||
self.assertEquals(2, response.cleaned_content.groups.count())
|
self.assertEquals(2, response.cleaned_content.groups.count())
|
||||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||||
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].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'])
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
class CustomUser(models.Model):
|
class CustomUser(models.Model):
|
||||||
"""
|
"""
|
||||||
A custom user model, which uses a 'through' table for the foreign key
|
A custom user model, which uses a 'through' table for the foreign key
|
||||||
"""
|
"""
|
||||||
username = models.CharField(max_length=255, unique=True)
|
username = models.CharField(max_length=255, unique=True)
|
||||||
groups = models.ManyToManyField(
|
groups = models.ManyToManyField(
|
||||||
to=Group, blank=True, null=True, through='UserGroupMap'
|
to=Group, blank=True, null=True, through='UserGroupMap'
|
||||||
)
|
)
|
||||||
|
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('custom_user', (), {
|
return ('custom_user', (), {
|
||||||
'pk': self.id
|
'pk': self.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class UserGroupMap(models.Model):
|
class UserGroupMap(models.Model):
|
||||||
user = models.ForeignKey(to=CustomUser)
|
user = models.ForeignKey(to=CustomUser)
|
||||||
group = models.ForeignKey(to=Group)
|
group = models.ForeignKey(to=Group)
|
||||||
|
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('user_group_map', (), {
|
return ('user_group_map', (), {
|
||||||
'pk': self.id
|
'pk': self.id
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.test import TestCase
|
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||||
from djangorestframework.tests.models import CustomUser
|
from djangorestframework.tests.models import CustomUser
|
||||||
|
from djangorestframework.tests.testcases import TestModelsTestCase
|
||||||
|
|
||||||
|
|
||||||
class GroupResource(ModelResource):
|
class GroupResource(ModelResource):
|
||||||
model = Group
|
model = Group
|
||||||
|
|
||||||
|
|
||||||
class UserForm(ModelForm):
|
class UserForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
exclude = ('last_login', 'date_joined')
|
exclude = ('last_login', 'date_joined')
|
||||||
|
|
||||||
|
|
||||||
class UserResource(ModelResource):
|
class UserResource(ModelResource):
|
||||||
model = User
|
model = User
|
||||||
form = UserForm
|
form = UserForm
|
||||||
|
|
||||||
|
|
||||||
class CustomUserResource(ModelResource):
|
class CustomUserResource(ModelResource):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
||||||
|
@ -31,9 +35,9 @@ urlpatterns = patterns('',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModelViewTests(TestCase):
|
class ModelViewTests(TestModelsTestCase):
|
||||||
"""Test the model views djangorestframework provides"""
|
"""Test the model views djangorestframework provides"""
|
||||||
urls = 'djangorestframework.tests.modelviews'
|
urls = 'djangorestframework.tests.modelviews'
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
"""Ensure that a model object can be created"""
|
"""Ensure that a model object can be created"""
|
||||||
|
@ -52,18 +56,18 @@ class ModelViewTests(TestCase):
|
||||||
self.assertEqual(0, User.objects.count())
|
self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, User.objects.count())
|
self.assertEqual(1, User.objects.count())
|
||||||
|
|
||||||
user = User.objects.all()[0]
|
user = User.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
self.assertEqual('bar', user.username)
|
||||||
self.assertEqual('baz', user.password)
|
self.assertEqual('baz', user.password)
|
||||||
self.assertEqual(1, user.groups.count())
|
self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
self.assertEqual('foo', group.name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
"""
|
||||||
Ensure that a model object with a m2m relation can be created where that
|
Ensure that a model object with a m2m relation can be created where that
|
||||||
|
@ -74,13 +78,13 @@ class ModelViewTests(TestCase):
|
||||||
self.assertEqual(0, User.objects.count())
|
self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, CustomUser.objects.count())
|
self.assertEqual(1, CustomUser.objects.count())
|
||||||
|
|
||||||
user = CustomUser.objects.all()[0]
|
user = CustomUser.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
self.assertEqual('bar', user.username)
|
||||||
self.assertEqual(1, user.groups.count())
|
self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
self.assertEqual('foo', group.name)
|
||||||
|
|
|
@ -23,14 +23,14 @@ else:
|
||||||
class ClientView(View):
|
class ClientView(View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return {'resource': 'Protected!'}
|
return {'resource': 'Protected!'}
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', oauth_required(ClientView.as_view())),
|
url(r'^$', oauth_required(ClientView.as_view())),
|
||||||
url(r'^oauth/', include('oauth_provider.urls')),
|
url(r'^oauth/', include('oauth_provider.urls')),
|
||||||
url(r'^accounts/login/$', 'djangorestframework.utils.staticviews.api_login'),
|
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OAuthTests(TestCase):
|
class OAuthTests(TestCase):
|
||||||
"""
|
"""
|
||||||
OAuth authentication:
|
OAuth authentication:
|
||||||
|
@ -42,23 +42,23 @@ else:
|
||||||
* the third-party website is able to retrieve data from the API
|
* the third-party website is able to retrieve data from the API
|
||||||
"""
|
"""
|
||||||
urls = 'djangorestframework.tests.oauthentication'
|
urls = 'djangorestframework.tests.oauthentication'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
self.username = 'john'
|
self.username = 'john'
|
||||||
self.email = 'lennon@thebeatles.com'
|
self.email = 'lennon@thebeatles.com'
|
||||||
self.password = 'password'
|
self.password = 'password'
|
||||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||||
|
|
||||||
# OAuth requirements
|
# OAuth requirements
|
||||||
self.resource = Resource(name='data', url='/')
|
self.resource = Resource(name='data', url='/')
|
||||||
self.resource.save()
|
self.resource.save()
|
||||||
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
|
self.CONSUMER_KEY = 'dpf43f3p2l4k3l03'
|
||||||
self.CONSUMER_SECRET = 'kd94hf93k423kf44'
|
self.CONSUMER_SECRET = 'kd94hf93k423kf44'
|
||||||
self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
|
self.consumer = Consumer(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
|
||||||
name='api.example.com', user=self.user)
|
name='api.example.com', user=self.user)
|
||||||
self.consumer.save()
|
self.consumer.save()
|
||||||
|
|
||||||
def test_oauth_invalid_and_anonymous_access(self):
|
def test_oauth_invalid_and_anonymous_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that the resource is protected and the OAuth authorization view
|
Verify that the resource is protected and the OAuth authorization view
|
||||||
|
@ -69,16 +69,16 @@ else:
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
response = self.client.get('/oauth/authorize/', follow=True)
|
response = self.client.get('/oauth/authorize/', follow=True)
|
||||||
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
|
self.assertRedirects(response, '/accounts/login/?next=/oauth/authorize/')
|
||||||
|
|
||||||
def test_oauth_authorize_access(self):
|
def test_oauth_authorize_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that once logged in, the user can access the authorization page
|
Verify that once logged in, the user can access the authorization page
|
||||||
but can't display the page because the request token is not specified.
|
but can't display the page because the request token is not specified.
|
||||||
"""
|
"""
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
response = self.client.get('/oauth/authorize/', follow=True)
|
response = self.client.get('/oauth/authorize/', follow=True)
|
||||||
self.assertEqual(response.content, 'No request token specified.')
|
self.assertEqual(response.content, 'No request token specified.')
|
||||||
|
|
||||||
def _create_request_token_parameters(self):
|
def _create_request_token_parameters(self):
|
||||||
"""
|
"""
|
||||||
A shortcut to create request's token parameters.
|
A shortcut to create request's token parameters.
|
||||||
|
@ -93,28 +93,28 @@ else:
|
||||||
'oauth_callback': 'http://api.example.com/request_token_ready',
|
'oauth_callback': 'http://api.example.com/request_token_ready',
|
||||||
'scope': 'data',
|
'scope': 'data',
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_oauth_request_token_retrieval(self):
|
def test_oauth_request_token_retrieval(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.failIf(token.key not in response.content)
|
self.failIf(token.key not in response.content)
|
||||||
self.failIf(token.secret not in response.content)
|
self.failIf(token.secret not in response.content)
|
||||||
|
|
||||||
def test_oauth_user_request_authorization(self):
|
def test_oauth_user_request_authorization(self):
|
||||||
"""
|
"""
|
||||||
Verify that the user can access the authorization page once logged in
|
Verify that the user can access the authorization page once logged in
|
||||||
and the request token has been retrieved.
|
and the request token has been retrieved.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
parameters = {'oauth_token': token.key,}
|
parameters = {'oauth_token': token.key,}
|
||||||
|
@ -129,7 +129,7 @@ else:
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
self.failIf(token.key not in response['Location'])
|
self.failIf(token.key not in response['Location'])
|
||||||
self.assertEqual(token.is_approved, 1)
|
self.assertEqual(token.is_approved, 1)
|
||||||
|
|
||||||
def _create_access_token_parameters(self, token):
|
def _create_access_token_parameters(self, token):
|
||||||
"""
|
"""
|
||||||
A shortcut to create access' token parameters.
|
A shortcut to create access' token parameters.
|
||||||
|
@ -145,13 +145,13 @@ else:
|
||||||
'oauth_verifier': token.verifier,
|
'oauth_verifier': token.verifier,
|
||||||
'scope': 'data',
|
'scope': 'data',
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_oauth_access_token_retrieval(self):
|
def test_oauth_access_token_retrieval(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
@ -160,7 +160,7 @@ else:
|
||||||
parameters['authorize_access'] = 1 # fake authorization by the user
|
parameters['authorize_access'] = 1 # fake authorization by the user
|
||||||
response = self.client.post("/oauth/authorize/", parameters)
|
response = self.client.post("/oauth/authorize/", parameters)
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -169,7 +169,7 @@ else:
|
||||||
self.failIf(access_token.key not in response.content)
|
self.failIf(access_token.key not in response.content)
|
||||||
self.failIf(access_token.secret not in response.content)
|
self.failIf(access_token.secret not in response.content)
|
||||||
self.assertEqual(access_token.user.username, 'john')
|
self.assertEqual(access_token.user.username, 'john')
|
||||||
|
|
||||||
def _create_access_parameters(self, access_token):
|
def _create_access_parameters(self, access_token):
|
||||||
"""
|
"""
|
||||||
A shortcut to create access' parameters.
|
A shortcut to create access' parameters.
|
||||||
|
@ -188,13 +188,13 @@ else:
|
||||||
signature = signature_method.sign(oauth_request, self.consumer, access_token)
|
signature = signature_method.sign(oauth_request, self.consumer, access_token)
|
||||||
parameters['oauth_signature'] = signature
|
parameters['oauth_signature'] = signature
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
def test_oauth_protected_resource_access(self):
|
def test_oauth_protected_resource_access(self):
|
||||||
"""
|
"""
|
||||||
Verify that the request token can be retrieved by the server.
|
Verify that the request token can be retrieved by the server.
|
||||||
"""
|
"""
|
||||||
# Setup
|
# Setup
|
||||||
response = self.client.get("/oauth/request_token/",
|
response = self.client.get("/oauth/request_token/",
|
||||||
self._create_request_token_parameters())
|
self._create_request_token_parameters())
|
||||||
token = list(Token.objects.all())[-1]
|
token = list(Token.objects.all())[-1]
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
@ -205,7 +205,7 @@ else:
|
||||||
token = Token.objects.get(key=token.key)
|
token = Token.objects.get(key=token.key)
|
||||||
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
response = self.client.get("/oauth/access_token/", self._create_access_token_parameters(token))
|
||||||
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
|
access_token = list(Token.objects.filter(token_type=Token.ACCESS))[-1]
|
||||||
|
|
||||||
# Starting the test here
|
# Starting the test here
|
||||||
response = self.client.get("/", self._create_access_token_parameters(access_token))
|
response = self.client.get("/", self._create_access_token_parameters(access_token))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import djangorestframework
|
import djangorestframework
|
||||||
|
|
||||||
class TestVersion(TestCase):
|
class TestVersion(TestCase):
|
||||||
"""Simple sanity test to check the VERSION exists"""
|
"""Simple sanity test to check the VERSION exists"""
|
||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
|
|
|
@ -8,76 +8,76 @@
|
||||||
# >>> req = RequestFactory().get('/')
|
# >>> req = RequestFactory().get('/')
|
||||||
# >>> some_view = View()
|
# >>> some_view = View()
|
||||||
# >>> some_view.request = req # Make as if this request had been dispatched
|
# >>> some_view.request = req # Make as if this request had been dispatched
|
||||||
#
|
#
|
||||||
# FormParser
|
# FormParser
|
||||||
# ============
|
# ============
|
||||||
#
|
#
|
||||||
# Data flatening
|
# Data flatening
|
||||||
# ----------------
|
# ----------------
|
||||||
#
|
#
|
||||||
# Here is some example data, which would eventually be sent along with a post request :
|
# Here is some example data, which would eventually be sent along with a post request :
|
||||||
#
|
#
|
||||||
# >>> inpt = urlencode([
|
# >>> inpt = urlencode([
|
||||||
# ... ('key1', 'bla1'),
|
# ... ('key1', 'bla1'),
|
||||||
# ... ('key2', 'blo1'), ('key2', 'blo2'),
|
# ... ('key2', 'blo1'), ('key2', 'blo2'),
|
||||||
# ... ])
|
# ... ])
|
||||||
#
|
#
|
||||||
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
# Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||||
#
|
#
|
||||||
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = FormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
# >>> data == {'key1': 'bla1', 'key2': 'blo1'}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
# However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||||
#
|
#
|
||||||
# >>> class MyFormParser(FormParser):
|
# >>> class MyFormParser(FormParser):
|
||||||
# ...
|
# ...
|
||||||
# ... def is_a_list(self, key, val_list):
|
# ... def is_a_list(self, key, val_list):
|
||||||
# ... return len(val_list) > 1
|
# ... return len(val_list) > 1
|
||||||
#
|
#
|
||||||
# This new parser only flattens the lists of parameters that contain a single value.
|
# This new parser only flattens the lists of parameters that contain a single value.
|
||||||
#
|
#
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
# >>> data == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
# .. note:: The same functionality is available for :class:`parsers.MultiPartParser`.
|
||||||
#
|
#
|
||||||
# Submitting an empty list
|
# Submitting an empty list
|
||||||
# --------------------------
|
# --------------------------
|
||||||
#
|
#
|
||||||
# When submitting an empty select multiple, like this one ::
|
# When submitting an empty select multiple, like this one ::
|
||||||
#
|
#
|
||||||
# <select multiple="multiple" name="key2"></select>
|
# <select multiple="multiple" name="key2"></select>
|
||||||
#
|
#
|
||||||
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
# The browsers usually strip the parameter completely. A hack to avoid this, and therefore being able to submit an empty select multiple, is to submit a value that tells the server that the list is empty ::
|
||||||
#
|
#
|
||||||
# <select multiple="multiple" name="key2"><option value="_empty"></select>
|
# <select multiple="multiple" name="key2"><option value="_empty"></select>
|
||||||
#
|
#
|
||||||
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
# :class:`parsers.FormParser` provides the server-side implementation for this hack. Considering the following posted data :
|
||||||
#
|
#
|
||||||
# >>> inpt = urlencode([
|
# >>> inpt = urlencode([
|
||||||
# ... ('key1', 'blo1'), ('key1', '_empty'),
|
# ... ('key1', 'blo1'), ('key1', '_empty'),
|
||||||
# ... ('key2', '_empty'),
|
# ... ('key2', '_empty'),
|
||||||
# ... ])
|
# ... ])
|
||||||
#
|
#
|
||||||
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
# :class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||||
#
|
#
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'blo1'}
|
# >>> data == {'key1': 'blo1'}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
# Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||||
#
|
#
|
||||||
# >>> class MyFormParser(FormParser):
|
# >>> class MyFormParser(FormParser):
|
||||||
# ...
|
# ...
|
||||||
# ... def is_a_list(self, key, val_list):
|
# ... def is_a_list(self, key, val_list):
|
||||||
# ... return key == 'key2'
|
# ... return key == 'key2'
|
||||||
# ...
|
# ...
|
||||||
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
# >>> (data, files) = MyFormParser(some_view).parse(StringIO(inpt))
|
||||||
# >>> data == {'key1': 'blo1', 'key2': []}
|
# >>> data == {'key1': 'blo1', 'key2': []}
|
||||||
# True
|
# True
|
||||||
#
|
#
|
||||||
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
# Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||||
# """
|
# """
|
||||||
# import httplib, mimetypes
|
# import httplib, mimetypes
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
# from djangorestframework.parsers import MultiPartParser
|
# from djangorestframework.parsers import MultiPartParser
|
||||||
# from djangorestframework.views import View
|
# from djangorestframework.views import View
|
||||||
# from StringIO import StringIO
|
# from StringIO import StringIO
|
||||||
#
|
#
|
||||||
# def encode_multipart_formdata(fields, files):
|
# def encode_multipart_formdata(fields, files):
|
||||||
# """For testing multipart parser.
|
# """For testing multipart parser.
|
||||||
# fields is a sequence of (name, value) elements for regular form fields.
|
# fields is a sequence of (name, value) elements for regular form fields.
|
||||||
|
@ -112,10 +112,10 @@
|
||||||
# body = CRLF.join(L)
|
# body = CRLF.join(L)
|
||||||
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
# content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||||
# return content_type, body
|
# return content_type, body
|
||||||
#
|
#
|
||||||
# def get_content_type(filename):
|
# def get_content_type(filename):
|
||||||
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
# return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
#
|
#
|
||||||
#class TestMultiPartParser(TestCase):
|
#class TestMultiPartParser(TestCase):
|
||||||
# def setUp(self):
|
# def setUp(self):
|
||||||
# self.req = RequestFactory()
|
# self.req = RequestFactory()
|
||||||
|
@ -136,6 +136,8 @@ from cgi import parse_qs
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.parsers import FormParser
|
from djangorestframework.parsers import FormParser
|
||||||
|
from djangorestframework.parsers import XMLParser
|
||||||
|
import datetime
|
||||||
|
|
||||||
class Form(forms.Form):
|
class Form(forms.Form):
|
||||||
field1 = forms.CharField(max_length=3)
|
field1 = forms.CharField(max_length=3)
|
||||||
|
@ -143,13 +145,66 @@ class Form(forms.Form):
|
||||||
|
|
||||||
class TestFormParser(TestCase):
|
class TestFormParser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.string = "field1=abc&field2=defghijk"
|
self.string = "field1=abc&field2=defghijk"
|
||||||
|
|
||||||
def test_parse(self):
|
def test_parse(self):
|
||||||
""" Make sure the `QueryDict` works OK """
|
""" Make sure the `QueryDict` works OK """
|
||||||
parser = FormParser(None)
|
parser = FormParser(None)
|
||||||
|
|
||||||
stream = StringIO(self.string)
|
stream = StringIO(self.string)
|
||||||
(data, files) = parser.parse(stream)
|
(data, files) = parser.parse(stream)
|
||||||
|
|
||||||
self.assertEqual(Form(data).is_valid(), True)
|
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)
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
import re
|
||||||
from django import http
|
|
||||||
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
|
from djangorestframework.views import View
|
||||||
from djangorestframework.compat import View as DjangoView
|
from djangorestframework.compat import View as DjangoView
|
||||||
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer
|
from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||||
from djangorestframework.parsers import JSONParser, YAMLParser
|
XMLRenderer, JSONPRenderer, DocumentingHTMLRenderer
|
||||||
|
from djangorestframework.parsers import JSONParser, YAMLParser, XMLParser
|
||||||
from djangorestframework.mixins import ResponseMixin
|
from djangorestframework.mixins import ResponseMixin
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
from djangorestframework.utils.mediatypes import add_media_type_param
|
|
||||||
|
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
DUMMYSTATUS = status.HTTP_200_OK
|
DUMMYSTATUS = status.HTTP_200_OK
|
||||||
DUMMYCONTENT = 'dummycontent'
|
DUMMYCONTENT = 'dummycontent'
|
||||||
|
@ -18,31 +22,58 @@ DUMMYCONTENT = 'dummycontent'
|
||||||
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
RENDERER_A_SERIALIZER = lambda x: 'Renderer A: %s' % x
|
||||||
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
|
RENDERER_B_SERIALIZER = lambda x: 'Renderer B: %s' % x
|
||||||
|
|
||||||
|
|
||||||
class RendererA(BaseRenderer):
|
class RendererA(BaseRenderer):
|
||||||
media_type = 'mock/renderera'
|
media_type = 'mock/renderera'
|
||||||
format="formata"
|
format = "formata"
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
return RENDERER_A_SERIALIZER(obj)
|
return RENDERER_A_SERIALIZER(obj)
|
||||||
|
|
||||||
|
|
||||||
class RendererB(BaseRenderer):
|
class RendererB(BaseRenderer):
|
||||||
media_type = 'mock/rendererb'
|
media_type = 'mock/rendererb'
|
||||||
format="formatb"
|
format = "formatb"
|
||||||
|
|
||||||
def render(self, obj=None, media_type=None):
|
def render(self, obj=None, media_type=None):
|
||||||
return RENDERER_B_SERIALIZER(obj)
|
return RENDERER_B_SERIALIZER(obj)
|
||||||
|
|
||||||
|
|
||||||
class MockView(ResponseMixin, DjangoView):
|
class MockView(ResponseMixin, DjangoView):
|
||||||
renderers = (RendererA, RendererB)
|
renderers = (RendererA, RendererB)
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
response = Response(DUMMYSTATUS, DUMMYCONTENT)
|
response = Response(DUMMYSTATUS, DUMMYCONTENT)
|
||||||
return self.render(response)
|
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('',
|
urlpatterns = patterns('',
|
||||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
|
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
|
||||||
|
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
|
||||||
|
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
|
||||||
|
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-Type'], RendererB.media_type)
|
||||||
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
def test_specified_renderer_serializes_content_on_accept_query(self):
|
def test_specified_renderer_serializes_content_on_accept_query(self):
|
||||||
"""The '_accept' query string should behave in the same way as the Accept header."""
|
"""The '_accept' query string should behave in the same way as the Accept header."""
|
||||||
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
|
resp = self.client.get('/?_accept=%s' % RendererB.media_type)
|
||||||
|
@ -144,14 +175,15 @@ class RendererIntegrationTests(TestCase):
|
||||||
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
self.assertEquals(resp.status_code, DUMMYSTATUS)
|
||||||
|
|
||||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
_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):
|
class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -162,65 +194,219 @@ class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Test basic JSON rendering.
|
Test basic JSON rendering.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = JSONRenderer(None)
|
renderer = JSONRenderer(None)
|
||||||
content = renderer.render(obj, 'application/json')
|
content = renderer.render(obj, 'application/json')
|
||||||
|
# Fix failing test case which depends on version of JSON library.
|
||||||
self.assertEquals(content, _flat_repr)
|
self.assertEquals(content, _flat_repr)
|
||||||
|
|
||||||
def test_with_content_type_args(self):
|
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)
|
renderer = JSONRenderer(None)
|
||||||
content = renderer.render(obj, 'application/json; indent=2')
|
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):
|
def test_render_and_parse(self):
|
||||||
"""
|
"""
|
||||||
Test rendering and then parsing returns the original object.
|
Test rendering and then parsing returns the original object.
|
||||||
IE obj -> render -> parse -> obj.
|
IE obj -> render -> parse -> obj.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
|
|
||||||
renderer = JSONRenderer(None)
|
renderer = JSONRenderer(None)
|
||||||
parser = JSONParser(None)
|
parser = JSONParser(None)
|
||||||
|
|
||||||
content = renderer.render(obj, 'application/json')
|
content = renderer.render(obj, 'application/json')
|
||||||
(data, files) = parser.parse(StringIO(content))
|
(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:
|
if YAMLRenderer:
|
||||||
_yaml_repr = 'foo: [bar, baz]\n'
|
_yaml_repr = 'foo: [bar, baz]\n'
|
||||||
|
|
||||||
|
|
||||||
class YAMLRendererTests(TestCase):
|
class YAMLRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
"""
|
"""
|
||||||
Test basic YAML rendering.
|
Test basic YAML rendering.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
renderer = YAMLRenderer(None)
|
renderer = YAMLRenderer(None)
|
||||||
content = renderer.render(obj, 'application/yaml')
|
content = renderer.render(obj, 'application/yaml')
|
||||||
self.assertEquals(content, _yaml_repr)
|
self.assertEquals(content, _yaml_repr)
|
||||||
|
|
||||||
|
|
||||||
def test_render_and_parse(self):
|
def test_render_and_parse(self):
|
||||||
"""
|
"""
|
||||||
Test rendering and then parsing returns the original object.
|
Test rendering and then parsing returns the original object.
|
||||||
IE obj -> render -> parse -> obj.
|
IE obj -> render -> parse -> obj.
|
||||||
"""
|
"""
|
||||||
obj = {'foo':['bar','baz']}
|
obj = {'foo': ['bar', 'baz']}
|
||||||
|
|
||||||
renderer = YAMLRenderer(None)
|
renderer = YAMLRenderer(None)
|
||||||
parser = YAMLParser(None)
|
parser = YAMLParser(None)
|
||||||
|
|
||||||
content = renderer.render(obj, 'application/yaml')
|
content = renderer.render(obj, 'application/yaml')
|
||||||
(data, files) = parser.parse(StringIO(content))
|
(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')
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
#from djangorestframework.response import Response
|
#from djangorestframework.response import Response
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#class TestResponse(TestCase):
|
#class TestResponse(TestCase):
|
||||||
#
|
#
|
||||||
# # Interface tests
|
# # Interface tests
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,28 +1,33 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
|
from djangorestframework.renderers import JSONRenderer
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
class MyView(View):
|
||||||
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
|
"""
|
||||||
permissions = ()
|
Mock resource which simply returns a URL, so that we can ensure
|
||||||
|
that reversed URLs are fully qualified.
|
||||||
|
"""
|
||||||
|
renderers = (JSONRenderer, )
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return reverse('another')
|
return reverse('myview', request=request)
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', MockView.as_view()),
|
url(r'^myview$', MyView.as_view(), name='myview'),
|
||||||
url(r'^another$', MockView.as_view(), name='another'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReverseTests(TestCase):
|
class ReverseTests(TestCase):
|
||||||
"""Tests for """
|
"""
|
||||||
|
Tests for fully qualifed URLs when using `reverse`.
|
||||||
|
"""
|
||||||
urls = 'djangorestframework.tests.reverse'
|
urls = 'djangorestframework.tests.reverse'
|
||||||
|
|
||||||
def test_reversed_urls_are_fully_qualified(self):
|
def test_reversed_urls_are_fully_qualified(self):
|
||||||
response = self.client.get('/')
|
response = self.client.get('/myview')
|
||||||
self.assertEqual(json.loads(response.content), 'http://testserver/another')
|
self.assertEqual(json.loads(response.content), 'http://testserver/myview')
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""Tests for the resource module"""
|
"""Tests for the resource module"""
|
||||||
from django.test import TestCase
|
|
||||||
from djangorestframework.serializer import Serializer
|
|
||||||
|
|
||||||
from django.db import models
|
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 datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
class TestObjectToData(TestCase):
|
class TestObjectToData(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests for the Serializer class.
|
Tests for the Serializer class.
|
||||||
"""
|
"""
|
||||||
|
@ -34,9 +34,7 @@ class TestObjectToData(TestCase):
|
||||||
self.assertEquals(self.serialize(Foo().foo), 1)
|
self.assertEquals(self.serialize(Foo().foo), 1)
|
||||||
|
|
||||||
def test_datetime(self):
|
def test_datetime(self):
|
||||||
"""
|
"""datetime objects are left as-is."""
|
||||||
datetime objects are left as-is.
|
|
||||||
"""
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
self.assertEquals(self.serialize(now), 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({'keys': 'foo'}), {'keys': u'foo'})
|
||||||
self.assertEquals(self.serialize({'values': 'foo'}), {'values': 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):
|
class TestFieldNesting(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -56,8 +57,8 @@ class TestFieldNesting(TestCase):
|
||||||
self.serialize = self.serializer.serialize
|
self.serialize = self.serializer.serialize
|
||||||
|
|
||||||
class M1(models.Model):
|
class M1(models.Model):
|
||||||
field1 = models.CharField()
|
field1 = models.CharField(max_length=256)
|
||||||
field2 = models.CharField()
|
field2 = models.CharField(max_length=256)
|
||||||
|
|
||||||
class M2(models.Model):
|
class M2(models.Model):
|
||||||
field = models.OneToOneField(M1)
|
field = models.OneToOneField(M1)
|
||||||
|
@ -103,6 +104,27 @@ class TestFieldNesting(TestCase):
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
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):
|
def test_serializer_classname_nesting(self):
|
||||||
"""
|
"""
|
||||||
Test related model serialization
|
Test related model serialization
|
||||||
|
@ -121,3 +143,18 @@ class TestFieldNesting(TestCase):
|
||||||
|
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
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})
|
||||||
|
|
|
@ -3,14 +3,10 @@ from django.test import TestCase
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
|
|
||||||
|
|
||||||
class TestStatus(TestCase):
|
class TestStatus(TestCase):
|
||||||
"""Simple sanity test to check the status module"""
|
"""Simple sanity test to check the status module"""
|
||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
"""Ensure the status module is present and correct."""
|
"""Ensure the status module is present and correct."""
|
||||||
self.assertEquals(200, status.OK)
|
|
||||||
self.assertEquals(200, status.HTTP_200_OK)
|
self.assertEquals(200, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertEquals(404, status.NOT_FOUND)
|
|
||||||
self.assertEquals(404, status.HTTP_404_NOT_FOUND)
|
self.assertEquals(404, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
63
djangorestframework/tests/testcases.py
Normal file
63
djangorestframework/tests/testcases.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# http://djangosnippets.org/snippets/1011/
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.db.models import loading
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
NO_SETTING = ('!', None)
|
||||||
|
|
||||||
|
class TestSettingsManager(object):
|
||||||
|
"""
|
||||||
|
A class which can modify some Django settings temporarily for a
|
||||||
|
test and then revert them to their original values later.
|
||||||
|
|
||||||
|
Automatically handles resyncing the DB if INSTALLED_APPS is
|
||||||
|
modified.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._original_settings = {}
|
||||||
|
|
||||||
|
def set(self, **kwargs):
|
||||||
|
for k,v in kwargs.iteritems():
|
||||||
|
self._original_settings.setdefault(k, getattr(settings, k,
|
||||||
|
NO_SETTING))
|
||||||
|
setattr(settings, k, v)
|
||||||
|
if 'INSTALLED_APPS' in kwargs:
|
||||||
|
self.syncdb()
|
||||||
|
|
||||||
|
def syncdb(self):
|
||||||
|
loading.cache.loaded = False
|
||||||
|
call_command('syncdb', verbosity=0)
|
||||||
|
|
||||||
|
def revert(self):
|
||||||
|
for k,v in self._original_settings.iteritems():
|
||||||
|
if v == NO_SETTING:
|
||||||
|
delattr(settings, k)
|
||||||
|
else:
|
||||||
|
setattr(settings, k, v)
|
||||||
|
if 'INSTALLED_APPS' in self._original_settings:
|
||||||
|
self.syncdb()
|
||||||
|
self._original_settings = {}
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
A subclass of the Django TestCase with a settings_manager
|
||||||
|
attribute which is an instance of TestSettingsManager.
|
||||||
|
|
||||||
|
Comes with a tearDown() method that calls
|
||||||
|
self.settings_manager.revert().
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SettingsTestCase, self).__init__(*args, **kwargs)
|
||||||
|
self.settings_manager = TestSettingsManager()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.settings_manager.revert()
|
||||||
|
|
||||||
|
class TestModelsTestCase(SettingsTestCase):
|
||||||
|
def setUp(self, *args, **kwargs):
|
||||||
|
installed_apps = tuple(settings.INSTALLED_APPS) + ('djangorestframework.tests',)
|
||||||
|
self.settings_manager.set(INSTALLED_APPS=installed_apps)
|
|
@ -21,25 +21,25 @@ class MockView(View):
|
||||||
class MockView_PerViewThrottling(MockView):
|
class MockView_PerViewThrottling(MockView):
|
||||||
permissions = ( PerViewThrottling, )
|
permissions = ( PerViewThrottling, )
|
||||||
|
|
||||||
class MockView_PerResourceThrottling(MockView):
|
class MockView_PerResourceThrottling(MockView):
|
||||||
permissions = ( PerResourceThrottling, )
|
permissions = ( PerResourceThrottling, )
|
||||||
resource = FormResource
|
resource = FormResource
|
||||||
|
|
||||||
class MockView_MinuteThrottling(MockView):
|
class MockView_MinuteThrottling(MockView):
|
||||||
throttle = '3/min'
|
throttle = '3/min'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ThrottlingTests(TestCase):
|
class ThrottlingTests(TestCase):
|
||||||
urls = 'djangorestframework.tests.throttling'
|
urls = 'djangorestframework.tests.throttling'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Reset the cache so that no throttles will be active
|
Reset the cache so that no throttles will be active
|
||||||
"""
|
"""
|
||||||
cache.clear()
|
cache.clear()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_requests_are_throttled(self):
|
def test_requests_are_throttled(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited
|
Ensure request rate is limited
|
||||||
|
@ -48,7 +48,7 @@ class ThrottlingTests(TestCase):
|
||||||
for dummy in range(4):
|
for dummy in range(4):
|
||||||
response = MockView.as_view()(request)
|
response = MockView.as_view()(request)
|
||||||
self.assertEqual(503, response.status_code)
|
self.assertEqual(503, response.status_code)
|
||||||
|
|
||||||
def set_throttle_timer(self, view, value):
|
def set_throttle_timer(self, view, value):
|
||||||
"""
|
"""
|
||||||
Explicitly set the timer, overriding time.time()
|
Explicitly set the timer, overriding time.time()
|
||||||
|
@ -71,7 +71,7 @@ class ThrottlingTests(TestCase):
|
||||||
|
|
||||||
response = MockView.as_view()(request)
|
response = MockView.as_view()(request)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
|
|
||||||
def ensure_is_throttled(self, view, expect):
|
def ensure_is_throttled(self, view, expect):
|
||||||
request = self.factory.get('/')
|
request = self.factory.get('/')
|
||||||
request.user = User.objects.create(username='a')
|
request.user = User.objects.create(username='a')
|
||||||
|
@ -80,27 +80,27 @@ class ThrottlingTests(TestCase):
|
||||||
request.user = User.objects.create(username='b')
|
request.user = User.objects.create(username='b')
|
||||||
response = view.as_view()(request)
|
response = view.as_view()(request)
|
||||||
self.assertEqual(expect, response.status_code)
|
self.assertEqual(expect, response.status_code)
|
||||||
|
|
||||||
def test_request_throttling_is_per_user(self):
|
def test_request_throttling_is_per_user(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is only limited per user, not globally for
|
Ensure request rate is only limited per user, not globally for
|
||||||
PerUserThrottles
|
PerUserThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView, 200)
|
self.ensure_is_throttled(MockView, 200)
|
||||||
|
|
||||||
def test_request_throttling_is_per_view(self):
|
def test_request_throttling_is_per_view(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited globally per View for PerViewThrottles
|
Ensure request rate is limited globally per View for PerViewThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
||||||
|
|
||||||
def test_request_throttling_is_per_resource(self):
|
def test_request_throttling_is_per_resource(self):
|
||||||
"""
|
"""
|
||||||
Ensure request rate is limited globally per Resource for PerResourceThrottles
|
Ensure request rate is limited globally per Resource for PerResourceThrottles
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
|
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
|
||||||
|
|
||||||
|
|
||||||
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
||||||
"""
|
"""
|
||||||
Ensure the response returns an X-Throttle field with status and next attributes
|
Ensure the response returns an X-Throttle field with status and next attributes
|
||||||
|
@ -111,7 +111,7 @@ class ThrottlingTests(TestCase):
|
||||||
self.set_throttle_timer(view, timer)
|
self.set_throttle_timer(view, timer)
|
||||||
response = view.as_view()(request)
|
response = view.as_view()(request)
|
||||||
self.assertEquals(response['X-Throttle'], expect)
|
self.assertEquals(response['X-Throttle'], expect)
|
||||||
|
|
||||||
def test_seconds_fields(self):
|
def test_seconds_fields(self):
|
||||||
"""
|
"""
|
||||||
Ensure for second based throttles.
|
Ensure for second based throttles.
|
||||||
|
@ -122,7 +122,7 @@ class ThrottlingTests(TestCase):
|
||||||
(0, 'status=SUCCESS; next=1.00 sec'),
|
(0, 'status=SUCCESS; next=1.00 sec'),
|
||||||
(0, 'status=FAILURE; next=1.00 sec')
|
(0, 'status=FAILURE; next=1.00 sec')
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_minutes_fields(self):
|
def test_minutes_fields(self):
|
||||||
"""
|
"""
|
||||||
Ensure for minute based throttles.
|
Ensure for minute based throttles.
|
||||||
|
@ -133,7 +133,7 @@ class ThrottlingTests(TestCase):
|
||||||
(0, 'status=SUCCESS; next=60.00 sec'),
|
(0, 'status=SUCCESS; next=60.00 sec'),
|
||||||
(0, 'status=FAILURE; next=60.00 sec')
|
(0, 'status=FAILURE; next=60.00 sec')
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_next_rate_remains_constant_if_followed(self):
|
def test_next_rate_remains_constant_if_followed(self):
|
||||||
"""
|
"""
|
||||||
If a client follows the recommended next request rate,
|
If a client follows the recommended next request rate,
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.resources import FormResource, ModelResource
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.resources import Resource
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestDisabledValidations(TestCase):
|
class TestDisabledValidations(TestCase):
|
||||||
|
@ -22,7 +19,7 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||||
|
|
||||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||||
|
@ -33,12 +30,11 @@ class TestDisabledValidations(TestCase):
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
resource = DisabledFormResource
|
resource = DisabledFormResource
|
||||||
|
|
||||||
view = MockView()
|
view = MockView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||||
"""If the view's form is None and does not have a Resource with a model set then
|
"""If the view's form is None and does not have a Resource with a model set then
|
||||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||||
|
@ -47,17 +43,18 @@ class TestDisabledValidations(TestCase):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)#
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||||
class DisabledModelFormView(View):
|
class DisabledModelFormView(View):
|
||||||
resource = ModelResource
|
resource = ModelResource
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
class TestNonFieldErrors(TestCase):
|
class TestNonFieldErrors(TestCase):
|
||||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||||
|
@ -68,15 +65,15 @@ class TestNonFieldErrors(TestCase):
|
||||||
field1 = forms.CharField(required=False)
|
field1 = forms.CharField(required=False)
|
||||||
field2 = forms.CharField(required=False)
|
field2 = forms.CharField(required=False)
|
||||||
ERROR_TEXT = 'You may not supply both field1 and field2'
|
ERROR_TEXT = 'You may not supply both field1 and field2'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
||||||
raise forms.ValidationError(self.ERROR_TEXT)
|
raise forms.ValidationError(self.ERROR_TEXT)
|
||||||
return self.cleaned_data #pragma: no cover
|
return self.cleaned_data
|
||||||
|
|
||||||
class MockResource(FormResource):
|
class MockResource(FormResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -84,41 +81,40 @@ class TestNonFieldErrors(TestCase):
|
||||||
content = {'field1': 'example1', 'field2': 'example2'}
|
content = {'field1': 'example1', 'field2': 'example2'}
|
||||||
try:
|
try:
|
||||||
MockResource(view).validate_request(content, None)
|
MockResource(view).validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
else:
|
else:
|
||||||
self.fail('ErrorResponse was not raised') #pragma: no cover
|
self.fail('ErrorResponse was not raised')
|
||||||
|
|
||||||
|
|
||||||
class TestFormValidation(TestCase):
|
class TestFormValidation(TestCase):
|
||||||
"""Tests which check basic form validation.
|
"""Tests which check basic form validation.
|
||||||
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
||||||
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class MockForm(forms.Form):
|
class MockForm(forms.Form):
|
||||||
qwerty = forms.CharField(required=True)
|
qwerty = forms.CharField(required=True)
|
||||||
|
|
||||||
class MockFormResource(FormResource):
|
class MockFormResource(FormResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockModelResource(ModelResource):
|
class MockModelResource(ModelResource):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
|
|
||||||
class MockFormView(View):
|
class MockFormView(View):
|
||||||
resource = MockFormResource
|
resource = MockFormResource
|
||||||
|
|
||||||
class MockModelFormView(View):
|
class MockModelFormView(View):
|
||||||
resource = MockModelResource
|
resource = MockModelResource
|
||||||
|
|
||||||
self.MockFormResource = MockFormResource
|
self.MockFormResource = MockFormResource
|
||||||
self.MockModelResource = MockModelResource
|
self.MockModelResource = MockModelResource
|
||||||
self.MockFormView = MockFormView
|
self.MockFormView = MockFormView
|
||||||
self.MockModelFormView = MockModelFormView
|
self.MockModelFormView = MockModelFormView
|
||||||
|
|
||||||
|
|
||||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
"""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)
|
self.assertEqual(validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def validation_failure_raises_response_exception(self, validator):
|
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.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
validator._validate(content, None, allowed_extra_fields=('extra',))
|
validator._validate(content, None, allowed_extra_fields=('extra',))
|
||||||
|
|
||||||
|
def validation_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):
|
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
||||||
content = {'qwerty': 'uiop'}
|
content = {'qwerty': 'uiop'}
|
||||||
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
||||||
|
|
||||||
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
||||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||||
content = {}
|
content = {}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
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"""
|
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
||||||
content = {'qwerty': ''}
|
content = {'qwerty': ''}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
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"""
|
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
||||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
"""If validation for multiple reasons, ensure the response contains each error"""
|
||||||
content = {'qwerty': '', 'extra': 'extra'}
|
content = {'qwerty': '', 'extra': 'extra'}
|
||||||
try:
|
try:
|
||||||
validator.validate_request(content, None)
|
validator.validate_request(content, None)
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
|
self.assertEqual(exc.response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
||||||
'extra': ['This field does not exist.']}})
|
'extra': ['This field does not exist.']}})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
# Tests on FormResource
|
# Tests on FormResource
|
||||||
|
|
||||||
|
@ -202,6 +208,10 @@ class TestFormValidation(TestCase):
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
validator = self.MockFormResource(self.MockFormView())
|
||||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
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):
|
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
validator = self.MockFormResource(self.MockFormView())
|
||||||
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
||||||
|
@ -263,56 +273,53 @@ class TestFormValidation(TestCase):
|
||||||
|
|
||||||
class TestModelFormValidator(TestCase):
|
class TestModelFormValidator(TestCase):
|
||||||
"""Tests specific to ModelFormValidatorMixin"""
|
"""Tests specific to ModelFormValidatorMixin"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create a validator for a model with two fields and a property."""
|
"""Create a validator for a model with two fields and a property."""
|
||||||
class MockModel(models.Model):
|
class MockModel(models.Model):
|
||||||
qwerty = models.CharField(max_length=256)
|
qwerty = models.CharField(max_length=256)
|
||||||
uiop = models.CharField(max_length=256, blank=True)
|
uiop = models.CharField(max_length=256, blank=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def readonly(self):
|
def readonly(self):
|
||||||
return 'read only'
|
return 'read only'
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
class MockResource(ModelResource):
|
||||||
model = MockModel
|
model = MockModel
|
||||||
|
|
||||||
class MockView(View):
|
class MockView(View):
|
||||||
resource = MockResource
|
resource = MockResource
|
||||||
|
|
||||||
self.validator = MockResource(MockView)
|
|
||||||
|
|
||||||
|
self.validator = MockResource(MockView)
|
||||||
|
|
||||||
def test_property_fields_are_allowed_on_model_forms(self):
|
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."""
|
"""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)
|
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def test_property_fields_are_not_required_on_model_forms(self):
|
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."""
|
"""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)
|
self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
|
|
||||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
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.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
|
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_requires_fields_on_model_forms(self):
|
def test_validate_requires_fields_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
content = {'readonly': 'read only'}
|
content = {'readonly': 'read only'}
|
||||||
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
self.assertRaises(ErrorResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||||
content = {'qwerty':'example', 'readonly': 'read only'}
|
content = {'qwerty': 'example', 'readonly': 'read only'}
|
||||||
self.validator.validate_request(content, None)
|
self.validator.validate_request(content, None)
|
||||||
|
|
||||||
def test_model_form_validator_uses_model_forms(self):
|
def test_model_form_validator_uses_model_forms(self):
|
||||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 TestCase
|
||||||
from django.test import Client
|
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',
|
class MockView(View):
|
||||||
url(r'^robots.txt$', 'deny_robots'),
|
"""This is a basic mock view"""
|
||||||
url(r'^favicon.ico$', 'favicon'),
|
pass
|
||||||
url(r'^accounts/login$', 'api_login'),
|
|
||||||
url(r'^accounts/logout$', 'api_logout'),
|
|
||||||
|
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"""
|
"""Test the extra views djangorestframework provides"""
|
||||||
urls = 'djangorestframework.tests.views'
|
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')
|
|
||||||
|
|
||||||
def test_login_view(self):
|
def test_login_view(self):
|
||||||
"""Ensure the login view exists"""
|
"""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.status_code, 200)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||||
|
|
||||||
def test_logout_view(self):
|
def test_logout_view(self):
|
||||||
"""Ensure the logout view exists"""
|
"""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.status_code, 200)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add login/logout behaviour tests
|
# TODO: Add login/logout behaviour tests
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
from django.conf.urls.defaults import patterns
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
|
||||||
(r'robots.txt', 'deny_robots'),
|
template_name = {'template_name': 'djangorestframework/login.html'}
|
||||||
(r'^accounts/login/$', 'api_login'),
|
|
||||||
(r'^accounts/logout/$', 'api_logout'),
|
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'),
|
|
||||||
)
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import django
|
||||||
from django.utils.encoding import smart_unicode
|
from django.utils.encoding import smart_unicode
|
||||||
from django.utils.xmlutils import SimplerXMLGenerator
|
from django.utils.xmlutils import SimplerXMLGenerator
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
|
@ -8,11 +9,6 @@ from djangorestframework.compat import StringIO
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
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 media_type_matches, is_form_media_type
|
||||||
from mediatypes import add_media_type_param, get_media_type_params, order_by_precedence
|
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
|
||||||
return (obj,)
|
return (obj,)
|
||||||
|
|
||||||
|
|
||||||
def url_resolves(url):
|
def url_resolves(url):
|
||||||
"""
|
"""
|
||||||
Return True if the given URL is mapped to a view in the urlconf, False otherwise.
|
Return True if the given URL is mapped to a view in the urlconf, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resolve(url)
|
resolve(url)
|
||||||
except:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return True
|
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
|
# From xml2dict
|
||||||
class XML2Dict(object):
|
class XML2Dict(object):
|
||||||
|
|
||||||
|
@ -103,8 +67,8 @@ class XML2Dict(object):
|
||||||
old = node_tree[tag]
|
old = node_tree[tag]
|
||||||
if not isinstance(old, list):
|
if not isinstance(old, list):
|
||||||
node_tree.pop(tag)
|
node_tree.pop(tag)
|
||||||
node_tree[tag] = [old] # multi times, so change old dict to a list
|
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].append(tree) # add the new one
|
||||||
|
|
||||||
return node_tree
|
return node_tree
|
||||||
|
|
||||||
|
@ -117,13 +81,13 @@ class XML2Dict(object):
|
||||||
"""
|
"""
|
||||||
result = re.compile("\{(.*)\}(.*)").search(tag)
|
result = re.compile("\{(.*)\}(.*)").search(tag)
|
||||||
if result:
|
if result:
|
||||||
value.namespace, tag = result.groups()
|
value.namespace, tag = result.groups()
|
||||||
return (tag, value)
|
return (tag, value)
|
||||||
|
|
||||||
def parse(self, file):
|
def parse(self, file):
|
||||||
"""parse a xml file to a dict"""
|
"""parse a xml file to a dict"""
|
||||||
f = open(file, 'r')
|
f = open(file, 'r')
|
||||||
return self.fromstring(f.read())
|
return self.fromstring(f.read())
|
||||||
|
|
||||||
def fromstring(self, s):
|
def fromstring(self, s):
|
||||||
"""parse a string"""
|
"""parse a string"""
|
||||||
|
@ -151,11 +115,15 @@ class XMLRenderer():
|
||||||
self._to_xml(xml, value)
|
self._to_xml(xml, value)
|
||||||
xml.endElement(key)
|
xml.endElement(key)
|
||||||
|
|
||||||
|
elif data is None:
|
||||||
|
# Don't output any value
|
||||||
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
xml.characters(smart_unicode(data))
|
xml.characters(smart_unicode(data))
|
||||||
|
|
||||||
def dict2xml(self, data):
|
def dict2xml(self, data):
|
||||||
stream = StringIO.StringIO()
|
stream = StringIO.StringIO()
|
||||||
|
|
||||||
xml = SimplerXMLGenerator(stream, "utf-8")
|
xml = SimplerXMLGenerator(stream, "utf-8")
|
||||||
xml.startDocument()
|
xml.startDocument()
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
from djangorestframework.utils.description import get_name
|
|
||||||
|
|
||||||
def get_breadcrumbs(url):
|
def get_breadcrumbs(url):
|
||||||
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""
|
||||||
|
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
|
|
||||||
def breadcrumbs_recursive(url, breadcrumbs_list):
|
def breadcrumbs_recursive(url, breadcrumbs_list):
|
||||||
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
|
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(view, unused_args, unused_kwargs) = resolve(url)
|
(view, unused_args, unused_kwargs) = resolve(url)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||||
if isinstance(getattr(view, 'cls_instance', None), View):
|
if isinstance(getattr(view, 'cls_instance', None), View):
|
||||||
breadcrumbs_list.insert(0, (get_name(view), url))
|
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url))
|
||||||
|
|
||||||
if url == '':
|
if url == '':
|
||||||
# All done
|
# All done
|
||||||
return breadcrumbs_list
|
return breadcrumbs_list
|
||||||
|
|
||||||
elif url.endswith('/'):
|
elif url.endswith('/'):
|
||||||
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
|
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
|
||||||
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
|
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)
|
||||||
|
|
||||||
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
|
# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
|
||||||
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
|
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -61,10 +61,10 @@ def order_by_precedence(media_type_lst):
|
||||||
1. 'type/*'
|
1. 'type/*'
|
||||||
0. '*/*'
|
0. '*/*'
|
||||||
"""
|
"""
|
||||||
ret = [[],[],[],[]]
|
ret = [[], [], [], []]
|
||||||
for media_type in media_type_lst:
|
for media_type in media_type_lst:
|
||||||
precedence = _MediaType(media_type).precedence
|
precedence = _MediaType(media_type).precedence
|
||||||
ret[3-precedence].append(media_type)
|
ret[3 - precedence].append(media_type)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,29 +103,6 @@ class _MediaType(object):
|
||||||
return 2
|
return 2
|
||||||
return 3
|
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):
|
def __str__(self):
|
||||||
return unicode(self).encode('utf-8')
|
return unicode(self).encode('utf-8')
|
||||||
|
|
||||||
|
@ -134,4 +111,3 @@ class _MediaType(object):
|
||||||
for key, val in self.params.items():
|
for key, val in self.params.items():
|
||||||
ret += "; %s=%s" % (key, val)
|
ret += "; %s=%s" % (key, val)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -5,11 +5,13 @@ be subclassing in your implementation.
|
||||||
By setting or modifying class attributes on your view, you change it's predefined behaviour.
|
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.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 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.response import Response, ErrorResponse
|
||||||
from djangorestframework.mixins import *
|
from djangorestframework.mixins import *
|
||||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
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):
|
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.
|
Performs request deserialization, response serialization, authentication and input validation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
resource = None
|
||||||
"""
|
"""
|
||||||
The resource to use when validating requests and filtering responses,
|
The resource to use when validating requests and filtering responses,
|
||||||
or `None` to use default behaviour.
|
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.
|
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.
|
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.
|
List of all authenticating methods to attempt.
|
||||||
"""
|
"""
|
||||||
authentication = ( authentication.UserLoggedInAuthentication,
|
|
||||||
authentication.BasicAuthentication )
|
permissions = (permissions.FullAnonAccess,)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
List of all permissions that must be checked.
|
List of all permissions that must be checked.
|
||||||
"""
|
"""
|
||||||
permissions = ( permissions.FullAnonAccess, )
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
"""
|
"""
|
||||||
Override the default :meth:`as_view` to store an instance of the view
|
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
|
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 = super(View, cls).as_view(**initkwargs)
|
||||||
view.cls_instance = cls(**initkwargs)
|
view.cls_instance = cls(**initkwargs)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allowed_methods(self):
|
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)]
|
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):
|
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,
|
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||||
|
|
||||||
|
|
||||||
def initial(self, request, *args, **kargs):
|
def initial(self, request, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
Hook for any code that needs to run prior to anything else.
|
Hook for any code that needs to run prior to anything else.
|
||||||
|
@ -95,14 +185,25 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
"""
|
"""
|
||||||
pass
|
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):
|
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
|
self.headers[field] = value
|
||||||
|
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
# all other authentication is CSRF exempt.
|
# all other authentication is CSRF exempt.
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
@ -112,13 +213,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.headers = {}
|
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:
|
try:
|
||||||
self.initial(request, *args, **kwargs)
|
self.initial(request, *args, **kwargs)
|
||||||
|
|
||||||
# Authenticate and check request has the relevant permissions
|
# Authenticate and check request has the relevant permissions
|
||||||
self._check_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)
|
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||||
response.cleaned_content = self.filter_response(response.raw_content)
|
response.cleaned_content = self.filter_response(response.raw_content)
|
||||||
|
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
response = exc.response
|
response = exc.response
|
||||||
|
|
||||||
# Always add these headers.
|
return self.final(request, response, *args, **kwargs)
|
||||||
#
|
|
||||||
# TODO - this isn't actually the correct way to set the vary header,
|
def options(self, request, *args, **kwargs):
|
||||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
response_obj = {
|
||||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
'name': self.get_name(),
|
||||||
response.headers['Vary'] = 'Authenticate, Accept'
|
'description': self.get_description(),
|
||||||
|
'renders': self._rendered_media_types,
|
||||||
# merge with headers possibly set at some point in the view
|
'parses': self._parsed_media_types,
|
||||||
response.headers.update(self.headers)
|
}
|
||||||
|
form = self.get_bound_form()
|
||||||
return self.render(response)
|
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):
|
class ModelView(View):
|
||||||
|
@ -165,20 +270,23 @@ class ModelView(View):
|
||||||
"""
|
"""
|
||||||
resource = resources.ModelResource
|
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.
|
A view which provides default operations for read/update/delete against a model instance.
|
||||||
"""
|
"""
|
||||||
_suffix = 'Instance'
|
_suffix = 'Instance'
|
||||||
|
|
||||||
|
|
||||||
class ListModelView(ListModelMixin, ModelView):
|
class ListModelView(ListModelMixin, ModelView):
|
||||||
"""
|
"""
|
||||||
A view which provides default operations for list, against a model in the database.
|
A view which provides default operations for list, against a model in the database.
|
||||||
"""
|
"""
|
||||||
_suffix = 'List'
|
_suffix = 'List'
|
||||||
|
|
||||||
|
|
||||||
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
||||||
"""
|
"""
|
||||||
A view which provides default operations for list and create, against a model in the database.
|
A view which provides default operations for list and create, against a model in the database.
|
||||||
"""
|
"""
|
||||||
_suffix = 'List'
|
_suffix = 'List'
|
||||||
|
|
9
docs/check_sphinx.py
Normal file
9
docs/check_sphinx.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import pytest
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def test_build_docs(tmpdir):
|
||||||
|
doctrees = tmpdir.join("doctrees")
|
||||||
|
htmldir = "html" #we want to keep the docs
|
||||||
|
subprocess.check_call([
|
||||||
|
"sphinx-build", "-q", "-bhtml",
|
||||||
|
"-d", str(doctrees), ".", str(htmldir)])
|
16
docs/conf.py
16
docs/conf.py
|
@ -14,8 +14,8 @@
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'djangorestframework'))
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'djangorestframework')) # for documenting the library
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'examples'))
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'examples')) # for importing settings
|
||||||
import settings
|
import settings
|
||||||
from django.core.management import setup_environ
|
from django.core.management import setup_environ
|
||||||
setup_environ(settings)
|
setup_environ(settings)
|
||||||
|
@ -55,9 +55,13 @@ copyright = u'2011, Tom Christie'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.1'
|
|
||||||
|
import djangorestframework
|
||||||
|
|
||||||
|
version = djangorestframework.__version__
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.1'
|
release = version
|
||||||
|
|
||||||
autodoc_member_order='bysource'
|
autodoc_member_order='bysource'
|
||||||
|
|
||||||
|
@ -100,7 +104,7 @@ pygments_style = 'sphinx'
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'default'
|
html_theme = 'sphinxdoc'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
|
@ -220,3 +224,5 @@ html_static_path = []
|
||||||
#man_pages = [
|
#man_pages = [
|
||||||
# ()
|
# ()
|
||||||
#]
|
#]
|
||||||
|
|
||||||
|
linkcheck_timeout = 120 # seconds, set to extra large value for link_checks
|
||||||
|
|
10
docs/contents.rst
Normal file
10
docs/contents.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
howto
|
||||||
|
library
|
||||||
|
examples
|
||||||
|
|
23
docs/examples.rst
Normal file
23
docs/examples.rst
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
There are a few real world web API examples included with Django REST framework.
|
||||||
|
|
||||||
|
#. :doc:`examples/objectstore` - Using :class:`views.View` classes for APIs that do not map to models.
|
||||||
|
#. :doc:`examples/pygments` - Using :class:`views.View` classes with forms for input validation.
|
||||||
|
#. :doc:`examples/blogpost` - Using :class:`views.ModelView` classes for APIs that map directly to models.
|
||||||
|
|
||||||
|
All the examples are freely available for testing in the sandbox:
|
||||||
|
|
||||||
|
http://rest.ep.io
|
||||||
|
|
||||||
|
(The :doc:`examples/sandbox` resource is also documented.)
|
||||||
|
|
||||||
|
Example Reference
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
examples/*
|
|
@ -1,9 +1,7 @@
|
||||||
.. _blogposts:
|
|
||||||
|
|
||||||
Blog Posts API
|
Blog Posts API
|
||||||
==============
|
==============
|
||||||
|
|
||||||
* http://api.django-rest-framework.org/blog-post/
|
* http://rest.ep.io/blog-post/
|
||||||
|
|
||||||
The models
|
The models
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
.. _modelviews:
|
|
||||||
|
|
||||||
Getting Started - Model Views
|
Getting Started - Model Views
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
@ -7,11 +5,11 @@ Getting Started - Model Views
|
||||||
|
|
||||||
A live sandbox instance of this API is available:
|
A live sandbox instance of this API is available:
|
||||||
|
|
||||||
http://api.django-rest-framework.org/model-resource-example/
|
http://rest.ep.io/model-resource-example/
|
||||||
|
|
||||||
You can browse the API using a web browser, or from the command line::
|
You can browse the API using a web browser, or from the command line::
|
||||||
|
|
||||||
curl -X GET http://api.django-rest-framework.org/resource-example/ -H 'Accept: text/plain'
|
curl -X GET http://rest.ep.io/resource-example/ -H 'Accept: text/plain'
|
||||||
|
|
||||||
Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways:
|
Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways:
|
||||||
|
|
||||||
|
@ -43,16 +41,16 @@ And we're done. We've now got a fully browseable API, which supports multiple i
|
||||||
|
|
||||||
We can visit the API in our browser:
|
We can visit the API in our browser:
|
||||||
|
|
||||||
* http://api.django-rest-framework.org/model-resource-example/
|
* http://rest.ep.io/model-resource-example/
|
||||||
|
|
||||||
Or access it from the command line using curl:
|
Or access it from the command line using curl:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# Demonstrates API's input validation using form input
|
# Demonstrates API's input validation using form input
|
||||||
bash: curl -X POST --data 'foo=true' http://api.django-rest-framework.org/model-resource-example/
|
bash: curl -X POST --data 'foo=true' http://rest.ep.io/model-resource-example/
|
||||||
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
||||||
|
|
||||||
# Demonstrates API's input validation using JSON input
|
# Demonstrates API's input validation using JSON input
|
||||||
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://api.django-rest-framework.org/model-resource-example/
|
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://rest.ep.io/model-resource-example/
|
||||||
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
.. _objectstore:
|
|
||||||
|
|
||||||
Object Store API
|
Object Store API
|
||||||
================
|
================
|
||||||
|
|
||||||
* http://api.django-rest-framework.org/object-store/
|
* http://rest.ep.io/object-store/
|
||||||
|
|
||||||
This example shows an object store API that can be used to store arbitrary serializable content.
|
This example shows an object store API that can be used to store arbitrary serializable content.
|
||||||
|
|
||||||
|
|
66
docs/examples/permissions.rst
Normal file
66
docs/examples/permissions.rst
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
Permissions
|
||||||
|
===========
|
||||||
|
|
||||||
|
This example will show how you can protect your api by using authentication
|
||||||
|
and how you can limit the amount of requests a user can do to a resource by setting
|
||||||
|
a throttle to your view.
|
||||||
|
|
||||||
|
Authentication
|
||||||
|
--------------
|
||||||
|
|
||||||
|
If you want to protect your api from unauthorized users, Django REST Framework
|
||||||
|
offers you two default authentication methods:
|
||||||
|
|
||||||
|
* Basic Authentication
|
||||||
|
* Django's session-based authentication
|
||||||
|
|
||||||
|
These authentication methods are by default enabled. But they are not used unless
|
||||||
|
you specifically state that your view requires authentication.
|
||||||
|
|
||||||
|
To do this you just need to import the `Isauthenticated` class from the frameworks' `permissions` module.::
|
||||||
|
|
||||||
|
from djangorestframework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
Then you enable authentication by setting the right 'permission requirement' to the `permissions` class attribute of your View like
|
||||||
|
the example View below.:
|
||||||
|
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/permissionsexample/views.py
|
||||||
|
:pyobject: LoggedInExampleView
|
||||||
|
|
||||||
|
The `IsAuthenticated` permission will only let a user do a 'GET' if he is authenticated. Try it
|
||||||
|
yourself on the live sandbox__
|
||||||
|
|
||||||
|
__ http://rest.ep.io/permissions-example/loggedin
|
||||||
|
|
||||||
|
|
||||||
|
Throttling
|
||||||
|
----------
|
||||||
|
|
||||||
|
If you want to limit the amount of requests a client is allowed to do on
|
||||||
|
a resource, then you can set a 'throttle' to achieve this.
|
||||||
|
|
||||||
|
For this to work you'll need to import the `PerUserThrottling` class from the `permissions`
|
||||||
|
module.::
|
||||||
|
|
||||||
|
from djangorestframework.permissions import PerUserThrottling
|
||||||
|
|
||||||
|
In the example below we have limited the amount of requests one 'client' or 'user'
|
||||||
|
may do on our view to 10 requests per minute.:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/permissionsexample/views.py
|
||||||
|
:pyobject: ThrottlingExampleView
|
||||||
|
|
||||||
|
Try it yourself on the live sandbox__.
|
||||||
|
|
||||||
|
__ http://rest.ep.io/permissions-example/throttling
|
||||||
|
|
||||||
|
Now if you want a view to require both aurhentication and throttling, you simply declare them
|
||||||
|
both::
|
||||||
|
|
||||||
|
permissions = (PerUserThrottling, Isauthenticated)
|
||||||
|
|
||||||
|
To see what other throttles are available, have a look at the :mod:`permissions` module.
|
||||||
|
|
||||||
|
If you want to implement your own authentication method, then refer to the :mod:`authentication`
|
||||||
|
module.
|
|
@ -1,5 +1,3 @@
|
||||||
.. _codehighlighting:
|
|
||||||
|
|
||||||
Code Highlighting API
|
Code Highlighting API
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
@ -8,11 +6,11 @@ We're going to provide a simple wrapper around the awesome `pygments <http://pyg
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
A live sandbox instance of this API is available at http://api.django-rest-framework.org/pygments/
|
A live sandbox instance of this API is available at http://rest.ep.io/pygments/
|
||||||
|
|
||||||
You can browse the API using a web browser, or from the command line::
|
You can browse the API using a web browser, or from the command line::
|
||||||
|
|
||||||
curl -X GET http://api.django-rest-framework.org/pygments/ -H 'Accept: text/plain'
|
curl -X GET http://rest.ep.io/pygments/ -H 'Accept: text/plain'
|
||||||
|
|
||||||
|
|
||||||
URL configuration
|
URL configuration
|
||||||
|
@ -79,13 +77,13 @@ For example if we make a POST request using form input:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
bash: curl -X POST --data 'code=print "hello, world!"' --data 'style=foobar' -H 'X-Requested-With: XMLHttpRequest' http://api.django-rest-framework.org/pygments/
|
bash: curl -X POST --data 'code=print "hello, world!"' --data 'style=foobar' -H 'X-Requested-With: XMLHttpRequest' http://rest.ep.io/pygments/
|
||||||
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}
|
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}
|
||||||
|
|
||||||
Or if we make the same request using JSON:
|
Or if we make the same request using JSON:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
bash: curl -X POST --data-binary '{"code":"print \"hello, world!\"", "style":"foobar"}' -H 'Content-Type: application/json' -H 'X-Requested-With: XMLHttpRequest' http://api.django-rest-framework.org/pygments/
|
bash: curl -X POST --data-binary '{"code":"print \"hello, world!\"", "style":"foobar"}' -H 'Content-Type: application/json' -H 'X-Requested-With: XMLHttpRequest' http://rest.ep.io/pygments/
|
||||||
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}
|
{"detail": {"style": ["Select a valid choice. foobar is not one of the available choices."], "lexer": ["This field is required."]}}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
.. _sandbox:
|
|
||||||
|
|
||||||
Sandbox Root API
|
Sandbox Root API
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
.. _views:
|
|
||||||
|
|
||||||
Getting Started - Views
|
Getting Started - Views
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
@ -7,11 +5,11 @@ Getting Started - Views
|
||||||
|
|
||||||
A live sandbox instance of this API is available:
|
A live sandbox instance of this API is available:
|
||||||
|
|
||||||
http://api.django-rest-framework.org/resource-example/
|
http://rest.ep.io/resource-example/
|
||||||
|
|
||||||
You can browse the API using a web browser, or from the command line::
|
You can browse the API using a web browser, or from the command line::
|
||||||
|
|
||||||
curl -X GET http://api.django-rest-framework.org/resource-example/ -H 'Accept: text/plain'
|
curl -X GET http://rest.ep.io/resource-example/ -H 'Accept: text/plain'
|
||||||
|
|
||||||
We're going to start off with a simple example, that demonstrates a few things:
|
We're going to start off with a simple example, that demonstrates a few things:
|
||||||
|
|
||||||
|
@ -43,16 +41,16 @@ Now we'll write our views. The first is a read only view that links to three in
|
||||||
|
|
||||||
That's us done. Our API now provides both programmatic access using JSON and XML, as well a nice browseable HTML view, so we can now access it both from the browser:
|
That's us done. Our API now provides both programmatic access using JSON and XML, as well a nice browseable HTML view, so we can now access it both from the browser:
|
||||||
|
|
||||||
* http://api.django-rest-framework.org/resource-example/
|
* http://rest.ep.io/resource-example/
|
||||||
|
|
||||||
And from the command line:
|
And from the command line:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# Demonstrates API's input validation using form input
|
# Demonstrates API's input validation using form input
|
||||||
bash: curl -X POST --data 'foo=true' http://api.django-rest-framework.org/resource-example/1/
|
bash: curl -X POST --data 'foo=true' http://rest.ep.io/resource-example/1/
|
||||||
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
||||||
|
|
||||||
# Demonstrates API's input validation using JSON input
|
# Demonstrates API's input validation using JSON input
|
||||||
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://api.django-rest-framework.org/resource-example/1/
|
bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://rest.ep.io/resource-example/1/
|
||||||
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
{"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}}
|
||||||
|
|
8
docs/howto.rst
Normal file
8
docs/howto.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
How Tos, FAQs & Notes
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
howto/*
|
|
@ -7,7 +7,7 @@ Alternative frameworks
|
||||||
There are a number of alternative REST frameworks for Django:
|
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-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/>`_
|
* Other interesting projects include `dagny <https://github.com/zacharyvoase/dagny>`_ and `dj-webmachine <http://benoitc.github.com/dj-webmachine/>`_
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
Using Django REST framework Mixin classes
|
Using Django REST framework Mixin classes
|
||||||
=========================================
|
=========================================
|
||||||
|
|
||||||
This example demonstrates creating a REST API **without** using Django REST framework's :class:`.Resource` or :class:`.ModelResource`,
|
This example demonstrates creating a REST API **without** using Django REST framework's :class:`.Resource` or :class:`.ModelResource`, but instead using Django's :class:`View` class, and adding the :class:`ResponseMixin` class to provide full HTTP Accept header content negotiation,
|
||||||
but instead using Django :class:`View` class, and adding the :class:`EmitterMixin` class to provide full HTTP Accept header content negotiation,
|
|
||||||
a browseable Web API, and much of the other goodness that Django REST framework gives you for free.
|
a browseable Web API, and much of the other goodness that Django REST framework gives you for free.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
A live sandbox instance of this API is available for testing:
|
A live sandbox instance of this API is available for testing:
|
||||||
|
|
||||||
* http://api.django-rest-framework.org/mixin/
|
* http://rest.ep.io/mixin/
|
||||||
|
|
||||||
You can browse the API using a web browser, or from the command line::
|
You can browse the API using a web browser, or from the command line::
|
||||||
|
|
||||||
curl -X GET http://api.django-rest-framework.org/mixin/
|
curl -X GET http://rest.ep.io/mixin/
|
||||||
|
|
||||||
|
|
||||||
URL configuration
|
URL configuration
|
||||||
|
@ -26,5 +25,6 @@ Everything we need for this example can go straight into the URL conf...
|
||||||
.. include:: ../../examples/mixin/urls.py
|
.. include:: ../../examples/mixin/urls.py
|
||||||
:literal:
|
:literal:
|
||||||
|
|
||||||
That's it. Auto-magically our API now supports multiple output formats, specified either by using standard HTTP Accept header content negotiation, or by using the `&_accept=application/json` style parameter overrides.
|
That's it. Auto-magically our API now supports multiple output formats, specified either by using
|
||||||
|
standard HTTP Accept header content negotiation, or by using the `&_accept=application/json` style parameter overrides.
|
||||||
We even get a nice HTML view which can be used to self-document our API.
|
We even get a nice HTML view which can be used to self-document our API.
|
||||||
|
|
39
docs/howto/reverse.rst
Normal file
39
docs/howto/reverse.rst
Normal 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
|
|
@ -3,48 +3,71 @@
|
||||||
Setup
|
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
|
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',
|
Login / Logout
|
||||||
(r'robots.txt', 'deny_robots'),
|
--------------
|
||||||
(r'favicon.ico', 'favicon'),
|
|
||||||
# Add your resources here
|
|
||||||
(r'^accounts/login/$', 'api_login'),
|
|
||||||
(r'^accounts/logout/$', 'api_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
|
|
@ -27,4 +27,4 @@ There are a few things that can be helpful to remember when using CURL with djan
|
||||||
|
|
||||||
#. You can use basic authentication to send the username and password::
|
#. You can use basic authentication to send the username and password::
|
||||||
|
|
||||||
curl -X GET -H 'Accept: application/json' -u <user>:<password> http://example.com/my-api/
|
curl -X GET -H 'Accept: application/json' -u <user>:<password> http://example.com/my-api/
|
||||||
|
|
39
docs/howto/usingurllib2.rst
Normal file
39
docs/howto/usingurllib2.rst
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
Using urllib2 with Django REST Framework
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Python's standard library comes with some nice modules
|
||||||
|
you can use to test your api or even write a full client.
|
||||||
|
|
||||||
|
Using the 'GET' method
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Here's an example which does a 'GET' on the `model-resource` example
|
||||||
|
in the sandbox.::
|
||||||
|
|
||||||
|
>>> import urllib2
|
||||||
|
>>> r = urllib2.urlopen('htpp://rest.ep.io/model-resource-example')
|
||||||
|
>>> r.getcode() # Check if the response was ok
|
||||||
|
200
|
||||||
|
>>> print r.read() # Examin the response itself
|
||||||
|
[{"url": "http://rest.ep.io/model-resource-example/1/", "baz": "sdf", "foo": true, "bar": 123}]
|
||||||
|
|
||||||
|
Using the 'POST' method
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
And here's an example which does a 'POST' to create a new instance. First let's encode
|
||||||
|
the data we want to POST. We'll use `urllib` for encoding and the `time` module
|
||||||
|
to send the current time as as a string value for our POST.::
|
||||||
|
|
||||||
|
>>> import urllib, time
|
||||||
|
>>> d = urllib.urlencode((('bar', 123), ('baz', time.asctime())))
|
||||||
|
|
||||||
|
Now use the `Request` class and specify the 'Content-type'::
|
||||||
|
|
||||||
|
>>> req = urllib2.Request('http://rest.ep.io/model-resource-example/', data=d, headers={'Content-Type':'application/x-www-form-urlencoded'})
|
||||||
|
>>> resp = urllib2.urlopen(req)
|
||||||
|
>>> resp.getcode()
|
||||||
|
201
|
||||||
|
>>> resp.read()
|
||||||
|
'{"url": "http://rest.ep.io/model-resource-example/4/", "baz": "Fri Dec 30 18:22:52 2011", "foo": false, "bar": 123}'
|
||||||
|
|
||||||
|
That should get you started to write a client for your own api.
|
105
docs/index.rst
105
docs/index.rst
|
@ -11,11 +11,12 @@ Introduction
|
||||||
|
|
||||||
Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs.
|
Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs.
|
||||||
|
|
||||||
**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_
|
**Browse example APIs created with Django REST framework:** `The Sandbox <http://rest.ep.io/>`_
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
---------
|
||||||
|
|
||||||
* Automatically provides an awesome Django admin style `browse-able self-documenting API <http://api.django-rest-framework.org>`_.
|
* Automatically provides an awesome Django admin style `browse-able self-documenting API <http://rest.ep.io>`_.
|
||||||
* Clean, simple, views for Resources, using Django's new `class based views <http://docs.djangoproject.com/en/dev/topics/class-based-views/>`_.
|
* Clean, simple, views for Resources, using Django's new `class based views <http://docs.djangoproject.com/en/dev/topics/class-based-views/>`_.
|
||||||
* Support for ModelResources with out-of-the-box default implementations and input validation.
|
* Support for ModelResources with out-of-the-box default implementations and input validation.
|
||||||
* Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise.
|
* Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise.
|
||||||
|
@ -26,10 +27,10 @@ Features:
|
||||||
Resources
|
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>`_.
|
* 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>`_.
|
* 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!)
|
* 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)
|
* 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
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
||||||
You can install Django REST framework using ``pip`` or ``easy_install``::
|
You can install Django REST framework using ``pip`` or ``easy_install``::
|
||||||
|
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
|
|
||||||
Or get the latest development version using mercurial or git::
|
Or get the latest development version using git::
|
||||||
|
|
||||||
hg clone https://bitbucket.org/tomchristie/django-rest-framework
|
|
||||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||||
|
|
||||||
Or you can `download the current release <http://pypi.python.org/pypi/djangorestframework>`_.
|
|
||||||
|
|
||||||
Setup
|
Setup
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -64,6 +64,12 @@ To add Django REST framework to a Django project:
|
||||||
|
|
||||||
* Ensure that the ``djangorestframework`` directory is on your ``PYTHONPATH``.
|
* Ensure that the ``djangorestframework`` directory is on your ``PYTHONPATH``.
|
||||||
* Add ``djangorestframework`` to your ``INSTALLED_APPS``.
|
* 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.
|
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.
|
Using Django REST framework can be as simple as adding a few lines to your urlconf.
|
||||||
|
|
||||||
|
The following example exposes your `MyModel` model through an api. It will provide two views:
|
||||||
|
|
||||||
|
* A view which lists your model instances and simultaniously allows creation of instances
|
||||||
|
from that view.
|
||||||
|
|
||||||
|
* Another view which lets you view, update or delete your model instances individually.
|
||||||
|
|
||||||
``urls.py``::
|
``urls.py``::
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||||
from myapp.models import MyModel
|
from myapp.models import MyModel
|
||||||
|
|
||||||
class MyResource(ModelResource):
|
class MyResource(ModelResource):
|
||||||
model = MyModel
|
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)),
|
url(r'^(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MyResource)),
|
||||||
)
|
)
|
||||||
|
|
||||||
Django REST framework comes with two "getting started" examples.
|
.. include:: howto.rst
|
||||||
|
|
||||||
#. :ref:`views`
|
.. include:: library.rst
|
||||||
#. :ref:`modelviews`
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
|
|
||||||
There are a few real world web API examples included with Django REST framework.
|
|
||||||
|
|
||||||
#. :ref:`objectstore` - Using :class:`views.View` classes for APIs that do not map to models.
|
|
||||||
#. :ref:`codehighlighting` - Using :class:`views.View` classes with forms for input validation.
|
|
||||||
#. :ref:`blogposts` - Using :class:`views.ModelView` classes for APIs that map directly to models.
|
|
||||||
|
|
||||||
All the examples are freely available for testing in the sandbox:
|
|
||||||
|
|
||||||
http://api.django-rest-framework.org
|
|
||||||
|
|
||||||
(The :ref:`sandbox` resource is also documented.)
|
|
||||||
|
|
||||||
|
|
||||||
|
.. include:: examples.rst
|
||||||
How Tos, FAQs & Notes
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:hidden:
|
||||||
|
|
||||||
howto/setup
|
contents
|
||||||
howto/usingcurl
|
|
||||||
howto/alternativeframeworks
|
|
||||||
howto/mixin
|
|
||||||
|
|
||||||
Library Reference
|
.. include:: ../CHANGELOG.rst
|
||||||
-----------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
library/authentication
|
|
||||||
library/compat
|
|
||||||
library/mixins
|
|
||||||
library/parsers
|
|
||||||
library/permissions
|
|
||||||
library/renderers
|
|
||||||
library/resource
|
|
||||||
library/response
|
|
||||||
library/serializer
|
|
||||||
library/status
|
|
||||||
library/views
|
|
||||||
|
|
||||||
Examples Reference
|
|
||||||
------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
examples/views
|
|
||||||
examples/modelviews
|
|
||||||
examples/objectstore
|
|
||||||
examples/pygments
|
|
||||||
examples/blogpost
|
|
||||||
examples/sandbox
|
|
||||||
howto/mixin
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
------------------
|
------------------
|
||||||
|
@ -159,3 +121,8 @@ Indices and tables
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :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
8
docs/library.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Library
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
:glob:
|
||||||
|
|
||||||
|
library/*
|
5
docs/library/reverse.rst
Normal file
5
docs/library/reverse.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
:mod:`reverse`
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: reverse
|
||||||
|
:members:
|
|
@ -1,6 +1,6 @@
|
||||||
# Documentation requires Django & Sphinx, and their dependencies...
|
# Documentation requires Django & Sphinx, and their dependencies...
|
||||||
|
|
||||||
Django==1.2.4
|
Django>=1.2.4
|
||||||
Jinja2==2.5.5
|
Jinja2==2.5.5
|
||||||
Pygments==1.4
|
Pygments==1.4
|
||||||
Sphinx==1.0.7
|
Sphinx==1.0.7
|
||||||
|
|
2
docs/templates/layout.html
vendored
2
docs/templates/layout.html
vendored
|
@ -24,3 +24,5 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block footer %}
|
||||||
|
<div class="footer"> <p> Documentation version {{ version }} {% endblock %}</p></div>
|
||||||
|
|
1
examples/.epio-app
Normal file
1
examples/.epio-app
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rest
|
|
@ -2,6 +2,7 @@ from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def uuid_str():
|
def uuid_str():
|
||||||
return str(uuid.uuid1())
|
return str(uuid.uuid1())
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ RATING_CHOICES = ((0, 'Awful'),
|
||||||
|
|
||||||
MAX_POSTS = 10
|
MAX_POSTS = 10
|
||||||
|
|
||||||
|
|
||||||
class BlogPost(models.Model):
|
class BlogPost(models.Model):
|
||||||
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
||||||
title = models.CharField(max_length=128)
|
title = models.CharField(max_length=128)
|
||||||
|
@ -37,4 +39,3 @@ class Comment(models.Model):
|
||||||
comment = models.TextField()
|
comment = models.TextField()
|
||||||
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
|
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)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
from blogpost.models import BlogPost, Comment
|
from blogpost.models import BlogPost, Comment
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,17 +11,26 @@ class BlogPostResource(ModelResource):
|
||||||
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
|
fields = ('created', 'title', 'slug', 'content', 'url', 'comments')
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
|
|
||||||
|
def url(self, instance):
|
||||||
|
return reverse('blog-post',
|
||||||
|
kwargs={'key': instance.key},
|
||||||
|
request=self.request)
|
||||||
|
|
||||||
def comments(self, instance):
|
def comments(self, instance):
|
||||||
return reverse('comments', kwargs={'blogpost': instance.key})
|
return reverse('comments',
|
||||||
|
kwargs={'blogpost': instance.key},
|
||||||
|
request=self.request)
|
||||||
|
|
||||||
|
|
||||||
class CommentResource(ModelResource):
|
class CommentResource(ModelResource):
|
||||||
"""
|
"""
|
||||||
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
|
A Comment is associated with a given Blog Post and has a *username* and *comment*, and optionally a *rating*.
|
||||||
"""
|
"""
|
||||||
model = Comment
|
model = Comment
|
||||||
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
|
fields = ('username', 'comment', 'created', 'rating', 'url', 'blogpost')
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
|
|
||||||
def blogpost(self, instance):
|
def blogpost(self, instance):
|
||||||
return reverse('blog-post', kwargs={'key': instance.blogpost.key})
|
return reverse('blog-post',
|
||||||
|
kwargs={'key': instance.blogpost.key},
|
||||||
|
request=self.request)
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
"""Test a range of REST API usage of the example application.
|
"""Test a range of REST API usage of the example application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
from djangorestframework.views import InstanceModelView, ListOrCreateModelView
|
from djangorestframework.views import InstanceModelView, ListOrCreateModelView
|
||||||
|
|
||||||
from blogpost import models, urls
|
from blogpost import models, urls
|
||||||
|
@ -15,68 +14,68 @@ from blogpost import models, urls
|
||||||
|
|
||||||
# class AcceptHeaderTests(TestCase):
|
# class AcceptHeaderTests(TestCase):
|
||||||
# """Test correct behaviour of the Accept header as specified by RFC 2616:
|
# """Test correct behaviour of the Accept header as specified by RFC 2616:
|
||||||
#
|
#
|
||||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
||||||
#
|
#
|
||||||
# def assert_accept_mimetype(self, mimetype, expect=None):
|
# def assert_accept_mimetype(self, mimetype, expect=None):
|
||||||
# """Assert that a request with given mimetype in the accept header,
|
# """Assert that a request with given mimetype in the accept header,
|
||||||
# gives a response with the appropriate content-type."""
|
# gives a response with the appropriate content-type."""
|
||||||
# if expect is None:
|
# if expect is None:
|
||||||
# expect = mimetype
|
# expect = mimetype
|
||||||
#
|
#
|
||||||
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
||||||
#
|
#
|
||||||
# self.assertEquals(resp['content-type'], expect)
|
# self.assertEquals(resp['content-type'], expect)
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# def dont_test_accept_json(self):
|
# def dont_test_accept_json(self):
|
||||||
# """Ensure server responds with Content-Type of JSON when requested."""
|
# """Ensure server responds with Content-Type of JSON when requested."""
|
||||||
# self.assert_accept_mimetype('application/json')
|
# self.assert_accept_mimetype('application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_xml(self):
|
# def dont_test_accept_xml(self):
|
||||||
# """Ensure server responds with Content-Type of XML when requested."""
|
# """Ensure server responds with Content-Type of XML when requested."""
|
||||||
# self.assert_accept_mimetype('application/xml')
|
# self.assert_accept_mimetype('application/xml')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_json_when_prefered_to_xml(self):
|
# def dont_test_accept_json_when_prefered_to_xml(self):
|
||||||
# """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
# """Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
||||||
# self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
|
# self.assert_accept_mimetype('application/json;q=0.9, application/xml;q=0.1', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_xml_when_prefered_to_json(self):
|
# def dont_test_accept_xml_when_prefered_to_json(self):
|
||||||
# """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
# """Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
||||||
# self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
|
# self.assert_accept_mimetype('application/json;q=0.1, application/xml;q=0.9', expect='application/xml')
|
||||||
#
|
#
|
||||||
# def dont_test_default_json_prefered(self):
|
# def dont_test_default_json_prefered(self):
|
||||||
# """Ensure server responds with JSON in preference to XML."""
|
# """Ensure server responds with JSON in preference to XML."""
|
||||||
# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
|
# self.assert_accept_mimetype('application/json,application/xml', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_generic_subtype_format(self):
|
# def dont_test_accept_generic_subtype_format(self):
|
||||||
# """Ensure server responds with an appropriate type, when the subtype is left generic."""
|
# """Ensure server responds with an appropriate type, when the subtype is left generic."""
|
||||||
# self.assert_accept_mimetype('text/*', expect='text/html')
|
# self.assert_accept_mimetype('text/*', expect='text/html')
|
||||||
#
|
#
|
||||||
# def dont_test_accept_generic_type_format(self):
|
# def dont_test_accept_generic_type_format(self):
|
||||||
# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
# """Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
||||||
# self.assert_accept_mimetype('*/*', expect='application/json')
|
# self.assert_accept_mimetype('*/*', expect='application/json')
|
||||||
#
|
#
|
||||||
# def dont_test_invalid_accept_header_returns_406(self):
|
# def dont_test_invalid_accept_header_returns_406(self):
|
||||||
# """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
# """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
||||||
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
# resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
||||||
# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
# self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
||||||
# self.assertEquals(resp.status_code, 406)
|
# self.assertEquals(resp.status_code, 406)
|
||||||
#
|
#
|
||||||
# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
|
# def dont_test_prefer_specific_over_generic(self): # This test is broken right now
|
||||||
# """More specific accept types have precedence over less specific types."""
|
# """More specific accept types have precedence over less specific types."""
|
||||||
# self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
|
# self.assert_accept_mimetype('application/xml, */*', expect='application/xml')
|
||||||
# self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
|
# self.assert_accept_mimetype('*/*, application/xml', expect='application/xml')
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# class AllowedMethodsTests(TestCase):
|
# class AllowedMethodsTests(TestCase):
|
||||||
# """Basic tests to check that only allowed operations may be performed on a Resource"""
|
# """Basic tests to check that only allowed operations may be performed on a Resource"""
|
||||||
#
|
#
|
||||||
# def dont_test_reading_a_read_only_resource_is_allowed(self):
|
# def dont_test_reading_a_read_only_resource_is_allowed(self):
|
||||||
# """GET requests on a read only resource should default to a 200 (OK) response"""
|
# """GET requests on a read only resource should default to a 200 (OK) response"""
|
||||||
# resp = self.client.get(reverse(views.RootResource))
|
# resp = self.client.get(reverse(views.RootResource))
|
||||||
# self.assertEquals(resp.status_code, 200)
|
# self.assertEquals(resp.status_code, 200)
|
||||||
#
|
#
|
||||||
# def dont_test_writing_to_read_only_resource_is_not_allowed(self):
|
# def dont_test_writing_to_read_only_resource_is_not_allowed(self):
|
||||||
# """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
# """PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
||||||
# resp = self.client.put(reverse(views.RootResource), {})
|
# resp = self.client.put(reverse(views.RootResource), {})
|
||||||
|
@ -171,7 +170,7 @@ from blogpost import models, urls
|
||||||
|
|
||||||
|
|
||||||
class TestRotation(TestCase):
|
class TestRotation(TestCase):
|
||||||
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
|
"""For the example the maximum amount of Blogposts is capped off at views.MAX_POSTS.
|
||||||
Whenever a new Blogpost is posted the oldest one should be popped."""
|
Whenever a new Blogpost is posted the oldest one should be popped."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -193,7 +192,7 @@ class TestRotation(TestCase):
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
view(request)
|
view(request)
|
||||||
self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
|
self.assertEquals(len(models.BlogPost.objects.all()),models.MAX_POSTS)
|
||||||
|
|
||||||
def test_fifo_behaviour(self):
|
def test_fifo_behaviour(self):
|
||||||
'''It's fine that the Blogposts are capped off at MAX_POSTS. But we want to make sure we see FIFO behaviour.'''
|
'''It's fine that the Blogposts are capped off at MAX_POSTS. But we want to make sure we see FIFO behaviour.'''
|
||||||
for post in range(15):
|
for post in range(15):
|
||||||
|
@ -201,11 +200,11 @@ class TestRotation(TestCase):
|
||||||
request = self.factory.post('/blog-post', data=form_data)
|
request = self.factory.post('/blog-post', data=form_data)
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
view(request)
|
view(request)
|
||||||
request = self.factory.get('/blog-post')
|
request = self.factory.get('/blog-post')
|
||||||
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
view = ListOrCreateModelView.as_view(resource=urls.BlogPostResource)
|
||||||
response = view(request)
|
response = view(request)
|
||||||
response_posts = json.loads(response.content)
|
response_posts = json.loads(response.content)
|
||||||
response_titles = [d['title'] for d in response_posts]
|
response_titles = [d['title'] for d in response_posts]
|
||||||
response_titles.reverse()
|
response_titles.reverse()
|
||||||
self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])
|
self.assertEquals(response_titles, ['%s' % i for i in range(models.MAX_POSTS - 5, models.MAX_POSTS + 5)])
|
||||||
|
|
||||||
|
|
62
examples/epio.ini
Normal file
62
examples/epio.ini
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# This is an example epio.ini file.
|
||||||
|
# We suggest you edit it to fit your application's needs.
|
||||||
|
# Documentation for the options is available at www.ep.io/docs/epioini/
|
||||||
|
|
||||||
|
[wsgi]
|
||||||
|
|
||||||
|
# Location of your requirements file
|
||||||
|
requirements = requirements-epio.txt
|
||||||
|
|
||||||
|
|
||||||
|
[static]
|
||||||
|
|
||||||
|
# Serve the static directory directly as /static
|
||||||
|
/static/admin = ../shortcuts/django-admin-media/
|
||||||
|
|
||||||
|
|
||||||
|
[services]
|
||||||
|
|
||||||
|
# Uncomment to enable the PostgreSQL service.
|
||||||
|
postgres = true
|
||||||
|
|
||||||
|
# Uncomment to enable the Redis service
|
||||||
|
# redis = true
|
||||||
|
|
||||||
|
|
||||||
|
[checkout]
|
||||||
|
|
||||||
|
# By default your code is put in a directory called 'app'.
|
||||||
|
# You can change that here.
|
||||||
|
# directory_name = my_project
|
||||||
|
|
||||||
|
|
||||||
|
[env]
|
||||||
|
|
||||||
|
# Set any additional environment variables here. For example:
|
||||||
|
# IN_PRODUCTION = true
|
||||||
|
|
||||||
|
|
||||||
|
[symlinks]
|
||||||
|
|
||||||
|
# Any symlinks you'd like to add. As an example, link the symlink 'config.py'
|
||||||
|
# to the real file 'configs/epio.py':
|
||||||
|
# config.py = configs/epio.py
|
||||||
|
|
||||||
|
media/ = %(data_directory)s/
|
||||||
|
|
||||||
|
# #### If you're using Django, you'll want to uncomment some or all of these lines ####
|
||||||
|
# [django]
|
||||||
|
# # Path to your project root, relative to this directory.
|
||||||
|
# base = .
|
||||||
|
#
|
||||||
|
# [static]
|
||||||
|
# Serve the admin media
|
||||||
|
# # Django 1.3
|
||||||
|
# /static/admin = ../shortcuts/django-admin-media/
|
||||||
|
# # Django 1.2 and below
|
||||||
|
# /media = ../shortcuts/django-admin-media/
|
||||||
|
#
|
||||||
|
# [env]
|
||||||
|
# # Use a different settings module for ep.io (i.e. with DEBUG=False)
|
||||||
|
# DJANGO_SETTINGS_MODULE = production_settings
|
||||||
|
|
|
@ -1,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.mixins import ResponseMixin
|
||||||
from djangorestframework.renderers import DEFAULT_RENDERERS
|
from djangorestframework.renderers import DEFAULT_RENDERERS
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleView(ResponseMixin, View):
|
class ExampleView(ResponseMixin, View):
|
||||||
"""An example view using Django 1.3's class based views.
|
"""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
|
renderers = DEFAULT_RENDERERS
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
url = reverse('mixin-view', request=request)
|
||||||
response = Response(200, {'description': 'Some example content',
|
response = Response(200, {'description': 'Some example content',
|
||||||
'url': reverse('mixin-view')})
|
'url': url})
|
||||||
return self.render(response)
|
return self.render(response)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', ExampleView.as_view(), name='mixin-view'),
|
url(r'^$', ExampleView.as_view(), name='mixin-view'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django.db import models
|
||||||
|
|
||||||
MAX_INSTANCES = 10
|
MAX_INSTANCES = 10
|
||||||
|
|
||||||
|
|
||||||
class MyModel(models.Model):
|
class MyModel(models.Model):
|
||||||
foo = models.BooleanField()
|
foo = models.BooleanField()
|
||||||
bar = models.IntegerField(help_text='Must be an integer.')
|
bar = models.IntegerField(help_text='Must be an integer.')
|
||||||
|
@ -15,5 +16,3 @@ class MyModel(models.Model):
|
||||||
super(MyModel, self).save(*args, **kwargs)
|
super(MyModel, self).save(*args, **kwargs)
|
||||||
while MyModel.objects.all().count() > MAX_INSTANCES:
|
while MyModel.objects.all().count() > MAX_INSTANCES:
|
||||||
MyModel.objects.all().order_by('-created')[0].delete()
|
MyModel.objects.all().order_by('-created')[0].delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
from djangorestframework.resources import ModelResource
|
from djangorestframework.resources import ModelResource
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
from modelresourceexample.models import MyModel
|
from modelresourceexample.models import MyModel
|
||||||
|
|
||||||
|
|
||||||
class MyModelResource(ModelResource):
|
class MyModelResource(ModelResource):
|
||||||
model = MyModel
|
model = MyModel
|
||||||
fields = ('foo', 'bar', 'baz', 'url')
|
fields = ('foo', 'bar', 'baz', 'url')
|
||||||
ordering = ('created',)
|
ordering = ('created',)
|
||||||
|
|
||||||
|
def url(self, instance):
|
||||||
|
return reverse('model-resource-instance',
|
||||||
|
kwargs={'id': instance.id},
|
||||||
|
request=self.request)
|
||||||
|
|
|
@ -2,7 +2,10 @@ from django.conf.urls.defaults import patterns, url
|
||||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||||
from modelresourceexample.resources import MyModelResource
|
from modelresourceexample.resources import MyModelResource
|
||||||
|
|
||||||
|
my_model_list = ListOrCreateModelView.as_view(resource=MyModelResource)
|
||||||
|
my_model_instance = InstanceModelView.as_view(resource=MyModelResource)
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', ListOrCreateModelView.as_view(resource=MyModelResource), name='model-resource-root'),
|
url(r'^$', my_model_list, name='model-resource-root'),
|
||||||
url(r'^(?P<pk>[0-9]+)/$', InstanceModelView.as_view(resource=MyModelResource)),
|
url(r'^(?P<id>[0-9]+)/$', my_model_instance, name='model-resource-instance'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from objectstore.views import ObjectStoreRoot, StoredObject
|
from objectstore.views import ObjectStoreRoot, StoredObject
|
||||||
|
|
||||||
urlpatterns = patterns('objectstore.views',
|
urlpatterns = patterns('objectstore.views',
|
||||||
url(r'^$', ObjectStoreRoot.as_view(), name='object-store-root'),
|
url(r'^$', ObjectStoreRoot.as_view(), name='object-store-root'),
|
||||||
url(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', StoredObject.as_view(), name='stored-object'),
|
url(r'^(?P<key>[A-Za-z0-9_-]{1,64})/$', StoredObject.as_view(), name='stored-object'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
|
|
||||||
|
from djangorestframework.reverse import reverse
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
from djangorestframework import status
|
from djangorestframework import status
|
||||||
|
@ -13,6 +13,9 @@ import operator
|
||||||
OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
|
OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
|
||||||
MAX_FILES = 10
|
MAX_FILES = 10
|
||||||
|
|
||||||
|
if not os.path.exists(OBJECT_STORE_DIR):
|
||||||
|
os.makedirs(OBJECT_STORE_DIR)
|
||||||
|
|
||||||
|
|
||||||
def remove_oldest_files(dir, max_files):
|
def remove_oldest_files(dir, max_files):
|
||||||
"""
|
"""
|
||||||
|
@ -25,6 +28,20 @@ def remove_oldest_files(dir, max_files):
|
||||||
[os.remove(path) for path in ctime_sorted_paths[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):
|
class ObjectStoreRoot(View):
|
||||||
"""
|
"""
|
||||||
Root of the Object Store API.
|
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)
|
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],
|
ctime_sorted_basenames = [item[0] for item in sorted([(os.path.basename(path), os.path.getctime(path)) for path in filepaths],
|
||||||
key=operator.itemgetter(1), reverse=True)]
|
key=operator.itemgetter(1), reverse=True)]
|
||||||
return [reverse('stored-object', kwargs={'key':key}) for key in ctime_sorted_basenames]
|
return [get_file_url(key, request) for key in ctime_sorted_basenames]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
Create a new stored object, with a unique key.
|
Create a new stored object, with a unique key.
|
||||||
"""
|
"""
|
||||||
key = str(uuid.uuid1())
|
key = str(uuid.uuid1())
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
filename = get_filename(key)
|
||||||
pickle.dump(self.CONTENT, open(pathname, 'wb'))
|
pickle.dump(self.CONTENT, open(filename, 'wb'))
|
||||||
|
|
||||||
remove_oldest_files(OBJECT_STORE_DIR, MAX_FILES)
|
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):
|
class StoredObject(View):
|
||||||
|
@ -56,29 +77,30 @@ class StoredObject(View):
|
||||||
Represents a stored object.
|
Represents a stored object.
|
||||||
The object may be any picklable content.
|
The object may be any picklable content.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, key):
|
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)
|
filename = get_filename(key)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(filename):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status.HTTP_404_NOT_FOUND)
|
||||||
return pickle.load(open(pathname, 'rb'))
|
return pickle.load(open(filename, 'rb'))
|
||||||
|
|
||||||
def put(self, request, key):
|
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)
|
filename = get_filename(key)
|
||||||
pickle.dump(self.CONTENT, open(pathname, 'wb'))
|
pickle.dump(self.CONTENT, open(filename, 'wb'))
|
||||||
return self.CONTENT
|
return self.CONTENT
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request, key):
|
||||||
"""
|
"""
|
||||||
Delete a stored object, by removing it's pickled file.
|
Delete a stored object, by removing it's pickled file.
|
||||||
"""
|
"""
|
||||||
pathname = os.path.join(OBJECT_STORE_DIR, key)
|
filename = get_filename(key)
|
||||||
if not os.path.exists(pathname):
|
if not os.path.exists(filename):
|
||||||
return Response(status.HTTP_404_NOT_FOUND)
|
return Response(status.HTTP_404_NOT_FOUND)
|
||||||
os.remove(pathname)
|
os.remove(filename)
|
||||||
|
|
18
examples/permissionsexample/fixtures/initial_data.json
Normal file
18
examples/permissionsexample/fixtures/initial_data.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
|
|
@ -1 +1 @@
|
||||||
#for fixture loading
|
#for fixture loading
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user