aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml28
-rw-r--r--CONTRIBUTING.md193
-rw-r--r--README.md105
-rw-r--r--docs/404.html201
-rwxr-xr-xdocs/api-guide/authentication.md10
-rw-r--r--docs/api-guide/exceptions.md8
-rw-r--r--docs/api-guide/fields.md8
-rw-r--r--docs/api-guide/filtering.md9
-rwxr-xr-xdocs/api-guide/generic-views.md4
-rw-r--r--docs/api-guide/parsers.md6
-rw-r--r--docs/api-guide/permissions.md5
-rw-r--r--docs/api-guide/relations.md18
-rw-r--r--docs/api-guide/renderers.md27
-rw-r--r--docs/api-guide/routers.md34
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/api-guide/status-codes.md21
-rw-r--r--docs/api-guide/views.md4
-rw-r--r--docs/api-guide/viewsets.md2
-rw-r--r--docs/img/logo.pngbin0 -> 41532 bytes
-rw-r--r--docs/img/travis-status.pngbin0 -> 10023 bytes
-rw-r--r--docs/index.md45
-rw-r--r--docs/template.html29
-rw-r--r--docs/topics/2.2-announcement.md2
-rw-r--r--docs/topics/contributing.md99
-rw-r--r--docs/topics/credits.md10
-rw-r--r--docs/topics/release-notes.md43
-rw-r--r--docs/tutorial/1-serialization.md12
-rw-r--r--docs/tutorial/2-requests-and-responses.md6
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md9
-rw-r--r--docs/tutorial/quickstart.md5
-rwxr-xr-xmkdocs.py22
-rw-r--r--optionals.txt2
-rw-r--r--rest_framework/__init__.py18
-rw-r--r--rest_framework/authentication.py6
-rw-r--r--rest_framework/authtoken/models.py8
-rw-r--r--rest_framework/compat.py29
-rw-r--r--rest_framework/fields.py17
-rw-r--r--rest_framework/filters.py1
-rw-r--r--rest_framework/generics.py12
-rw-r--r--rest_framework/mixins.py10
-rw-r--r--rest_framework/parsers.py4
-rw-r--r--rest_framework/permissions.py22
-rw-r--r--rest_framework/renderers.py9
-rw-r--r--rest_framework/request.py11
-rw-r--r--rest_framework/response.py4
-rw-r--r--rest_framework/routers.py14
-rw-r--r--rest_framework/serializers.py52
-rw-r--r--rest_framework/status.py17
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/templatetags/rest_framework.py5
-rw-r--r--rest_framework/tests/models.py1
-rw-r--r--rest_framework/tests/test_authentication.py26
-rw-r--r--rest_framework/tests/test_fields.py92
-rw-r--r--rest_framework/tests/test_filters.py39
-rw-r--r--rest_framework/tests/test_genericrelations.py29
-rw-r--r--rest_framework/tests/test_generics.py60
-rw-r--r--rest_framework/tests/test_renderers.py72
-rw-r--r--rest_framework/tests/test_serializer.py120
-rw-r--r--rest_framework/tests/test_serializer_nested.py35
-rw-r--r--rest_framework/tests/test_status.py33
-rw-r--r--rest_framework/tests/test_templatetags.py19
-rw-r--r--rest_framework/tests/test_validation.py21
-rw-r--r--rest_framework/urlpatterns.py2
-rw-r--r--rest_framework/utils/encoders.py5
-rw-r--r--rest_framework/viewsets.py2
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py5
-rw-r--r--tox.ini36
68 files changed, 1513 insertions, 296 deletions
diff --git a/.travis.yml b/.travis.yml
index d12479e9..18fe66ab 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,20 +7,20 @@ python:
- "3.3"
env:
- - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
- - DJANGO="django==1.5.1 --use-mirrors"
- - DJANGO="django==1.4.5 --use-mirrors"
- - DJANGO="django==1.3.7 --use-mirrors"
+ - DJANGO="django==1.6"
+ - DJANGO="django==1.5.5"
+ - DJANGO="django==1.4.10"
+ - DJANGO="django==1.3.7"
install:
- pip install $DJANGO
- pip install defusedxml==0.3
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
+ - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
+ - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi"
+ - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
+ - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi"
+ - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
+ - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6; fi"
- export PYTHONPATH=.
script:
@@ -29,11 +29,11 @@ script:
matrix:
exclude:
- python: "3.2"
- env: DJANGO="django==1.4.5 --use-mirrors"
+ env: DJANGO="django==1.4.10"
- python: "3.2"
- env: DJANGO="django==1.3.7 --use-mirrors"
+ env: DJANGO="django==1.3.7"
- python: "3.3"
- env: DJANGO="django==1.4.5 --use-mirrors"
+ env: DJANGO="django==1.4.10"
- python: "3.3"
- env: DJANGO="django==1.3.7 --use-mirrors"
+ env: DJANGO="django==1.3.7"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..a7aa6fc4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,193 @@
+# Contributing to REST framework
+
+> The world can only really be changed one piece at a time. The art is picking that piece.
+>
+> — [Tim Berners-Lee][cite]
+
+There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
+
+## Community
+
+The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
+
+If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
+
+Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
+
+When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
+
+## Code of conduct
+
+Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
+
+Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
+
+The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
+
+# Issues
+
+It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
+
+Some tips on good issue reporting:
+
+* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
+* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
+* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
+* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
+* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
+
+## Triaging issues
+
+Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
+
+* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
+* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
+* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
+* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
+* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
+
+# Development
+
+To start developing on Django REST framework, clone the repo:
+
+ git clone git@github.com:tomchristie/django-rest-framework.git
+
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
+
+## Testing
+
+To run the tests, clone the repository, and then:
+
+ # Setup the virtual environment
+ virtualenv env
+ env/bin/activate
+ pip install -r requirements.txt
+ pip install -r optionals.txt
+
+ # Run the tests
+ rest_framework/runtests/runtests.py
+
+You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
+
+ tox
+
+## Pull requests
+
+It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
+
+It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests.
+
+It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
+
+GitHub's documentation for working on pull requests is [available here][pull-requests].
+
+Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
+
+Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect.
+
+![Travis status][travis-status]
+
+*Above: Travis build notifications*
+
+## Managing compatibility issues
+
+Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
+
+# Documentation
+
+The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
+
+There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
+
+## Building the documentation
+
+To build the documentation, simply run the `mkdocs.py` script.
+
+ ./mkdocs.py
+
+This will build the html output into the `html` directory.
+
+You can build the documentation and open a preview in a browser window by using the `-p` flag.
+
+ ./mkdocs.py -p
+
+## Language style
+
+Documentation should be in American English. The tone of the documentation is very important - try to stick to a simple, plain, objective and well-balanced style where possible.
+
+Some other tips:
+
+* Keep paragraphs reasonably short.
+* Use double spacing after the end of sentences.
+* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
+
+## Markdown style
+
+There are a couple of conventions you should follow when working on the documentation.
+
+##### 1. Headers
+
+Headers should use the hash style. For example:
+
+ ### Some important topic
+
+The underline style should not be used. **Don't do this:**
+
+ Some important topic
+ ====================
+
+##### 2. Links
+
+Links should always use the reference style, with the referenced hyperlinks kept at the end of the document.
+
+ Here is a link to [some other thing][other-thing].
+
+ More text...
+
+ [other-thing]: http://example.com/other/thing
+
+This style helps keep the documentation source consistent and readable.
+
+If you are hyperlinking to another REST framework document, you should use a relative link, and link to the `.md` suffix. For example:
+
+ [authentication]: ../api-guide/authentication.md
+
+Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
+
+##### 3. Notes
+
+If you want to draw attention to a note or warning, use a pair of enclosing lines, like so:
+
+ ---
+
+ **Note:** A useful documentation note.
+
+ ---
+
+# Third party packages
+
+New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
+
+## Getting started
+
+If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
+
+We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
+
+## Linking to your package
+
+Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
+
+[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
+[code-of-conduct]: https://www.djangoproject.com/conduct/
+[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
+[so-filter]: http://stackexchange.com/filters/66475/rest-framework
+[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
+[pep-8]: http://www.python.org/dev/peps/pep-0008/
+[travis-status]: https://raw.github.com/tomchristie/django-rest-framework/master/docs/img/travis-status.png
+[pull-requests]: https://help.github.com/articles/using-pull-requests
+[tox]: http://tox.readthedocs.org/en/latest/
+[markdown]: http://daringfireball.net/projects/markdown/basics
+[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
+[mou]: http://mouapp.com/
+[django-reusable-app]: https://github.com/dabapps/django-reusable-app
diff --git a/README.md b/README.md
index 62883e32..73e4b13f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Django REST framework
-**Awesome web-browseable Web APIs.**
-
[![build-status-image]][travis]
-**Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs].
+**Awesome web-browseable Web APIs.**
+
+**Note**: Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
# Overview
@@ -48,55 +48,58 @@ Let's take a look at a quick example of using REST framework to build a simple m
Here's our project's root `urls.py` module:
- from django.conf.urls.defaults import url, patterns, include
- from django.contrib.auth.models import User, Group
- from rest_framework import viewsets, routers
+```python
+from django.conf.urls.defaults import url, patterns, include
+from django.contrib.auth.models import User, Group
+from rest_framework import viewsets, routers
- # ViewSets define the view behavior.
- class UserViewSet(viewsets.ModelViewSet):
- model = User
+# ViewSets define the view behavior.
+class UserViewSet(viewsets.ModelViewSet):
+ model = User
- class GroupViewSet(viewsets.ModelViewSet):
- model = Group
+class GroupViewSet(viewsets.ModelViewSet):
+ model = Group
- # Routers provide an easy way of automatically determining the URL conf
- router = routers.DefaultRouter()
- router.register(r'users', UserViewSet)
- router.register(r'groups', GroupViewSet)
+# Routers provide an easy way of automatically determining the URL conf
+router = routers.DefaultRouter()
+router.register(r'users', UserViewSet)
+router.register(r'groups', GroupViewSet)
- # Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
- url(r'^', include(router.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+# Wire up our API using automatic URL routing.
+# Additionally, we include login URLs for the browseable API.
+urlpatterns = patterns('',
+ url(r'^', include(router.urls)),
+ url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+)
+```
We'd also like to configure a couple of settings for our API.
Add the following to your `settings.py` module:
- REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
-
- # Use Django's standard `django.contrib.auth` permissions,
- # or allow read-only access for unauthenticated users.
- 'DEFAULT_PERMISSION_CLASSES': [
- 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
- ]
- }
-
+```python
+REST_FRAMEWORK = {
+ # Use hyperlinked styles by default.
+ # Only used if the `serializer_class` attribute is not set on a view.
+ 'DEFAULT_MODEL_SERIALIZER_CLASS':
+ 'rest_framework.serializers.HyperlinkedModelSerializer',
+
+ # Use Django's standard `django.contrib.auth` permissions,
+ # or allow read-only access for unauthenticated users.
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
+ ]
+}
+```
Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
That's it, we're done!
# Documentation & Support
-Full documentation for the project is available at [http://django-rest-framework.org][docs].
+Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC.
@@ -110,7 +113,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
# License
-Copyright (c) 2011-2013, Tom Christie
+Copyright (c) 2011-2014, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -140,21 +143,21 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/
-[index]: http://django-rest-framework.org/
-[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
-[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
-[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
-[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
-[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
-[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
-[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
-[routers]: http://django-rest-framework.org/api-guide/routers.html
-[serializers]: http://django-rest-framework.org/api-guide/serializers.html
-[authentication]: http://django-rest-framework.org/api-guide/authentication.html
-
-[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
+[index]: http://www.django-rest-framework.org/
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication
+[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers
+[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer
+[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views
+[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html
+[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html
+[routers]: http://www.django-rest-framework.org/api-guide/routers.html
+[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html
+[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html
+
+[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
-[image]: http://django-rest-framework.org/img/quickstart.png
+[image]: http://www.django-rest-framework.org/img/quickstart.png
[tox]: http://testrun.org/tox/latest/
@@ -162,7 +165,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[wlonk]: https://twitter.com/wlonk/status/261689665952833536
[laserllama]: https://twitter.com/laserllama/status/328688333750407168
-[docs]: http://django-rest-framework.org/
+[docs]: http://www.django-rest-framework.org/
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
[pyyaml]: http://pypi.python.org/pypi/PyYAML
diff --git a/docs/404.html b/docs/404.html
new file mode 100644
index 00000000..864247e7
--- /dev/null
+++ b/docs/404.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<html lang="en">
+<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>Django REST framework - 404 - Page not found</title>
+ <link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="canonical" href="http://www.django-rest-framework.org/404"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="Django, API, REST, 404 - Page not found">
+ <meta name="author" content="Tom Christie">
+
+ <!-- Le styles -->
+ <link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/default.css" rel="stylesheet">
+
+ <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+ <!--[if lt IE 9]>
+ <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+ <![endif]-->
+
+ <script type="text/javascript">
+
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', 'UA-18852272-2']);
+ _gaq.push(['_trackPageview']);
+
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+
+ </script>
+ </head>
+ <body onload="prettyPrint()" class="404-page">
+
+ <div class="wrapper">
+
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
+ <a class="repo-link btn btn-inverse btn-small disabled" href="#">Next <i class="icon-arrow-right icon-white"></i></a>
+ <a class="repo-link btn btn-inverse btn-small disabled" href="#"><i class="icon-arrow-left icon-white"></i> Previous</a>
+ <a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ <li><a href="http://www.django-rest-framework.org">Home</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/api-guide/requests">Requests</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/responses">Responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/views">Views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/routers">Routers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/parsers">Parsers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/renderers">Renderers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/serializers">Serializers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/authentication">Authentication</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/permissions">Permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/throttling">Throttling</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/filtering">Filtering</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/pagination">Pagination</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/testing">Testing</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/settings">Settings</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/release-notes">Release Notes</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/credits">Credits</a></li>
+ </ul>
+ </li>
+ </ul>
+ <ul class="nav pull-right">
+ <!-- TODO
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Trunk</a></li>
+ <li><a href="#">2.0.0</a></li>
+ </ul>
+ </li>
+ -->
+ </ul>
+ </div><!--/.nav-collapse -->
+ </div>
+ </div>
+ </div>
+
+ <div class="body-content">
+ <div class="container-fluid">
+
+<!-- Search Modal -->
+<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h3 id="myModalLabel">Documentation search</h3>
+ </div>
+ <div class="modal-body">
+ <!-- Custom google search -->
+ <script>
+ (function() {
+ var cx = '015016005043623903336:rxraeohqk6w';
+ var gcse = document.createElement('script');
+ gcse.type = 'text/javascript';
+ gcse.async = true;
+ gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
+ '//www.google.com/cse/cse.js?cx=' + cx;
+ var s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(gcse, s);
+ })();
+ </script>
+ <gcse:search></gcse:search>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
+ </div>
+</div>
+
+ <div class="row-fluid">
+ <div id="main-content" class="span12">
+ <h1 id="404-page-not-found" style="text-align: center">404</h1>
+ <p style="text-align: center"><strong>Page not found</strong></p>
+ <p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
+ </div><!--/span-->
+ </div><!--/row-->
+ </div><!--/.fluid-container-->
+ </div><!--/.body content-->
+
+ <div id="push"></div>
+ </div><!--/.wrapper -->
+
+ <footer class="span12">
+ <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p>
+ </footer>
+
+ <!-- Le javascript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="http://www.django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
+ <script src="http://www.django-rest-framework.org/js/prettify-1.0.js"></script>
+ <script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
+ <script>
+ //$('.side-nav').scrollspy()
+ var shiftWindow = function() { scrollBy(0, -50) };
+ if (location.hash) shiftWindow();
+ window.addEventListener("hashchange", shiftWindow);
+
+ $('.dropdown-menu').on('click touchstart', function(event) {
+ event.stopPropagation();
+ });
+
+ // Dynamically force sidenav to no higher than browser window
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+
+ $(function(){
+ $(window).resize(function(){
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+ });
+ });
+ </script>
+</body></html>
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 7caeac1e..53efc49a 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -162,10 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
+ from django.contrib.auth import get_user_model
+ from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
- @receiver(post_save, sender=User)
+ @receiver(post_save, sender=get_user_model())
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
@@ -265,6 +267,12 @@ This authentication class depends on the optional [django-oauth2-provider][djang
'provider.oauth2',
)
+Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting:
+
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'rest_framework.authentication.OAuth2Authentication',
+ ),
+
You must also include the following in your root `urls.py` module:
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index c46d415e..221df679 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -88,6 +88,14 @@ The **base class** for all exceptions raised inside REST framework.
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.
+For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so:
+
+ from rest_framework.exceptions import APIException
+
+ class ServiceUnavailable(APIException):
+ status_code = 503
+ detail = 'Service temporarily unavailable, try again later.'
+
## ParseError
**Signature:** `ParseError(detail=None)`
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 962c49e2..e05c0306 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -41,7 +41,7 @@ Defaults to `True`.
### `default`
-If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
+If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
@@ -286,7 +286,7 @@ An image representation.
Corresponds to `django.forms.fields.ImageField`.
-Requires the `PIL` package.
+Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained.
Signature and validation is the same as with `FileField`.
@@ -299,9 +299,9 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
# Custom fields
-If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
+If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
-The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
+The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation.
## Examples
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index a0132ffc..0e02a2a7 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -360,6 +360,14 @@ For example, you might need to restrict users to only being able to see objects
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
+# Third party packages
+
+The following third party packages provide additional filter implementations.
+
+## Django REST framework chain
+
+The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
+
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
@@ -368,3 +376,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views,
[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
+[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index b9242724..83c3e45f 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -163,12 +163,14 @@ For example:
return 20
return 100
-**Save hooks**:
+**Save / deletion hooks**:
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes.
* `pre_save(self, obj)` - A hook that is called before saving an object.
* `post_save(self, obj, created=False)` - A hook that is called after saving an object.
+* `pre_delete(self, obj)` - A hook that is called before deleting an object.
+* `post_delete(self, obj)` - A hook that is called after deleting an object.
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 1030fcb6..72a4af64 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -186,9 +186,15 @@ The following third party packages are also available.
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
+## CamelCase JSON
+
+[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
+
[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
[juanriaza]: https://github.com/juanriaza
+[vbabiy]: https://github.com/vbabiy
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 871de84e..6a0f48f4 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
+## REST Condition
+
+The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
+
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md
[throttling]: throttling.md
@@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t
[filtering]: filtering.md
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
+[rest-condition]: https://github.com/caxap/rest_condition
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index b9d96b5e..cc4f5585 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o
For example, the following serializer.
class AlbumSerializer(serializers.ModelSerializer):
- tracks = RelatedField(many=True)
+ tracks = serializers.RelatedField(many=True)
class Meta:
model = Album
@@ -134,7 +134,7 @@ By default this field is read-write, although you can change this behavior using
**Arguments**:
-* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `<modelname>-detail`. **required**.
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
@@ -202,7 +202,7 @@ This field is always read-only.
**Arguments**:
-* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `<model_name>-detail`. **required**.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
@@ -442,7 +442,19 @@ In the 2.4 release, these parts of the API will be removed entirely.
For more details see the [2.2 release announcement][2.2-announcement].
+---
+
+# Third Party Packages
+
+The following third party packages are also available.
+
+## DRF Nested Routers
+
+The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
+
[cite]: http://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
+[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 657377d9..7798827b 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
-**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
+---
+
+**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
+
+The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
+
+---
**.media_type**: `application/javascript`
@@ -167,14 +173,14 @@ The template name is determined by (in order of preference):
An example of a view that uses `TemplateHTMLRenderer`:
- class UserDetail(generics.RetrieveUserAPIView):
+ class UserDetail(generics.RetrieveAPIView):
"""
A view that returns a templated HTML representations of a given user.
"""
queryset = User.objects.all()
renderer_classes = (TemplateHTMLRenderer,)
- def get(self, request, *args, **kwargs)
+ def get(self, request, *args, **kwargs):
self.object = self.get_object()
return Response({'user': self.object}, template_name='user_detail.html')
@@ -409,12 +415,22 @@ The following third party packages are also available.
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
+## UltraJSON
+
+[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package.
+
+## CamelCase JSON
+
+[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
+
+
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[cors]: http://www.w3.org/TR/cors/
[cors-docs]: ../topics/ajax-csrf-cors.md
+[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
[testing]: testing.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
@@ -424,5 +440,10 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[messagepack]: http://msgpack.org/
[juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu
+[vbabiy]: https://github.com/vbabiy
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
+[ultrajson]: https://github.com/esnme/ultrajson
+[hzy]: https://github.com/hzy
+[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index fb48197e..7efc140a 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo
## Usage
-Here's an example of a simple URL conf, that uses `DefaultRouter`.
+Here's an example of a simple URL conf, that uses `SimpleRouter`.
from rest_framework import routers
@@ -37,6 +37,18 @@ The example above would generate the following URL patterns:
* URL pattern: `^accounts/$` Name: `'account-list'`
* URL pattern: `^accounts/{pk}/$` Name: `'account-detail'`
+---
+
+**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part.
+
+Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have any `.model` or `.queryset` attribute set. If you try to register that viewset you'll see an error like this:
+
+ 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.model' or '.queryset' attribute.
+
+This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name.
+
+---
+
### Extra link and actions
Any methods on the viewset decorated with `@link` or `@action` will also be routed.
@@ -150,4 +162,24 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
+# Third Party Packages
+
+The following third party packages are also available.
+
+## DRF Nested Routers
+
+The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
+
+## wq.db
+
+The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration.
+
+ from wq.db.rest import app
+ from myapp.models import MyModel
+
+ app.router.register_model(MyModel)
+
[cite]: http://guides.rubyonrails.org/routing.html
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
+[wq.db]: http://wq.io/wq.db
+[wq.db-router]: http://wq.io/docs/app.py
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 4c3fb9d3..6fc25f57 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -425,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_
fields = ('url', 'account_name', 'users', 'created')
lookup_field = 'slug'
-Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
+Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md
index 409f659b..64c46434 100644
--- a/docs/api-guide/status-codes.md
+++ b/docs/api-guide/status-codes.md
@@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc
The full set of HTTP status codes included in the `status` module is listed below.
+The module also includes a set of helper functions for testing if a status code is in a given range.
+
+ from rest_framework import status
+ from rest_framework.test import APITestCase
+
+ class ExampleTestCase(APITestCase):
+ def test_url_root(self):
+ url = reverse('index')
+ response = self.client.get(url)
+ self.assertTrue(status.is_success(response.status_code))
+
+
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
and [RFC 6585][rfc6585].
@@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
+## Helper functions
+
+The following helper functions are available for identifying the category of the response code.
+
+ is_informational() # 1xx
+ is_success() # 2xx
+ is_redirect() # 3xx
+ is_client_error() # 4xx
+ is_server_error() # 5xx
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md
index 15581e09..194a7a6b 100644
--- a/docs/api-guide/views.md
+++ b/docs/api-guide/views.md
@@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
-[settings]: api-guide/settings.md
-[throttling]: api-guide/throttling.md
+[settings]: settings.md
+[throttling]: throttling.md
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 1062cb32..4fdd9364 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
#### Example
-Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
+Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example:
class AccountViewSet(viewsets.ModelViewSet):
"""
diff --git a/docs/img/logo.png b/docs/img/logo.png
new file mode 100644
index 00000000..73de34f4
--- /dev/null
+++ b/docs/img/logo.png
Binary files differ
diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png
new file mode 100644
index 00000000..fec98cf9
--- /dev/null
+++ b/docs/img/travis-status.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index bb2129f6..b5c3339a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,15 +1,21 @@
-<p class="badges">
+<p class="badges" height=20px>
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
-<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
+<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
</p>
-# Django REST framework
+---
+
+<p>
+<img src="img/logo.png" width=600px style="display: block; margin: 0 auto 0 auto">
+</p>
-**Awesome web-browsable Web APIs.**
+<!--
+# Django REST framework
+-->
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
@@ -20,13 +26,16 @@ Some reasons you might want to use REST framework:
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
+* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite].
-There is a live example API for testing purposes, [available here][sandbox].
-
-**Below**: *Screenshot from the browsable API*
+---
![Screenshot][image]
+**Above**: *Screenshot from the browsable API*
+
+----
+
## Requirements
REST framework requires the following:
@@ -100,7 +109,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
We're ready to create our API now.
Here's our project's root `urls.py` module:
- from django.conf.urls.defaults import url, patterns, include
+ from django.conf.urls import url, patterns, include
from django.contrib.auth.models import User, Group
from rest_framework import viewsets, routers
@@ -112,7 +121,7 @@ Here's our project's root `urls.py` module:
model = Group
- # Routers provide an easy way of automatically determining the URL conf
+ # Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
@@ -140,6 +149,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [5 - Relationships & hyperlinked APIs][tut-5]
* [6 - Viewsets & routers][tut-6]
+There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox].
+
## API Guide
The API guide is your complete reference manual to all the functionality provided by REST framework.
@@ -177,6 +188,7 @@ General guides to using REST framework.
* [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
+* [Contributing to REST framework][contributing]
* [2.0 Announcement][rest-framework-2-announcement]
* [2.2 Announcement][2.2-announcement]
* [2.3 Announcement][2.3-announcement]
@@ -218,7 +230,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
## License
-Copyright (c) 2011-2013, Tom Christie
+Copyright (c) 2011-2014, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -243,7 +255,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
-[urlobject]: https://github.com/zacharyvoase/urlobject
+[mozilla]: http://www.mozilla.org/en-US/about/
+[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
@@ -255,11 +268,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[index]: .
-[oauth1-section]: api-guide/authentication.html#oauthauthentication
-[oauth2-section]: api-guide/authentication.html#oauth2authentication
-[serializer-section]: api-guide/serializers.html#serializers
-[modelserializer-section]: api-guide/serializers.html#modelserializer
-[functionview-section]: api-guide/views.html#function-based-views
+[oauth1-section]: api-guide/authentication#oauthauthentication
+[oauth2-section]: api-guide/authentication#oauth2authentication
+[serializer-section]: api-guide/serializers#serializers
+[modelserializer-section]: api-guide/serializers#modelserializer
+[functionview-section]: api-guide/views#function-based-views
[sandbox]: http://restframework.herokuapp.com/
[quickstart]: tutorial/quickstart.md
diff --git a/docs/template.html b/docs/template.html
index 749d0afe..a397d067 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -4,6 +4,7 @@
<meta charset="utf-8">
<title>{{ title }}</title>
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="canonical" href="{{ canonical_url }}"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
<meta name="author" content="Tom Christie">
@@ -101,6 +102,7 @@
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
+ <li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
@@ -168,31 +170,12 @@
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
{{ toc }}
<div>
- <hr>
-
-<p><strong>The team behind REST framework are launching a new API service.</strong></p>
-
-<p>If you want to be first in line when we start issuing invitations, please sign up here:</p>
-
-<!-- Begin MailChimp Signup Form -->
-<link href="//cdn-images.mailchimp.com/embedcode/slim-081711.css" rel="stylesheet" type="text/css">
-<style type="text/css">
- #mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
- /* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
- We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
-</style>
-<div id="mc_embed_signup" style="background: rgb(245, 245, 245)">
-<form action="http://dabapps.us1.list-manage1.com/subscribe/post?u=cf73a9994eb5b8d8d461b5dfb&amp;id=cb6af8e8bd" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
-<!-- <label for="mce-EMAIL">Keep me posted!</label>
- --> <input style="width: 90%" type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
- <div class="clear"><input class="btn btn-success" type="submit" value="Yes, keep me posted!" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
-</form>
-</div>
-</style></div>
- </ul>
+{{ ad_block }}
+
+</div>
+</ul>
-<!--End mc_embed_signup-->
</div>
</div>
diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md
index 7d276049..a997c782 100644
--- a/docs/topics/2.2-announcement.md
+++ b/docs/topics/2.2-announcement.md
@@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
-[credits]: http://django-rest-framework.org/topics/credits.html
+[credits]: http://www.django-rest-framework.org/topics/credits
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[marcgibbons]: https://github.com/marcgibbons/
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 123e4a8a..30d292f8 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -6,50 +6,92 @@
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
-# Community
+## Community
-If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users.
+The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
-You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter.
+If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
+## Code of conduct
+
+Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
+
+Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
+
+The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
+
# Issues
-It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
+It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
Some tips on good issue reporting:
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
+* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
+* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
+## Triaging issues
+Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
-* TODO: Triage
+* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
+* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
+* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
+* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
+* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
# Development
+To start developing on Django REST framework, clone the repo:
+
+ git clone git@github.com:tomchristie/django-rest-framework.git
+
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
+
+## Testing
+
+To run the tests, clone the repository, and then:
+
+ # Setup the virtual environment
+ virtualenv env
+ env/bin/activate
+ pip install -r requirements.txt
+ pip install -r optionals.txt
+
+ # Run the tests
+ rest_framework/runtests/runtests.py
-* git clone & PYTHONPATH
-* Pep8
-* Recommend editor that runs pep8
+You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
-### Pull requests
+ tox
-* Make pull requests early
-* Describe branching
+## Pull requests
-### Managing compatibility issues
+It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
-* Describe compat module
+It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests.
-# Testing
+It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
-* Running the tests
-* tox
+GitHub's documentation for working on pull requests is [available here][pull-requests].
+
+Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
+
+Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect.
+
+![Travis status][travis-status]
+
+*Above: Travis build notifications*
+
+## Managing compatibility issues
+
+Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
# Documentation
@@ -77,7 +119,7 @@ Some other tips:
* Keep paragraphs reasonably short.
* Use double spacing after the end of sentences.
-* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'.
+* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
## Markdown style
@@ -118,25 +160,34 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
---
- **Note:** Make sure you do this thing.
+ **Note:** A useful documentation note.
---
# Third party packages
-* Django reusable app
+New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
+
+## Getting started
+
+If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
+
+We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
-# Core committers
+## Linking to your package
-* Still use pull reqs
-* Credits
+Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
-[github]: https://github.com/tomchristie/django-rest-framework
-[django-packages]: https://www.djangopackages.com/grids/g/api/
+[code-of-conduct]: https://www.djangoproject.com/conduct/
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
+[pep-8]: http://www.python.org/dev/peps/pep-0008/
+[travis-status]: ../img/travis-status.png
+[pull-requests]: https://help.github.com/articles/using-pull-requests
+[tox]: http://tox.readthedocs.org/en/latest/
[markdown]: http://daringfireball.net/projects/markdown/basics
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
+[django-reusable-app]: https://github.com/dabapps/django-reusable-app
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index e6de1dbe..d4c00bc4 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -177,6 +177,11 @@ The following people have helped make REST framework great.
* Jacek Bzdak - [jbzdak]
* Alexander Lukanin - [alexanderlukanin13]
* Yamila Moreno - [yamila-moreno]
+* Rob Hudson - [robhudson]
+* Alex Good - [alexjg]
+* Ian Foote - [ian-foote]
+* Chuck Harmston - [chuckharmston]
+* Philip Forget - [philipforget]
Many thanks to everyone who's contributed to the project.
@@ -390,3 +395,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[jbzdak]: https://github.com/jbzdak
[alexanderlukanin13]: https://github.com/alexanderlukanin13
[yamila-moreno]: https://github.com/yamila-moreno
+[robhudson]: https://github.com/robhudson
+[alexjg]: https://github.com/alexjg
+[ian-foote]: https://github.com/ian-foote
+[chuckharmston]: https://github.com/chuckharmston
+[philipforget]: https://github.com/philipforget
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 3df8869a..ca966d20 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -42,8 +42,38 @@ You can determine your currently installed version using `pip freeze`:
### Master
+* JSON renderer now deals with objects that implement a dict-like interface.
+* Fix compatiblity with newer versions of `django-oauth-plus`.
+* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations.
+* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied.
+* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params.
+
+### 2.3.10
+
+**Date**: 6th December 2013
+
+* Add in choices information for ChoiceFields in response to `OPTIONS` requests.
+* Added `pre_delete()` and `post_delete()` method hooks.
+* Added status code category helper functions.
+* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception.
+* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header.
+* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400.
+
+### 2.3.9
+
+**Date**: 15th November 2013
+
+* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`.
+* Include errors in HTML forms in browsable API.
* Added JSON renderer support for numpy scalars.
+* Added `transform_<fieldname>` hooks on serializers for easily modifying field output.
* Added `get_context` hook in `BrowsableAPIRenderer`.
+* Allow serializers to be passed `files` but no `data`.
+* `HTMLFormRenderer` now renders serializers directly to HTML without needing to create an intermediate form object.
+* Added `get_filter_backends` hook.
+* Added queryset aggregates to allowed fields in `OrderingFilter`.
+* Bugfix: Fix decimal suppoprt with `YAMLRenderer`.
+* Bugfix: Fix submission of unicode in browsable API through raw data form.
### 2.3.8
@@ -60,6 +90,19 @@ You can determine your currently installed version using `pip freeze`:
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
+* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*]
+
+---
+
+[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
+
+ class DisablePaginationMixin(object):
+ def get_paginate_by(self, queryset=None):
+ if self.request.QUERY_PARAMS[self.paginate_by_param] == '0':
+ return None
+ return super(DisablePaginationMixin, self).get_paginate_by(queryset)
+
+---
### 2.3.7
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index e1c0009c..2298df59 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -183,9 +183,11 @@ At this point we've translated the model instance into Python native datatypes.
Deserialization is similar. First we parse a stream into Python native datatypes...
- import StringIO
+ # This import will use either `StringIO.StringIO` or `io.BytesIO`
+ # as appropriate, depending on if we're running Python 2 or Python 3.
+ from rest_framework.compat import BytesIO
- stream = StringIO.StringIO(content)
+ stream = BytesIO(content)
data = JSONParser().parse(stream)
...then we restore those native datatypes into to a fully populated object instance.
@@ -261,8 +263,7 @@ The root of our API is going to be a view that supports listing all the existing
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
- else:
- return JSONResponse(serializer.errors, status=400)
+ return JSONResponse(serializer.errors, status=400)
Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
@@ -288,8 +289,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
- else:
- return JSONResponse(serializer.errors, status=400)
+ return JSONResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index 7fa4f3e4..603edd08 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
@@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module.
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index ecf92a7b..bdc6b579 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -67,6 +67,9 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not
We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
+ from django.contrib.auth.models import User
+
+
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
@@ -164,10 +167,10 @@ In the snippets app, create a new file, `permissions.py`
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
- if request.method in permissions.SAFE_METHODS:
+ if request.method in permissions.SAFE_METHODS:
return True
-
- # Write permissions are only allowed to the owner of the snippet
+
+ # Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 80bb9abb..8bf8c7f5 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.
+Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute.
+
+For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications.
+
## URLs
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
@@ -169,6 +173,7 @@ Great, that was easy!
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
+[readme-example-api]: ../#example
[image]: ../img/quickstart.png
[tutorial]: 1-serialization.md
[guide]: ../#api-guide
diff --git a/mkdocs.py b/mkdocs.py
index 13228a0c..f973096f 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -18,8 +18,8 @@ if local:
suffix = '.html'
index = 'index.html'
else:
- base_url = 'http://django-rest-framework.org'
- suffix = '.html'
+ base_url = 'http://www.django-rest-framework.org'
+ suffix = ''
index = ''
@@ -90,7 +90,10 @@ for idx in range(len(path_list)):
path = path_list[idx]
rel = '../' * path.count('/')
- if idx > 0:
+ if idx == 1 and not local:
+ # Link back to '/', not '/index'
+ prev_url_map[path] = '/'
+ elif idx > 0:
prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
if idx < len(path_list) - 1:
@@ -141,8 +144,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
if filename == 'index.md':
main_title = 'Django REST framework - APIs made easy'
else:
- main_title = 'Django REST framework - ' + main_title
+ main_title = main_title + ' - Django REST framework'
+ if relative_path == 'index.md':
+ canonical_url = base_url
+ else:
+ canonical_url = base_url + '/' + relative_path[:-3] + suffix
prev_url = prev_url_map.get(relative_path)
next_url = next_url_map.get(relative_path)
@@ -152,6 +159,13 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = output.replace('{{ title }}', main_title)
output = output.replace('{{ description }}', description)
output = output.replace('{{ page_id }}', filename[:-3])
+ output = output.replace('{{ canonical_url }}', canonical_url)
+
+ if filename =='index.md':
+ output = output.replace('{{ ad_block }}', """<hr><p><strong>The team behind REST framework is launching a new API service.</strong></p>
+<p>If you want to be first in line when we start issuing invitations, please <a href="http://brightapi.com">sign up here</a>.</p>""")
+ else:
+ output = output.replace('{{ ad_block }}', '')
if prev_url:
output = output.replace('{{ prev_url }}', prev_url)
diff --git a/optionals.txt b/optionals.txt
index 4ebfceab..96f4b2f4 100644
--- a/optionals.txt
+++ b/optionals.txt
@@ -2,6 +2,6 @@ markdown>=2.1.0
PyYAML>=3.10
defusedxml>=0.3
django-filter>=0.5.4
-django-oauth-plus>=2.0
+django-oauth-plus>=2.2.1
oauth2>=1.5.211
django-oauth2-provider>=0.2.4
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 2bd2991b..f5483b9d 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,6 +1,20 @@
-__version__ = '2.3.8'
+"""
+______ _____ _____ _____ __ _
+| ___ \ ___/ ___|_ _| / _| | |
+| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
+| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
+| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
+\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
+"""
-VERSION = __version__ # synonym
+__title__ = 'Django REST framework'
+__version__ = '2.3.10'
+__author__ = 'Tom Christie'
+__license__ = 'BSD 2-Clause'
+__copyright__ = 'Copyright 2011-2013 Tom Christie'
+
+# Version synonym
+VERSION = __version__
# Header encoding (see RFC5987)
HTTP_HEADER_ENCODING = 'iso-8859-1'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index cf001a24..e491ce5f 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -9,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, provider_now
+from rest_framework.compat import oauth2_provider, provider_now, check_nonce
from rest_framework.authtoken.models import Token
@@ -281,7 +281,9 @@ class OAuthAuthentication(BaseAuthentication):
"""
Checks nonce of request, and return True if valid.
"""
- return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce'])
+ oauth_nonce = oauth_request['oauth_nonce']
+ oauth_timestamp = oauth_request['oauth_timestamp']
+ return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp)
class OAuth2Authentication(BaseAuthentication):
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 7601f5b7..024f62bf 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,11 +1,17 @@
import uuid
import hmac
from hashlib import sha1
-from rest_framework.compat import AUTH_USER_MODEL
from django.conf import settings
from django.db import models
+# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
+# Note that we don't perform this code in the compat module due to
+# bug report #1297
+# See: https://github.com/tomchristie/django-rest-framework/issues/1297
+AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+
+
class Token(models.Model):
"""
The default authorization token model.
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 581e29fc..b69749fe 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -7,6 +7,7 @@ versions of django/python, and compatibility wrappers around optional packages.
from __future__ import unicode_literals
import django
+import inspect
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
@@ -69,6 +70,13 @@ try:
except ImportError:
import urlparse
+# UserDict moves in Python 3
+try:
+ from UserDict import UserDict
+ from UserDict import DictMixin
+except ImportError:
+ from collections import UserDict
+ from collections import MutableMapping as DictMixin
# Try to import PIL in either of the two ways it can end up installed.
try:
@@ -96,13 +104,6 @@ def get_concrete_model(model_cls):
return model_cls
-# Django 1.5 add support for custom auth user model
-if django.VERSION >= (1, 5):
- AUTH_USER_MODEL = settings.AUTH_USER_MODEL
-else:
- AUTH_USER_MODEL = 'auth.User'
-
-
if django.VERSION >= (1, 5):
from django.views.generic import View
else:
@@ -529,9 +530,23 @@ except ImportError:
try:
import oauth_provider
from oauth_provider.store import store as oauth_provider_store
+
+ # check_nonce's calling signature in django-oauth-plus changes sometime
+ # between versions 2.0 and 2.2.1
+ def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp):
+ check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args
+ if 'timestamp' in check_nonce_args:
+ return oauth_provider_store.check_nonce(
+ request, oauth_request, oauth_nonce, oauth_timestamp
+ )
+ return oauth_provider_store.check_nonce(
+ request, oauth_request, oauth_nonce
+ )
+
except (ImportError, ImproperlyConfigured):
oauth_provider = None
oauth_provider_store = None
+ check_nonce = None
# OAuth 2 support is optional
try:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e23fc001..f1de447c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -428,7 +428,7 @@ class BooleanField(WritableField):
def field_from_native(self, data, files, field_name, into):
# HTML checkboxes do not explicitly represent unchecked as `False`
# we deal with that here...
- if isinstance(data, QueryDict):
+ if isinstance(data, QueryDict) and self.default is None:
self.default = False
return super(BooleanField, self).field_from_native(
@@ -497,6 +497,7 @@ class ChoiceField(WritableField):
}
def __init__(self, choices=(), *args, **kwargs):
+ self.empty = kwargs.pop('empty', '')
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
if not self.required:
@@ -513,6 +514,11 @@ class ChoiceField(WritableField):
choices = property(_get_choices, _set_choices)
+ def metadata(self):
+ data = super(ChoiceField, self).metadata()
+ data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
+ return data
+
def validate(self, value):
"""
Validates that the input is in self.choices.
@@ -537,9 +543,10 @@ class ChoiceField(WritableField):
return False
def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
- return super(ChoiceField, self).from_native(value)
+ value = super(ChoiceField, self).from_native(value)
+ if value == self.empty or value in validators.EMPTY_VALUES:
+ return self.empty
+ return value
class EmailField(CharField):
@@ -964,7 +971,7 @@ class ImageField(FileField):
return None
from rest_framework.compat import Image
- assert Image is not None, 'PIL must be installed for ImageField support'
+ assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.'
# We need to get a file object for PIL. We might have a path or we might
# have to read the data into memory.
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index e287a168..5c6a187c 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -124,6 +124,7 @@ class OrderingFilter(BaseFilterBackend):
def remove_invalid_fields(self, queryset, ordering):
field_names = [field.name for field in queryset.model._meta.fields]
+ field_names += queryset.query.aggregates.keys()
return [term for term in ordering if term.lstrip('-') in field_names]
def filter_queryset(self, request, queryset, view):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7cb80a84..fd411ad3 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -344,6 +344,18 @@ class GenericAPIView(views.APIView):
"""
pass
+ def pre_delete(self, obj):
+ """
+ Placeholder method for calling before deleting an object.
+ """
+ pass
+
+ def post_delete(self, obj):
+ """
+ Placeholder method for calling after saving an object.
+ """
+ pass
+
def metadata(self, request):
"""
Return a dictionary of metadata about the view.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 4606c78b..43950c4b 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
+from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
@@ -127,7 +128,12 @@ class UpdateModelMixin(object):
files=request.FILES, partial=partial)
if serializer.is_valid():
- self.pre_save(serializer.object)
+ try:
+ self.pre_save(serializer.object)
+ except ValidationError as err:
+ # full_clean on model instance may be called in pre_save, so we
+ # have to handle eventual errors.
+ return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
self.object = serializer.save(**save_kwargs)
self.post_save(self.object, created=created)
return Response(serializer.data, status=success_status_code)
@@ -186,5 +192,7 @@ class DestroyModelMixin(object):
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
+ self.pre_delete(obj)
obj.delete()
+ self.post_delete(obj)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 98fc0341..f1b3e38d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -83,7 +83,7 @@ class YAMLParser(BaseParser):
data = stream.read().decode(encoding)
return yaml.safe_load(data)
except (ValueError, yaml.parser.ParserError) as exc:
- raise ParseError('YAML parse error - %s' % six.u(exc))
+ raise ParseError('YAML parse error - %s' % six.text_type(exc))
class FormParser(BaseParser):
@@ -153,7 +153,7 @@ class XMLParser(BaseParser):
try:
tree = etree.parse(stream, parser=parser, forbid_dtd=True)
except (etree.ParseError, ValueError) as exc:
- raise ParseError('XML parse error - %s' % six.u(exc))
+ raise ParseError('XML parse error - %s' % six.text_type(exc))
data = self._xml_convert(tree.getroot())
return data
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index ab6655e7..f24a5123 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -54,9 +54,7 @@ class IsAuthenticated(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_authenticated():
- return True
- return False
+ return request.user and request.user.is_authenticated()
class IsAdminUser(BasePermission):
@@ -65,9 +63,7 @@ class IsAdminUser(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_staff:
- return True
- return False
+ return request.user and request.user.is_staff
class IsAuthenticatedOrReadOnly(BasePermission):
@@ -76,11 +72,9 @@ class IsAuthenticatedOrReadOnly(BasePermission):
"""
def has_permission(self, request, view):
- if (request.method in SAFE_METHODS or
- request.user and
- request.user.is_authenticated()):
- return True
- return False
+ return (request.method in SAFE_METHODS or
+ request.user and
+ request.user.is_authenticated())
class DjangoModelPermissions(BasePermission):
@@ -138,11 +132,9 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls)
- if (request.user and
+ return (request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and
- request.user.has_perms(perms)):
- return True
- return False
+ request.user.has_perms(perms))
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index fe4f43d4..2fdd3337 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -20,6 +20,7 @@ from rest_framework.compat import StringIO
from rest_framework.compat import six
from rest_framework.compat import smart_text
from rest_framework.compat import yaml
+from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
@@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer):
In the absence of the View having an associated form then return None.
"""
if request.method == method:
- data = request.DATA
- files = request.FILES
+ try:
+ data = request.DATA
+ files = request.FILES
+ except ParseError:
+ data = None
+ files = None
else:
data = None
files = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index adf63785..d1f04ec5 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -356,7 +356,16 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
- parsed = parser.parse(stream, media_type, self.parser_context)
+ try:
+ parsed = parser.parse(stream, media_type, self.parser_context)
+ except:
+ # If we get an exception during parsing, fill in empty data and
+ # re-raise. Ensures we don't simply repeat the error when
+ # attempting to render the browsable renderer response, or when
+ # logging the request or similar.
+ self._data = QueryDict('', self._request._encoding)
+ self._files = MultiValueDict()
+ raise
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 5877c8a3..1dc6abcf 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse):
assert charset, 'renderer returned unicode, and did not specify ' \
'a charset value.'
return bytes(ret.encode(charset))
+
+ if not ret:
+ del self['Content-Type']
+
return ret
@property
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 3fee1e49..97b35c10 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -184,18 +184,24 @@ class SimpleRouter(BaseRouter):
bound_methods[method] = action
return bound_methods
- def get_lookup_regex(self, viewset):
+ def get_lookup_regex(self, viewset, lookup_prefix=''):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
+
+ Note that lookup_prefix is not used directly inside REST rest_framework
+ itself, but is required in order to nicely support nested router
+ implementations, such as drf-nested-routers.
+
+ https://github.com/alanjds/drf-nested-routers
"""
if self.trailing_slash:
- base_regex = '(?P<{lookup_field}>[^/]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)'
else:
# Don't consume `.json` style suffixes
- base_regex = '(?P<{lookup_field}>[^/.]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)'
lookup_field = getattr(viewset, 'lookup_field', 'pk')
- return base_regex.format(lookup_field=lookup_field)
+ return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix)
def get_urls(self):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 4210d058..b22ca578 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -6,8 +6,8 @@ form encoded input.
Serialization in REST framework is a two-phase process:
1. Serializers marshal between complex types like model instances, and
-python primatives.
-2. The process of marshalling between python primatives and request and
+python primitives.
+2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
@@ -42,6 +42,7 @@ def pretty_name(name):
class RelationsList(list):
_deleted = []
+
class NestedValidationError(ValidationError):
"""
The default ValidationError behavior is to stringify each item in the list
@@ -56,9 +57,13 @@ class NestedValidationError(ValidationError):
def __init__(self, message):
if isinstance(message, dict):
- self.messages = [message]
+ self._messages = [message]
else:
- self.messages = message
+ self._messages = message
+
+ @property
+ def messages(self):
+ return self._messages
class DictWithMetadata(dict):
@@ -326,7 +331,7 @@ class BaseSerializer(WritableField):
return ret
- def from_native(self, data, files):
+ def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""
@@ -407,11 +412,19 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
- obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
+
+ # If we have a model manager or similar object then we need
+ # to iterate through each instance.
+ if (self.many and
+ not hasattr(obj, '__iter__') and
+ is_simple_callable(getattr(obj, 'all', None))):
+ obj = obj.all()
if self.source == '*':
if value:
- into.update(value)
+ reverted_data = self.restore_fields(value, {})
+ if not self._errors:
+ into.update(reverted_data)
else:
if value in (None, ''):
into[(self.source or field_name)] = None
@@ -606,6 +619,7 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
+ models.NullBooleanField: BooleanField,
models.FileField: FileField,
models.ImageField: ImageField,
}
@@ -699,7 +713,9 @@ class ModelSerializer(Serializer):
is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField)
- if is_m2m and not relation.field.rel.through._meta.auto_created:
+ if (is_m2m and
+ hasattr(relation.field.rel, 'through') and
+ not relation.field.rel.through._meta.auto_created):
has_through_model = True
if nested:
@@ -794,6 +810,8 @@ class ModelSerializer(Serializer):
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
kwargs['choices'] = model_field.flatchoices
+ if model_field.null:
+ kwargs['empty'] = None
return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
@@ -865,18 +883,18 @@ class ModelSerializer(Serializer):
# Reverse fk or one-to-one relations
for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.field.related_query_name()
+ field_name = obj.get_accessor_name()
if field_name in attrs:
related_data[field_name] = attrs.pop(field_name)
# Reverse m2m relations
for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.field.related_query_name()
+ field_name = obj.get_accessor_name()
if field_name in attrs:
m2m_data[field_name] = attrs.pop(field_name)
# Forward m2m relations
- for field in meta.many_to_many:
+ for field in meta.many_to_many + meta.virtual_fields:
if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name)
@@ -889,7 +907,10 @@ class ModelSerializer(Serializer):
# Update an existing instance...
if instance is not None:
for key, val in attrs.items():
- setattr(instance, key, val)
+ try:
+ setattr(instance, key, val)
+ except ValueError:
+ self._errors[key] = self.error_messages['required']
# ...or create a new instance
else:
@@ -933,11 +954,16 @@ class ModelSerializer(Serializer):
del(obj._m2m_data)
if getattr(obj, '_related_data', None):
+ related_fields = dict([
+ (field.get_accessor_name(), field)
+ for field, model
+ in obj._meta.get_all_related_objects_with_model()
+ ])
for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
for related_item in related:
- fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
+ fk_field = related_fields[accessor_name].field.name
setattr(related_item, fk_field, obj)
self.save_object(related_item)
diff --git a/rest_framework/status.py b/rest_framework/status.py
index b9f249f9..76435371 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
from __future__ import unicode_literals
+
+def is_informational(code):
+ return code >= 100 and code <= 199
+
+def is_success(code):
+ return code >= 200 and code <= 299
+
+def is_redirect(code):
+ return code >= 300 and code <= 399
+
+def is_client_error(code):
+ return code >= 400 and code <= 499
+
+def is_server_error(code):
+ return code >= 500 and code <= 599
+
+
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 495163b6..d19d5a2b 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -33,7 +33,7 @@
<div class="navbar-inner">
<div class="container-fluid">
<span href="/">
- {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
+ {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index e9c1cdd5..83c046f9 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import
from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict
+from django.utils.encoding import iri_to_uri
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
@@ -144,7 +145,9 @@ def add_query_param(request, key, val):
"""
Add a query parameter to the current request url, and return the new url.
"""
- return replace_query_param(request.get_full_path(), key, val)
+ iri = request.get_full_path()
+ uri = iri_to_uri(iri)
+ return replace_query_param(uri, key, val)
@register.filter
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 1598ecd9..32a726c0 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel):
class ActionItem(RESTFrameworkModel):
title = models.CharField(max_length=200)
+ started = models.NullBooleanField(default=False)
done = models.BooleanField(default=False)
info = CustomField(default='---', max_length=12)
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
index a44813b6..f072b81b 100644
--- a/rest_framework/tests/test_authentication.py
+++ b/rest_framework/tests/test_authentication.py
@@ -249,7 +249,7 @@ class OAuthTests(TestCase):
def setUp(self):
# these imports are here because oauth is optional and hiding them in try..except block or compat
# could obscure problems if something breaks
- from oauth_provider.models import Consumer, Resource
+ from oauth_provider.models import Consumer, Scope
from oauth_provider.models import Token as OAuthToken
from oauth_provider import consts
@@ -269,8 +269,8 @@ class OAuthTests(TestCase):
self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
name='example', user=self.user, status=self.consts.ACCEPTED)
- self.resource = Resource.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource,
+ self.scope = Scope.objects.create(name="resource name", url="api/")
+ self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope,
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
)
@@ -362,7 +362,8 @@ class OAuthTests(TestCase):
def test_post_form_with_urlencoded_parameters(self):
"""Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth/', params)
+ auth = self._create_authorization_header()
+ response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
@@ -397,10 +398,10 @@ class OAuthTests(TestCase):
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
@unittest.skipUnless(oauth, 'oauth2 not installed')
def test_get_form_with_readonly_resource_passing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
+ """Ensure POSTing with a readonly scope instead of a write scope fails"""
read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
+ read_only_access_token.scope.is_readonly = True
+ read_only_access_token.scope.save()
params = self._create_authorization_url_parameters()
response = self.csrf_client.get('/oauth-with-scope/', params)
self.assertEqual(response.status_code, 200)
@@ -410,8 +411,8 @@ class OAuthTests(TestCase):
def test_post_form_with_readonly_resource_failing_auth(self):
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
+ read_only_access_token.scope.is_readonly = True
+ read_only_access_token.scope.save()
params = self._create_authorization_url_parameters()
response = self.csrf_client.post('/oauth-with-scope/', params)
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
@@ -421,10 +422,11 @@ class OAuthTests(TestCase):
def test_post_form_with_write_resource_passing_auth(self):
"""Ensure POSTing with a write resource succeed"""
read_write_access_token = self.token
- read_write_access_token.resource.is_readonly = False
- read_write_access_token.resource.save()
+ read_write_access_token.scope.is_readonly = False
+ read_write_access_token.scope.save()
params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
+ auth = self._create_authorization_header()
+ response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
index 34fbab9c..5c96bce9 100644
--- a/rest_framework/tests/test_fields.py
+++ b/rest_framework/tests/test_fields.py
@@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer):
model = TimeFieldModel
+SAMPLE_CHOICES = [
+ ('red', 'Red'),
+ ('green', 'Green'),
+ ('blue', 'Blue'),
+]
+
+
+class ChoiceFieldModel(models.Model):
+ choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
+
+
+class ChoiceFieldModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChoiceFieldModel
+
+
+class ChoiceFieldModelWithNull(models.Model):
+ choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
+
+
+class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChoiceFieldModelWithNull
+
+
class BasicFieldTests(TestCase):
def test_auto_now_fields_read_only(self):
"""
@@ -667,34 +692,71 @@ class ChoiceFieldTests(TestCase):
"""
Tests for the ChoiceField options generator
"""
-
- SAMPLE_CHOICES = [
- ('red', 'Red'),
- ('green', 'Green'),
- ('blue', 'Blue'),
- ]
-
def test_choices_required(self):
"""
Make sure proper choices are rendered if field is required
"""
- f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, self.SAMPLE_CHOICES)
+ f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.choices, SAMPLE_CHOICES)
def test_choices_not_required(self):
"""
Make sure proper choices (plus blank) are rendered if the field isn't required
"""
- f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
+ f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
+
+ def test_invalid_choice_model(self):
+ s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
+ self.assertFalse(s.is_valid())
+ self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
+ self.assertEqual(s.data['choice'], '')
+
+ def test_empty_choice_model(self):
+ """
+ Test that the 'empty' value is correctly passed and used depending on
+ the 'null' property on the model field.
+ """
+ s = ChoiceFieldModelSerializer(data={'choice': ''})
+ self.assertTrue(s.is_valid())
+ self.assertEqual(s.data['choice'], '')
+
+ s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
+ self.assertTrue(s.is_valid())
+ self.assertEqual(s.data['choice'], None)
def test_from_native_empty(self):
"""
- Make sure from_native() returns None on empty param.
+ Make sure from_native() returns an empty string on empty param by default.
"""
- f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES)
- result = f.from_native('')
- self.assertEqual(result, None)
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+ self.assertEqual(f.from_native(''), '')
+ self.assertEqual(f.from_native(None), '')
+
+ def test_from_native_empty_override(self):
+ """
+ Make sure you can override from_native() behavior regarding empty values.
+ """
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
+ self.assertEqual(f.from_native(''), None)
+ self.assertEqual(f.from_native(None), None)
+
+ def test_metadata_choices(self):
+ """
+ Make sure proper choices are included in the field's metadata.
+ """
+ choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
+ f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+ self.assertEqual(f.metadata()['choices'], choices)
+
+ def test_metadata_choices_not_required(self):
+ """
+ Make sure proper choices are included in the field's metadata.
+ """
+ choices = [{'value': v, 'display_name': n}
+ for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
+ f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+ self.assertEqual(f.metadata()['choices'], choices)
class EmailFieldTests(TestCase):
diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py
index 379db29d..614f45cc 100644
--- a/rest_framework/tests/test_filters.py
+++ b/rest_framework/tests/test_filters.py
@@ -363,6 +363,12 @@ class OrdringFilterModel(models.Model):
text = models.CharField(max_length=100)
+class OrderingFilterRelatedModel(models.Model):
+ related_object = models.ForeignKey(OrdringFilterModel,
+ related_name="relateds")
+
+
+
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -472,3 +478,36 @@ class OrderingFilterTests(TestCase):
{'id': 1, 'title': 'zyx', 'text': 'abc'},
]
)
+
+ def test_ordering_by_aggregate_field(self):
+ # create some related models to aggregate order by
+ num_objs = [2, 5, 3]
+ for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
+ num_objs):
+ for _ in range(num_relateds):
+ new_related = OrderingFilterRelatedModel(
+ related_object=obj
+ )
+ new_related.save()
+
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ queryset = OrdringFilterModel.objects.all().annotate(
+ models.Count("relateds"))
+
+ view = OrderingListView.as_view()
+ request = factory.get('?ordering=relateds__count')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ ]
+ )
+
+
+
diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py
index c38bfb9f..2d341344 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/rest_framework/tests/test_genericrelations.py
@@ -69,6 +69,35 @@ class TestGenericRelations(TestCase):
}
self.assertEqual(serializer.data, expected)
+ def test_generic_nested_relation(self):
+ """
+ Test saving a GenericRelation field via a nested serializer.
+ """
+
+ class TagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ exclude = ('content_type', 'object_id')
+
+ class BookmarkSerializer(serializers.ModelSerializer):
+ tags = TagSerializer()
+
+ class Meta:
+ model = Bookmark
+ exclude = ('id',)
+
+ data = {
+ 'url': 'https://docs.djangoproject.com/',
+ 'tags': [
+ {'tag': 'contenttypes'},
+ {'tag': 'genericrelations'},
+ ]
+ }
+ serializer = BookmarkSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ self.assertEqual(serializer.object.tags.count(), 2)
+
def test_generic_fk(self):
"""
Test a relationship that spans a GenericForeignKey field.
diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py
index 79cd99ac..996bd5b0 100644
--- a/rest_framework/tests/test_generics.py
+++ b/rest_framework/tests/test_generics.py
@@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
model = BasicModel
+ def get_queryset(self):
+ queryset = super(InstanceView, self).get_queryset()
+ return queryset.exclude(text='filtered out')
+
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field() # read only
@@ -160,10 +164,10 @@ class TestInstanceView(TestCase):
"""
Create 3 BasicModel intances.
"""
- items = ['foo', 'bar', 'baz']
+ items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
BasicModel(text=item).save()
- self.objects = BasicModel.objects
+ self.objects = BasicModel.objects.exclude(text='filtered out')
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
@@ -352,6 +356,17 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1)
self.assertEqual(updated.text, 'foobar')
+ def test_put_to_filtered_out_instance(self):
+ """
+ PUT requests to an URL of instance which is filtered out should not be
+ able to create new objects.
+ """
+ data = {'text': 'foo'}
+ filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
+ request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
+ response = self.view(request, pk=filtered_out_pk).render()
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
def test_put_as_create_on_id_based_url(self):
"""
PUT requests to RetrieveUpdateDestroyAPIView should create an object
@@ -508,6 +523,25 @@ class ExclusiveFilterBackend(object):
return queryset.filter(text='other')
+class TwoFieldModel(models.Model):
+ field_a = models.CharField(max_length=100)
+ field_b = models.CharField(max_length=100)
+
+
+class DynamicSerializerView(generics.ListCreateAPIView):
+ model = TwoFieldModel
+ renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
+
+ def get_serializer_class(self):
+ if self.request.method == 'POST':
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ fields = ('field_b',)
+ return DynamicSerializer
+ return super(DynamicSerializerView, self).get_serializer_class()
+
+
class TestFilterBackendAppliedToViews(TestCase):
def setUp(self):
@@ -564,28 +598,6 @@ class TestFilterBackendAppliedToViews(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
-
-class TwoFieldModel(models.Model):
- field_a = models.CharField(max_length=100)
- field_b = models.CharField(max_length=100)
-
-
-class DynamicSerializerView(generics.ListCreateAPIView):
- model = TwoFieldModel
- renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
-
- def get_serializer_class(self):
- if self.request.method == 'POST':
- class DynamicSerializer(serializers.ModelSerializer):
- class Meta:
- model = TwoFieldModel
- fields = ('field_b',)
- return DynamicSerializer
- return super(DynamicSerializerView, self).get_serializer_class()
-
-
-class TestFilterBackendAppliedToViews(TestCase):
-
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py
index 76299a89..2ae8ae18 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/rest_framework/tests/test_renderers.py
@@ -15,7 +15,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
+from collections import MutableMapping
import datetime
+import json
import pickle
import re
@@ -64,11 +66,23 @@ class MockView(APIView):
class MockGETView(APIView):
-
def get(self, request, **kwargs):
return Response({'foo': ['bar', 'baz']})
+
+class MockPOSTView(APIView):
+ def post(self, request, **kwargs):
+ return Response({'foo': request.DATA})
+
+
+class EmptyGETView(APIView):
+ renderer_classes = (JSONRenderer,)
+
+ def get(self, request, **kwargs):
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -88,8 +102,10 @@ urlpatterns = patterns('',
url(r'^cache$', MockGETView.as_view()),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
+ url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
+ url(r'^empty$', EmptyGETView.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -219,6 +235,22 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
+ def test_parse_error_renderers_browsable_api(self):
+ """Invalid data should still render the browsable API correctly."""
+ resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
+ self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_204_no_content_responses_have_no_content_type_set(self):
+ """
+ Regression test for #1196
+
+ https://github.com/tomchristie/django-rest-framework/issues/1196
+ """
+ resp = self.client.get('/empty')
+ self.assertEqual(resp.get('Content-Type', None), None)
+ self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
+
_flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -244,6 +276,44 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
+ def test_render_dict_abc_obj(self):
+ class Dict(MutableMapping):
+ def __init__(self):
+ self._dict = dict()
+ def __getitem__(self, key):
+ return self._dict.__getitem__(key)
+ def __setitem__(self, key, value):
+ return self._dict.__setitem__(key, value)
+ def __delitem__(self, key):
+ return self._dict.__delitem__(key)
+ def __iter__(self):
+ return self._dict.__iter__()
+ def __len__(self):
+ return self._dict.__len__()
+ def keys(self):
+ return self._dict.keys()
+
+ x = Dict()
+ x['key'] = 'string value'
+ x[2] = 3
+ ret = JSONRenderer().render(x)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, {'key': 'string value', '2': 3})
+
+ def test_render_obj_with_getitem(self):
+ class DictLike(object):
+ def __init__(self):
+ self._dict = {}
+ def set(self, value):
+ self._dict = dict(value)
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ x = DictLike()
+ x.set({'a': 1, 'b': 'string'})
+ with self.assertRaises(TypeError):
+ JSONRenderer().render(x)
+
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
index 1f85a474..75d6e785 100644
--- a/rest_framework/tests/test_serializer.py
+++ b/rest_framework/tests/test_serializer.py
@@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
model = Person
+class NestedSerializerWithRenamedField(serializers.Serializer):
+ renamed_info = serializers.Field(source='info')
+
+
+class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer):
+ nested = NestedSerializerWithRenamedField(source='*')
+
+ class Meta:
+ model = Person
+
+
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
"""
Testing for #652.
@@ -456,6 +467,20 @@ class ValidationTests(TestCase):
)
self.assertEqual(serializer.is_valid(), True)
+ def test_writable_star_source_with_inner_source_fields(self):
+ """
+ Tests that a serializer with source="*" correctly expands the
+ it's fields into the outer serializer even if they have their
+ own 'source' parameters.
+ """
+
+ serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={
+ 'name': 'marko',
+ 'nested': {'renamed_info': 'hi'}},
+ )
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.errors, {})
+
class CustomValidationTests(TestCase):
class CommentSerializerWithFieldValidator(CommentSerializer):
@@ -558,6 +583,29 @@ class ModelValidationTests(TestCase):
self.assertFalse(second_serializer.is_valid())
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
+ def test_foreign_key_is_null_with_partial(self):
+ """
+ Test ModelSerializer validation with partial=True
+
+ Specifically test that a null foreign key does not pass validation
+ """
+ album = Album(title='test')
+ album.save()
+
+ class PhotoSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Photo
+
+ photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
+ self.assertTrue(photo_serializer.is_valid())
+ photo = photo_serializer.save()
+
+ # Updating only the album (foreign key)
+ photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
+ self.assertFalse(photo_serializer.is_valid())
+ self.assertTrue('album' in photo_serializer.errors)
+ self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required'])
+
def test_foreign_key_with_partial(self):
"""
Test ModelSerializer validation with partial=True
@@ -1720,3 +1768,75 @@ class TestSerializerTransformMethods(TestCase):
'b_renamed': None,
}
)
+
+
+class DefaultTrueBooleanModel(models.Model):
+ cat = models.BooleanField(default=True)
+ dog = models.BooleanField(default=False)
+
+
+class SerializerDefaultTrueBoolean(TestCase):
+
+ def setUp(self):
+ super(SerializerDefaultTrueBoolean, self).setUp()
+
+ class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DefaultTrueBooleanModel
+ fields = ('cat', 'dog')
+
+ self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
+
+ def test_enabled_as_false(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False,
+ 'dog': False})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
+
+ def test_enabled_as_true(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': True,
+ 'dog': True})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], True)
+ self.assertEqual(serializer.data['dog'], True)
+
+ def test_enabled_partial(self):
+ serializer = self.default_true_boolean_serializer(data={'cat': False},
+ partial=True)
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.data['cat'], False)
+ self.assertEqual(serializer.data['dog'], False)
+
+
+class BoolenFieldTypeTest(TestCase):
+ '''
+ Ensure the various Boolean based model fields are rendered as the proper
+ field type
+
+ '''
+
+ def setUp(self):
+ '''
+ Setup an ActionItemSerializer for BooleanTesting
+ '''
+ data = {
+ 'title': 'b' * 201,
+ }
+ self.serializer = ActionItemSerializer(data=data)
+
+ def test_booleanfield_type(self):
+ '''
+ Test that BooleanField is infered from models.BooleanField
+ '''
+ bfield = self.serializer.get_fields()['done']
+ self.assertEqual(type(bfield), fields.BooleanField)
+
+ def test_nullbooleanfield_type(self):
+ '''
+ Test that BooleanField is infered from models.NullBooleanField
+
+ https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8
+ '''
+ bfield = self.serializer.get_fields()['started']
+ self.assertEqual(type(bfield), fields.BooleanField)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
index 029f8bff..7114a060 100644
--- a/rest_framework/tests/test_serializer_nested.py
+++ b/rest_framework/tests/test_serializer_nested.py
@@ -6,6 +6,7 @@ Doesn't cover model serializers.
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import serializers
+from . import models
class WritableNestedSerializerBasicTests(TestCase):
@@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
serializer = self.AlbumSerializer(instance=original, data=data)
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(serializer.object, expected)
+
+
+class NestedModelSerializerUpdateTests(TestCase):
+ def test_second_nested_level(self):
+ john = models.Person.objects.create(name="john")
+
+ post = john.blogpost_set.create(title="Test blog post")
+ post.blogpostcomment_set.create(text="I hate this blog post")
+ post.blogpostcomment_set.create(text="I love this blog post")
+
+ class BlogPostCommentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.BlogPostComment
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
+ class Meta:
+ model = models.BlogPost
+ fields = ('id', 'title', 'comments')
+
+ class PersonSerializer(serializers.ModelSerializer):
+ posts = BlogPostSerializer(many=True, source='blogpost_set')
+ class Meta:
+ model = models.Person
+ fields = ('id', 'name', 'age', 'posts')
+
+ serialize = PersonSerializer(instance=john)
+ deserialize = PersonSerializer(data=serialize.data, instance=john)
+ self.assertTrue(deserialize.is_valid())
+
+ result = deserialize.object
+ result.save()
+ self.assertEqual(result.id, john.id)
+
diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py
new file mode 100644
index 00000000..7b1bdae3
--- /dev/null
+++ b/rest_framework/tests/test_status.py
@@ -0,0 +1,33 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.status import (
+ is_informational, is_success, is_redirect, is_client_error, is_server_error
+)
+
+
+class TestStatus(TestCase):
+ def test_status_categories(self):
+ self.assertFalse(is_informational(99))
+ self.assertTrue(is_informational(100))
+ self.assertTrue(is_informational(199))
+ self.assertFalse(is_informational(200))
+
+ self.assertFalse(is_success(199))
+ self.assertTrue(is_success(200))
+ self.assertTrue(is_success(299))
+ self.assertFalse(is_success(300))
+
+ self.assertFalse(is_redirect(299))
+ self.assertTrue(is_redirect(300))
+ self.assertTrue(is_redirect(399))
+ self.assertFalse(is_redirect(400))
+
+ self.assertFalse(is_client_error(399))
+ self.assertTrue(is_client_error(400))
+ self.assertTrue(is_client_error(499))
+ self.assertFalse(is_client_error(500))
+
+ self.assertFalse(is_server_error(499))
+ self.assertTrue(is_server_error(500))
+ self.assertTrue(is_server_error(599))
+ self.assertFalse(is_server_error(600)) \ No newline at end of file
diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py
new file mode 100644
index 00000000..609a9e08
--- /dev/null
+++ b/rest_framework/tests/test_templatetags.py
@@ -0,0 +1,19 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.test import APIRequestFactory
+from rest_framework.templatetags.rest_framework import add_query_param
+
+factory = APIRequestFactory()
+
+
+class TemplateTagTests(TestCase):
+
+ def test_add_query_param_with_non_latin_charactor(self):
+ # Ensure we don't double-escape non-latin characters
+ # that are present in the querystring.
+ # See #1314.
+ request = factory.get("/", {'q': '查询'})
+ json_url = add_query_param(request, "format", "json")
+ self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
+ self.assertIn("format=json", json_url)
diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py
index ebfdff9c..124c874d 100644
--- a/rest_framework/tests/test_validation.py
+++ b/rest_framework/tests/test_validation.py
@@ -47,12 +47,18 @@ class ShouldValidateModel(models.Model):
class ShouldValidateModelSerializer(serializers.ModelSerializer):
renamed = serializers.CharField(source='should_validate_field', required=False)
+ def validate_renamed(self, attrs, source):
+ value = attrs[source]
+ if len(value) < 3:
+ raise serializers.ValidationError('Minimum 3 characters.')
+ return attrs
+
class Meta:
model = ShouldValidateModel
fields = ('renamed',)
-class TestPreSaveValidationExclusions(TestCase):
+class TestPreSaveValidationExclusionsSerializer(TestCase):
def test_renamed_fields_are_model_validated(self):
"""
Ensure fields with 'source' applied do get still get model validation.
@@ -61,6 +67,19 @@ class TestPreSaveValidationExclusions(TestCase):
# does not have `blank=True`, so this serializer should not validate.
serializer = ShouldValidateModelSerializer(data={'renamed': ''})
self.assertEqual(serializer.is_valid(), False)
+ self.assertIn('renamed', serializer.errors)
+ self.assertNotIn('should_validate_field', serializer.errors)
+
+
+class TestCustomValidationMethods(TestCase):
+ def test_custom_validation_method_is_executed(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'})
+ self.assertFalse(serializer.is_valid())
+ self.assertIn('renamed', serializer.errors)
+
+ def test_custom_validation_method_passing(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'})
+ self.assertTrue(serializer.is_valid())
class ValidationSerializer(serializers.Serializer):
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index d9143bb4..0ff137b0 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
allowed_pattern = '(%s)' % '|'.join(allowed)
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
else:
- suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
+ suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 35ad206b..3ac920c6 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -44,6 +44,11 @@ class JSONEncoder(json.JSONEncoder):
return str(o)
elif hasattr(o, 'tolist'):
return o.tolist()
+ elif hasattr(o, '__getitem__'):
+ try:
+ return dict(o)
+ except:
+ pass
elif hasattr(o, '__iter__'):
return [i for i in o]
return super(JSONEncoder, self).default(o)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index d91323f2..7eb29f99 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views.
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Typically, rather than instantiate views from viewsets directly, you'll
-regsiter the viewset with a router and let the URL conf be determined
+register the viewset with a router and let the URL conf be determined
automatically.
router = DefaultRouter()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..5e409001
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index adf083cb..78cdb628 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ def get_version(package):
Return package version as listed in `__version__` in `init.py`.
"""
init_py = open(os.path.join(package, '__init__.py')).read()
- return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
+ return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
def get_packages(package):
@@ -45,6 +45,7 @@ version = get_version('rest_framework')
if sys.argv[-1] == 'publish':
os.system("python setup.py sdist upload")
+ os.system("python setup.py bdist_wheel upload")
print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")
@@ -54,7 +55,7 @@ if sys.argv[-1] == 'publish':
setup(
name='djangorestframework',
version=version,
- url='http://django-rest-framework.org',
+ url='http://www.django-rest-framework.org',
license='BSD',
description='Web APIs for Django, made easy.',
author='Tom Christie',
diff --git a/tox.ini b/tox.ini
index 6e3b8e0a..77766d20 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,84 +7,84 @@ commands = {envpython} rest_framework/runtests/runtests.py
[testenv:py3.3-django1.6]
basepython = python3.3
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
+deps = Django==1.6
django-filter==0.6a1
defusedxml==0.3
[testenv:py3.2-django1.6]
basepython = python3.2
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
+deps = Django==1.6
django-filter==0.6a1
defusedxml==0.3
[testenv:py2.7-django1.6]
basepython = python2.7
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
+deps = Django==1.6
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.1.1
[testenv:py2.6-django1.6]
basepython = python2.6
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
+deps = Django==1.6
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.1.1
[testenv:py3.3-django1.5]
basepython = python3.3
-deps = django==1.5
+deps = django==1.5.5
django-filter==0.6a1
defusedxml==0.3
[testenv:py3.2-django1.5]
basepython = python3.2
-deps = django==1.5
+deps = django==1.5.5
django-filter==0.6a1
defusedxml==0.3
[testenv:py2.7-django1.5]
basepython = python2.7
-deps = django==1.5
+deps = django==1.5.5
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1
[testenv:py2.6-django1.5]
basepython = python2.6
-deps = django==1.5
+deps = django==1.5.5
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1
[testenv:py2.7-django1.4]
basepython = python2.7
-deps = django==1.4.3
+deps = django==1.4.10
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1
[testenv:py2.6-django1.4]
basepython = python2.6
-deps = django==1.4.3
+deps = django==1.4.10
django-filter==0.6a1
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1
@@ -94,7 +94,7 @@ basepython = python2.7
deps = django==1.3.5
django-filter==0.5.4
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1
@@ -104,7 +104,7 @@ basepython = python2.6
deps = django==1.3.5
django-filter==0.5.4
defusedxml==0.3
- django-oauth-plus==2.0
+ django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.1.1