diff -Nru djangorestframework-2.3.7/.travis.yml djangorestframework-2.3.12/.travis.yml --- djangorestframework-2.3.7/.travis.yml 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/.travis.yml 2014-01-15 14:27:41.000000000 +0000 @@ -7,19 +7,20 @@ - "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 [[ ${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: @@ -28,11 +29,11 @@ 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 -Nru djangorestframework-2.3.7/CONTRIBUTING.md djangorestframework-2.3.12/CONTRIBUTING.md --- djangorestframework-2.3.7/CONTRIBUTING.md 1970-01-01 00:00:00.000000000 +0000 +++ djangorestframework-2.3.12/CONTRIBUTING.md 2014-01-15 14:27:41.000000000 +0000 @@ -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 -Nru djangorestframework-2.3.7/README.md djangorestframework-2.3.12/README.md --- djangorestframework-2.3.7/README.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/README.md 2014-01-15 14:27:41.000000000 +0000 @@ -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 @@ 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 - - # ViewSets define the view behavior. - class UserViewSet(viewsets.ModelViewSet): - model = User +```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 - 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 @@ # 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 @@ [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 +[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://django-rest-framework.org/topics/rest-framework-2-announcement.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 @@ [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 -Nru djangorestframework-2.3.7/debian/changelog djangorestframework-2.3.12/debian/changelog --- djangorestframework-2.3.7/debian/changelog 2013-09-27 09:44:04.000000000 +0000 +++ djangorestframework-2.3.12/debian/changelog 2014-01-23 15:03:56.000000000 +0000 @@ -1,3 +1,19 @@ +djangorestframework (2.3.12-1) unstable; urgency=medium + + * New upstream release + * Fixed FTBFS with django 1.6 (Closes: #729829) + * debian/control + - Update Standards-Version. + - Update X-Python-Version 2.6 to 2.7. + - Append dependency to python-djangorestframework-doc + * python-markdown to Depends + - Append dependency to python-djangorestframework + * Depends, Build-Depends + - python-django-guardian, python-oauth2, python-django-oauth-plus + * quilt to Build-Depends. + + -- Kouhei Maeda Fri, 24 Jan 2014 00:03:39 +0900 + djangorestframework (2.3.7-2) unstable; urgency=low * Append dependency to python-djangorestframework (Closes: #723820) diff -Nru djangorestframework-2.3.7/debian/control djangorestframework-2.3.12/debian/control --- djangorestframework-2.3.7/debian/control 2013-09-27 09:46:40.000000000 +0000 +++ djangorestframework-2.3.12/debian/control 2014-01-23 15:05:18.000000000 +0000 @@ -8,9 +8,13 @@ python-django (>= 1.3), python-markdown (>= 2.1.0), python-defusedxml (>= 0.3), - python-django-filters (>= 0.5.4) -Standards-Version: 3.9.4 -X-Python-Version: >= 2.6 + python-django-filters (>= 0.5.4), + python-django-guardian (>= 1.1.1), + python-oauth2 (>= 1.5.211), + python-django-oauth-plus (>= 2.2.1), + quilt +Standards-Version: 3.9.5 +X-Python-Version: >= 2.7 Homepage: http://django-rest-framework.org Package: python-djangorestframework @@ -24,7 +28,10 @@ Recommends: python-yaml (>= 3.10), python-defusedxml (>= 0.3), python-django-filters (>= 0.5.4), - python-markdown (>= 2.1.0) + python-django-guardian (>= 1.1.1), + python-markdown (>= 2.1.0), + python-oauth2 (>= 1.5.211), + python-django-oauth-plus (>= 2.2.1) Suggests: python-djangorestframework-doc Description: Web APIs for Django, made easy powerful and flexible toolkit that makes it easy to build Web APIs. @@ -39,7 +46,7 @@ Package: python-djangorestframework-doc Section: doc Architecture: all -Depends: ${misc:Depends}, libjs-twitter-bootstrap, libjs-jquery +Depends: ${misc:Depends}, libjs-twitter-bootstrap, libjs-jquery, python-markdown (>= 2.1.0) Description: Web APIs for Django, made easy (documentation) powerful and flexible toolkit that makes it easy to build Web APIs. Some reasons you might want to use REST framework: diff -Nru djangorestframework-2.3.7/debian/patches/Append-SESSION_SERIALIZER.patch djangorestframework-2.3.12/debian/patches/Append-SESSION_SERIALIZER.patch --- djangorestframework-2.3.7/debian/patches/Append-SESSION_SERIALIZER.patch 1970-01-01 00:00:00.000000000 +0000 +++ djangorestframework-2.3.12/debian/patches/Append-SESSION_SERIALIZER.patch 2014-01-14 22:17:00.000000000 +0000 @@ -0,0 +1,10 @@ +Index: djangorestframework-2.3.11/rest_framework/runtests/settings.py +=================================================================== +--- djangorestframework-2.3.11.orig/rest_framework/runtests/settings.py 2014-01-15 07:15:50.000000000 +0900 ++++ djangorestframework-2.3.11/rest_framework/runtests/settings.py 2014-01-15 07:16:58.276654442 +0900 +@@ -167,3 +167,5 @@ + TEST_OUTPUT_VERBOSE = True + TEST_OUTPUT_DESCRIPTIONS = True + TEST_OUTPUT_DIR = 'xmlrunner' ++ ++SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer' diff -Nru djangorestframework-2.3.7/debian/patches/append_uncompressed_js_libraries.patch djangorestframework-2.3.12/debian/patches/append_uncompressed_js_libraries.patch --- djangorestframework-2.3.7/debian/patches/append_uncompressed_js_libraries.patch 2013-09-03 22:50:27.000000000 +0000 +++ djangorestframework-2.3.12/debian/patches/append_uncompressed_js_libraries.patch 2014-01-23 14:58:12.000000000 +0000 @@ -12,10 +12,10 @@ - https://google-code-prettify.googlecode.com/files/prettify-1-Jun-2011.tar.bz2 - docs/js/prettify-1.0-uncompressed.js - rest_framework/static/rest_framework/js/prettify.js -Index: djangorestframework-2.3.7/docs/js/jquery-1.8.1.js +Index: djangorestframework-2.3.12/docs/js/jquery-1.8.1.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/docs/js/jquery-1.8.1.js 2013-07-26 05:17:40.000000000 +0900 ++++ djangorestframework-2.3.12/docs/js/jquery-1.8.1.js 2014-01-23 23:58:10.870866686 +0900 @@ -0,0 +1,9301 @@ +/*! + * jQuery JavaScript Library v1.8.1 @@ -9318,10 +9318,10 @@ +} + +})( window ); -Index: djangorestframework-2.3.7/docs/js/bootstrap-2.1.1.js +Index: djangorestframework-2.3.12/docs/js/bootstrap-2.1.1.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/docs/js/bootstrap-2.1.1.js 2013-09-03 19:18:54.583881877 +0900 ++++ djangorestframework-2.3.12/docs/js/bootstrap-2.1.1.js 2014-01-23 23:58:10.870866686 +0900 @@ -0,0 +1,2027 @@ +/* =================================================== + * bootstrap-transition.js v2.1.1 @@ -11351,10 +11351,10 @@ + +}(window.jQuery); \ No newline at end of file -Index: djangorestframework-2.3.7/rest_framework/static/rest_framework/js/bootstrap.js +Index: djangorestframework-2.3.12/rest_framework/static/rest_framework/js/bootstrap.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/rest_framework/static/rest_framework/js/bootstrap.js 2013-09-03 19:20:44.516214173 +0900 ++++ djangorestframework-2.3.12/rest_framework/static/rest_framework/js/bootstrap.js 2014-01-23 23:58:10.874866547 +0900 @@ -0,0 +1,2027 @@ +/* =================================================== + * bootstrap-transition.js v2.1.1 @@ -13384,10 +13384,10 @@ + +}(window.jQuery); \ No newline at end of file -Index: djangorestframework-2.3.7/rest_framework/static/rest_framework/js/jquery-1.8.1.js +Index: djangorestframework-2.3.12/rest_framework/static/rest_framework/js/jquery-1.8.1.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/rest_framework/static/rest_framework/js/jquery-1.8.1.js 2013-07-26 05:17:40.000000000 +0900 ++++ djangorestframework-2.3.12/rest_framework/static/rest_framework/js/jquery-1.8.1.js 2014-01-23 23:58:10.874866547 +0900 @@ -0,0 +1,9301 @@ +/*! + * jQuery JavaScript Library v1.8.1 @@ -22690,10 +22690,10 @@ +} + +})( window ); -Index: djangorestframework-2.3.7/rest_framework/static/rest_framework/js/prettify.js +Index: djangorestframework-2.3.12/rest_framework/static/rest_framework/js/prettify.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/rest_framework/static/rest_framework/js/prettify.js 2013-09-04 07:10:59.964334103 +0900 ++++ djangorestframework-2.3.12/rest_framework/static/rest_framework/js/prettify.js 2014-01-23 23:58:10.878866409 +0900 @@ -0,0 +1,1477 @@ +// Copyright (C) 2006 Google Inc. +// @@ -24172,10 +24172,10 @@ + 'PR_TYPE': PR_TYPE + }; +})(); -Index: djangorestframework-2.3.7/docs/js/prettify-1.0-uncompressed.js +Index: djangorestframework-2.3.12/docs/js/prettify-1.0-uncompressed.js =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/docs/js/prettify-1.0-uncompressed.js 2013-09-04 07:18:15.296782681 +0900 ++++ djangorestframework-2.3.12/docs/js/prettify-1.0-uncompressed.js 2014-01-23 23:58:10.878866409 +0900 @@ -0,0 +1,1477 @@ +// Copyright (C) 2006 Google Inc. +// @@ -25654,10 +25654,10 @@ + 'PR_TYPE': PR_TYPE + }; +})(); -Index: djangorestframework-2.3.7/rest_framework/static/rest_framework/css/bootstrap.css +Index: djangorestframework-2.3.12/rest_framework/static/rest_framework/css/bootstrap.css =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 -+++ djangorestframework-2.3.7/rest_framework/static/rest_framework/css/bootstrap.css 2013-09-04 07:48:14.330823706 +0900 ++++ djangorestframework-2.3.12/rest_framework/static/rest_framework/css/bootstrap.css 2014-01-23 23:58:10.878866409 +0900 @@ -0,0 +1,5774 @@ +/*! + * Bootstrap v2.1.1 diff -Nru djangorestframework-2.3.7/debian/patches/change_resouce_path_of_html_documents.patch djangorestframework-2.3.12/debian/patches/change_resouce_path_of_html_documents.patch --- djangorestframework-2.3.7/debian/patches/change_resouce_path_of_html_documents.patch 2013-09-01 14:13:55.000000000 +0000 +++ djangorestframework-2.3.12/debian/patches/change_resouce_path_of_html_documents.patch 2014-01-23 14:58:03.000000000 +0000 @@ -1,8 +1,8 @@ * change path of css, js, img, and anchor link in HTML documentation -Index: djangorestframework-2.3.7/mkdocs.py +Index: djangorestframework-2.3.12/mkdocs.py =================================================================== ---- djangorestframework-2.3.7.orig/mkdocs.py 2013-08-16 22:03:20.000000000 +0900 -+++ djangorestframework-2.3.7/mkdocs.py 2013-09-01 23:13:51.947421096 +0900 +--- djangorestframework-2.3.12.orig/mkdocs.py 2014-01-23 23:57:59.075274097 +0900 ++++ djangorestframework-2.3.12/mkdocs.py 2014-01-23 23:57:59.071274235 +0900 @@ -6,7 +6,9 @@ import shutil import sys @@ -24,7 +24,7 @@ suffix = '.html' index = 'index.html' else: -@@ -169,6 +172,10 @@ +@@ -184,6 +187,10 @@ output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output) output = re.sub(r'
:::bash', r'
', output)
          output = re.sub(r'
', r'
', output)
diff -Nru djangorestframework-2.3.7/debian/patches/fixes_layout.patch djangorestframework-2.3.12/debian/patches/fixes_layout.patch
--- djangorestframework-2.3.7/debian/patches/fixes_layout.patch	2013-09-01 14:22:49.000000000 +0000
+++ djangorestframework-2.3.12/debian/patches/fixes_layout.patch	2014-01-23 14:58:06.000000000 +0000
@@ -1,9 +1,9 @@
 * fixes layout of HTML Document for libjs-twitter-bootstrap 2.0.2
-Index: djangorestframework-2.3.7/docs/template.html
+Index: djangorestframework-2.3.12/docs/template.html
 ===================================================================
---- djangorestframework-2.3.7.orig/docs/template.html	2013-08-16 22:03:20.000000000 +0900
-+++ djangorestframework-2.3.7/docs/template.html	2013-09-01 23:20:43.865308030 +0900
-@@ -170,7 +170,7 @@
+--- djangorestframework-2.3.12.orig/docs/template.html	2014-01-23 23:58:04.783077043 +0900
++++ djangorestframework-2.3.12/docs/template.html	2014-01-23 23:58:04.779077181 +0900
+@@ -179,7 +179,7 @@
              
            
  
diff -Nru djangorestframework-2.3.7/debian/patches/remove_google_analytics.patch djangorestframework-2.3.12/debian/patches/remove_google_analytics.patch
--- djangorestframework-2.3.7/debian/patches/remove_google_analytics.patch	2013-09-01 14:25:05.000000000 +0000
+++ djangorestframework-2.3.12/debian/patches/remove_google_analytics.patch	2014-01-23 14:58:09.000000000 +0000
@@ -1,9 +1,9 @@
 * remove JavaScript code of Google Analytics
-Index: djangorestframework-2.3.7/docs/template.html
+Index: djangorestframework-2.3.12/docs/template.html
 ===================================================================
---- djangorestframework-2.3.7.orig/docs/template.html	2013-09-01 23:20:43.865308030 +0900
-+++ djangorestframework-2.3.7/docs/template.html	2013-09-01 23:24:30.796649355 +0900
-@@ -19,19 +19,6 @@
+--- djangorestframework-2.3.12.orig/docs/template.html	2014-01-23 23:58:07.738974929 +0900
++++ djangorestframework-2.3.12/docs/template.html	2014-01-23 23:58:07.734975067 +0900
+@@ -20,19 +20,6 @@
        
      
  
diff -Nru djangorestframework-2.3.7/debian/rules djangorestframework-2.3.12/debian/rules
--- djangorestframework-2.3.7/debian/rules	2013-09-03 09:58:42.000000000 +0000
+++ djangorestframework-2.3.12/debian/rules	2014-01-23 15:01:29.000000000 +0000
@@ -3,7 +3,7 @@
 #export DH_VERBOSE=1
 
 %:
-	dh $@ --with python2
+	dh $@ --with python2,quilt
 
 override_dh_auto_build:
 	python setup.py build
diff -Nru djangorestframework-2.3.7/docs/404.html djangorestframework-2.3.12/docs/404.html
--- djangorestframework-2.3.7/docs/404.html	1970-01-01 00:00:00.000000000 +0000
+++ djangorestframework-2.3.12/docs/404.html	2014-01-15 14:27:41.000000000 +0000
@@ -0,0 +1,201 @@
+
+
+
+    
+    Django REST framework - 404 - Page not found
+    
+    
+    
+    
+    
+
+    
+    
+    
+    
+    
+
+    
+    
+
+    
+  
+  
+
+  
+ + + +
+
+ + + + +
+
+

404

+

Page not found

+

Try the homepage, or search the documentation.

+
+
+
+
+ +
+
+ + + + + + + + + + diff -Nru djangorestframework-2.3.7/docs/api-guide/authentication.md djangorestframework-2.3.12/docs/api-guide/authentication.md --- djangorestframework-2.3.7/docs/api-guide/authentication.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/authentication.md 2014-01-15 14:27:41.000000000 +0000 @@ -46,6 +46,11 @@ You can also set the authentication scheme on a per-view or per-viewset basis, using the `APIView` class based views. + from rest_framework.authentication import SessionAuthentication, BasicAuthentication + from rest_framework.permissions import IsAuthenticated + from rest_framework.response import Response + from rest_framework.views import APIView + class ExampleView(APIView): authentication_classes = (SessionAuthentication, BasicAuthentication) permission_classes = (IsAuthenticated,) @@ -157,11 +162,18 @@ If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. - @receiver(post_save, sender=User) + 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=get_user_model()) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) +Note that you'll want to ensure you place this code snippet in an installed `models.py` module, or some other location that will be imported by Django on startup. + If you've already created some users, you can generate tokens for all existing users like this: from django.contrib.auth.models import User @@ -255,6 +267,12 @@ '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')), @@ -336,6 +354,10 @@ The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'. + from django.contrib.auth.models import User + from rest_framework import authentication + from rest_framework import exceptions + class ExampleAuthentication(authentication.BaseAuthentication): def authenticate(self, request): username = request.META.get('X_USERNAME') @@ -390,4 +412,4 @@ [oauthlib]: https://github.com/idan/oauthlib [doac]: https://github.com/Rediker-Software/doac [rediker]: https://github.com/Rediker-Software -[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/markdown/integrations.md# +[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md# diff -Nru djangorestframework-2.3.7/docs/api-guide/content-negotiation.md djangorestframework-2.3.12/docs/api-guide/content-negotiation.md --- djangorestframework-2.3.7/docs/api-guide/content-negotiation.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/content-negotiation.md 2014-01-15 14:27:41.000000000 +0000 @@ -54,6 +54,8 @@ The following is a custom content negotiation class which ignores the client request when selecting the appropriate parser or renderer. + from rest_framework.negotiation import BaseContentNegotiation + class IgnoreClientContentNegotiation(BaseContentNegotiation): def select_parser(self, request, parsers): """ @@ -77,6 +79,10 @@ You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views. + from myapp.negotiation import IgnoreClientContentNegotiation + from rest_framework.response import Response + from rest_framework.views import APIView + class NoNegotiationView(APIView): """ An example view that does not perform content negotiation. diff -Nru djangorestframework-2.3.7/docs/api-guide/exceptions.md djangorestframework-2.3.12/docs/api-guide/exceptions.md --- djangorestframework-2.3.7/docs/api-guide/exceptions.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/exceptions.md 2014-01-15 14:27:41.000000000 +0000 @@ -28,23 +28,74 @@ Might receive an error response indicating that the `DELETE` method is not allowed on that resource: HTTP/1.1 405 Method Not Allowed - Content-Type: application/json; charset=utf-8 + Content-Type: application/json Content-Length: 42 - + {"detail": "Method 'DELETE' not allowed."} +## Custom exception handling + +You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. + +The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. + +For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: + + HTTP/1.1 405 Method Not Allowed + Content-Type: application/json + Content-Length: 62 + + {"status_code": 405, "detail": "Method 'DELETE' not allowed."} + +In order to alter the style of the response, you could write the following custom exception handler: + + from rest_framework.views import exception_handler + + def custom_exception_handler(exc): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc) + + # Now add the HTTP status code to the response. + if response is not None: + response.data['status_code'] = response.status_code + + return response + +The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler' + } + +If not specified, the `'EXCEPTION_HANDLER'` setting defaults to the standard exception handler provided by REST framework: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' + } + +Note that the exception handler will only be called for responses generated by raised exceptions. It will not be used for any responses returned directly by the view, such as the `HTTP_400_BAD_REQUEST` responses that are returned by the generic views when serializer validation fails. + --- # API Reference ## APIException -**Signature:** `APIException(detail=None)` +**Signature:** `APIException()` 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 -Nru djangorestframework-2.3.7/docs/api-guide/fields.md djangorestframework-2.3.12/docs/api-guide/fields.md --- djangorestframework-2.3.7/docs/api-guide/fields.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/fields.md 2014-01-15 14:27:41.000000000 +0000 @@ -28,7 +28,13 @@ ### `read_only` -Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization. +Set this to `True` to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization. + +Defaults to `False` + +### `write_only` + +Set this to `True` to ensure that the field may be used when updating or creating an instance, but is not included when serializing the representation. Defaults to `False` @@ -41,7 +47,7 @@ ### `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. @@ -78,6 +84,9 @@ For example, using the following model. + from django.db import models + from django.utils.timezone import now + class Account(models.Model): owner = models.ForeignKey('auth.user') name = models.CharField(max_length=100) @@ -85,13 +94,14 @@ payment_expiry = models.DateTimeField() def has_expired(self): - now = datetime.datetime.now() - return now > self.payment_expiry + return now() > self.payment_expiry A serializer definition that looked like this: + from rest_framework import serializers + class AccountSerializer(serializers.HyperlinkedModelSerializer): - expired = Field(source='has_expired') + expired = serializers.Field(source='has_expired') class Meta: fields = ('url', 'owner', 'name', 'expired') @@ -125,12 +135,11 @@ This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: - from rest_framework import serializers from django.contrib.auth.models import User from django.utils.timezone import now + from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): - days_since_joined = serializers.SerializerMethodField('get_days_since_joined') class Meta: @@ -164,13 +173,13 @@ Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation. -**Signature:** `CharField(max_length=200, min_length=None)` +**Signature:** `URLField(max_length=200, min_length=None)` ## SlugField Corresponds to `django.db.models.fields.SlugField`. -**Signature:** `CharField(max_length=50, min_length=None)` +**Signature:** `SlugField(max_length=50, min_length=None)` ## ChoiceField @@ -283,7 +292,7 @@ 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`. @@ -296,9 +305,9 @@ # 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 -Nru djangorestframework-2.3.7/docs/api-guide/filtering.md djangorestframework-2.3.12/docs/api-guide/filtering.md --- djangorestframework-2.3.7/docs/api-guide/filtering.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/filtering.md 2014-01-15 14:27:41.000000000 +0000 @@ -20,6 +20,10 @@ For example: + from myapp.models import Purchase + from myapp.serializers import PurchaseSerializer + from rest_framework import generics + class PurchaseList(generics.ListAPIView) serializer_class = PurchaseSerializer @@ -90,6 +94,11 @@ You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class based views. + from django.contrib.auth.models import User + from myapp.serializers import UserSerializer + from rest_framework import filters + from rest_framework import generics + class UserListView(generics.ListAPIView): queryset = User.objects.all() serializer = UserSerializer @@ -150,9 +159,14 @@ For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example: + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + class ProductFilter(django_filters.FilterSet): - min_price = django_filters.NumberFilter(lookup_type='gte') - max_price = django_filters.NumberFilter(lookup_type='lte') + min_price = django_filters.NumberFilter(name="price", lookup_type='gte') + max_price = django_filters.NumberFilter(name="price", lookup_type='lte') class Meta: model = Product fields = ['category', 'in_stock', 'min_price', 'max_price'] @@ -162,10 +176,49 @@ serializer_class = ProductSerializer filter_class = ProductFilter + Which will allow you to make requests such as: http://example.com/api/products?category=clothing&max_price=10.00 +You can also span relationships using `django-filter`, let's assume that each +product has foreign key to `Manufacturer` model, so we create filter that +filters using `Manufacturer` name. For example: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer__name`] + +This enables us to make queries like: + + http://example.com/api/products?manufacturer__name=foo + +This is nice, but it shows underlying model structure in REST API, which may +be undesired, but you can use: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + + manufacturer = django_filters.CharFilter(name="manufacturer__name") + + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer`] + +And now you can execute: + + http://example.com/api/products?manufacturer=foo + For more details on using filter sets see the [django-filter documentation][django-filter-docs]. --- @@ -181,9 +234,9 @@ ## SearchFilter -The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. +The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. class UserListView(generics.ListAPIView): queryset = User.objects.all() @@ -229,13 +282,37 @@ http://example.com/api/users?ordering=account,username +### Specifying which fields may be ordered against + +It's recommended that you explicitly specify which fields the API should allowing in the ordering filter. You can do this by setting an `ordering_fields` attribute on the view, like so: + + class UserListView(generics.ListAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + filter_backends = (filters.OrderingFilter,) + ordering_fields = ('username', 'email') + +This helps prevent unexpected data leakage, such as allowing users to order against a password hash field or other sensitive data. + +If you *don't* specify an `ordering_fields` attribute on the view, the filter class will default to allowing the user to filter on any readable fields on the serializer specified by the `serializer_class` attribute. + +If you are confident that the queryset being used by the view doesn't contain any sensitive data, you can also explicitly specify that a view should allow ordering on *any* model field or queryset aggregate, by using the special value `'__all__'`. + + class BookingsListView(generics.ListAPIView): + queryset = Booking.objects.all() + serializer_class = BookingSerializer + filter_backends = (filters.OrderingFilter,) + ordering_fields = '__all__' + +### Specifying a default ordering + If an `ordering` attribute is set on the view, this will be used as the default ordering. Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results. class UserListView(generics.ListAPIView): queryset = User.objects.all() - serializer = UserSerializer + serializer_class = UserSerializer filter_backends = (filters.OrderingFilter,) ordering = ('username',) @@ -243,6 +320,49 @@ --- +## DjangoObjectPermissionsFilter + +The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission. + +This filter class must be used with views that provide either a `queryset` or a `model` attribute. + +If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute. + +A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this. + +**permissions.py**: + + class CustomObjectPermissions(permissions.DjangoObjectPermissions): + """ + Similar to `DjangoObjectPermissions`, but adding 'view' permissions. + """ + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + +**views.py**: + + class EventViewSet(viewsets.ModelViewSet): + """ + Viewset that only lists events if user has 'view' permissions, and only + allows operations on individual events if user has appropriate 'view', 'add', + 'change' or 'delete' permissions. + """ + queryset = Event.objects.all() + serializer = EventSerializer + filter_backends = (filters.DjangoObjectPermissionsFilter,) + permission_classes = (myapp.permissions.CustomObjectPermissions,) + +For more information on adding `'view'` permissions for models, see the [relevant section][view-permissions] of the `django-guardian` documentation, and [this blogpost][view-permissions-blogpost]. + +--- + # Custom generic filtering You can also provide your own generic filtering backend, or write an installable app for other developers to use. @@ -264,8 +384,20 @@ 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 +[guardian]: http://pythonhosted.org/django-guardian/ +[view-permissions]: http://pythonhosted.org/django-guardian/userguide/assign.html +[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 -Nru djangorestframework-2.3.7/docs/api-guide/generic-views.md djangorestframework-2.3.12/docs/api-guide/generic-views.md --- djangorestframework-2.3.7/docs/api-guide/generic-views.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/generic-views.md 2014-01-15 14:27:41.000000000 +0000 @@ -17,6 +17,11 @@ Typically when using the generic views, you'll override the view, and set several class attributes. + from django.contrib.auth.models import User + from myapp.serializers import UserSerializer + from rest_framework import generics + from rest_framework.permissions import IsAdminUser + class UserList(generics.ListCreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer @@ -60,15 +65,16 @@ * `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. * `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method. -* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf. +* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value. +* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`. **Shortcuts**: -* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. +* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided. **Pagination**: -The following attibutes are used to control pagination when used with list views. +The following attributes are used to control pagination when used with list views. * `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`. * `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`. @@ -108,13 +114,34 @@ filter = {} for field in self.multiple_lookup_fields: filter[field] = self.kwargs[field] - return get_object_or_404(queryset, **filter) + + obj = get_object_or_404(queryset, **filter) + self.check_object_permissions(self.request, obj) + return obj + +Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. + +#### `get_filter_backends(self)` + +Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute. + +May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria. + +For example: + + def get_filter_backends(self): + if "geo_route" in self.request.QUERY_PARAMS: + return (GeoRouteFilter, CategoryFilter) + elif "geo_point" in self.request.QUERY_PARAMS: + return (GeoPointFilter, CategoryFilter) + + return (CategoryFilter,) #### `get_serializer_class(self)` Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. -May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of uesr. +May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users. For example: @@ -125,7 +152,7 @@ #### `get_paginate_by(self)` -Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set. +Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set. You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response. @@ -136,12 +163,14 @@ 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. @@ -317,7 +346,7 @@ serializer_class = UserSerializer lookup_fields = ('account', 'username') -Using custom mixins is a good option if you have custom behavior that needs to be used +Using custom mixins is a good option if you have custom behavior that needs to be used ## Creating custom base classes @@ -326,7 +355,7 @@ class BaseRetrieveView(MultipleFieldLookupMixin, generics.RetrieveAPIView): pass - + class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView): pass diff -Nru djangorestframework-2.3.7/docs/api-guide/pagination.md djangorestframework-2.3.12/docs/api-guide/pagination.md --- djangorestframework-2.3.7/docs/api-guide/pagination.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/pagination.md 2014-01-15 14:27:41.000000000 +0000 @@ -13,6 +13,7 @@ Let's start by taking a look at an example from the Django documentation. from django.core.paginator import Paginator + objects = ['john', 'paul', 'george', 'ringo'] paginator = Paginator(objects, 2) page = paginator.page(1) @@ -22,6 +23,7 @@ At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results. from rest_framework.pagination import PaginationSerializer + serializer = PaginationSerializer(instance=page) serializer.data # {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']} @@ -83,11 +85,12 @@ The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely. -The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example. +The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example. REST_FRAMEWORK = { - 'PAGINATE_BY': 10, - 'PAGINATE_BY_PARAM': 'page_size' + 'PAGINATE_BY': 10, # Default to 10 + 'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`. + 'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`. } You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view. @@ -97,6 +100,7 @@ serializer_class = ExampleModelSerializer paginate_by = 10 paginate_by_param = 'page_size' + max_paginate_by = 100 Note that using a `paginate_by` value of `None` will turn off pagination for the view. @@ -114,6 +118,9 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this. + from rest_framework import pagination + from rest_framework import serializers + class LinksSerializer(serializers.Serializer): next = pagination.NextPageField(source='*') prev = pagination.PreviousPageField(source='*') @@ -135,7 +142,7 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view: - class PaginatedListView(ListAPIView): + class PaginatedListView(generics.ListAPIView): model = ExampleModel pagination_serializer_class = CustomPaginationSerializer paginate_by = 10 diff -Nru djangorestframework-2.3.7/docs/api-guide/parsers.md djangorestframework-2.3.12/docs/api-guide/parsers.md --- djangorestframework-2.3.7/docs/api-guide/parsers.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/parsers.md 2014-01-15 14:27:41.000000000 +0000 @@ -34,9 +34,13 @@ ) } -You can also set the renderers used for an individual view, or viewset, +You can also set the parsers used for an individual view, or viewset, using the `APIView` class based views. + from rest_framework.parsers import YAMLParser + from rest_framework.response import Response + from rest_framework.views import APIView + class ExampleView(APIView): """ A view that can accept POST requests with YAML content. @@ -182,9 +186,15 @@ [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 -Nru djangorestframework-2.3.7/docs/api-guide/permissions.md djangorestframework-2.3.12/docs/api-guide/permissions.md --- djangorestframework-2.3.7/docs/api-guide/permissions.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/permissions.md 2014-01-15 14:27:41.000000000 +0000 @@ -25,9 +25,17 @@ As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object. If you're writing your own views and want to enforce object level permissions, -you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object. +or if you override the `get_object` method on a generic view, then you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object. + This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions. +For example: + + def get_object(self): + obj = get_object_or_404(self.get_queryset()) + self.check_object_permissions(self.request, obj) + return obj + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. @@ -47,6 +55,10 @@ You can also set the authentication policy on a per-view, or per-viewset basis, using the `APIView` class based views. + from rest_framework.permissions import IsAuthenticated + from rest_framework.responses import Response + from rest_framework.views import APIView + class ExampleView(APIView): permission_classes = (IsAuthenticated,) @@ -108,7 +120,21 @@ ## DjangoModelPermissionsOrAnonReadOnly -Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. +Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. + +## DjangoObjectPermissions + +This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian]. + +When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned. + +* `POST` requests require the user to have the `add` permission on the model instance. +* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance. +* `DELETE` requests require the user to have the `delete` permission on the model instance. + +Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well. + +As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. Note that if you add a custom `view` permission for `GET`, `HEAD` and `OPTIONS` requests, you'll probably also want to consider adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions. ## TokenHasReadWriteScope @@ -157,6 +183,8 @@ The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted. + from rest_framework import permissions + class BlacklistPermission(permissions.BasePermission): """ Global permission check for blacklisted IPs. @@ -198,13 +226,25 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view. +## Composed Permissions + +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 [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions +[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian +[get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user [django-oauth-plus]: http://code.larlet.fr/django-oauth-plus [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [2.2-announcement]: ../topics/2.2-announcement.md [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 -Nru djangorestframework-2.3.7/docs/api-guide/relations.md djangorestframework-2.3.12/docs/api-guide/relations.md --- djangorestframework-2.3.7/docs/api-guide/relations.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/relations.md 2014-01-15 14:27:41.000000000 +0000 @@ -44,7 +44,7 @@ For example, the following serializer. class AlbumSerializer(serializers.ModelSerializer): - tracks = RelatedField(many=True) + tracks = serializers.RelatedField(many=True) class Meta: model = Album @@ -54,7 +54,7 @@ { 'album_name': 'Things We Lost In The Fire', - 'artist': 'Low' + 'artist': 'Low', 'tracks': [ '1: Sunflower', '2: Whitetail', @@ -76,7 +76,7 @@ For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = PrimaryKeyRelatedField(many=True, read_only=True) + tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Album @@ -86,7 +86,7 @@ { 'album_name': 'The Roots', - 'artist': 'Undun' + 'artist': 'Undun', 'tracks': [ 89, 90, @@ -110,8 +110,8 @@ For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = HyperlinkedRelatedField(many=True, read_only=True, - view_name='track-detail') + tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True, + view_name='track-detail') class Meta: model = Album @@ -121,7 +121,7 @@ { 'album_name': 'Graceland', - 'artist': 'Paul Simon' + 'artist': 'Paul Simon', 'tracks': [ 'http://www.example.com/api/tracks/45/', 'http://www.example.com/api/tracks/46/', @@ -134,7 +134,7 @@ **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 `-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`. @@ -148,7 +148,8 @@ For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = SlugRelatedField(many=True, read_only=True, slug_field='title') + tracks = serializers.SlugRelatedField(many=True, read_only=True, + slug_field='title') class Meta: model = Album @@ -158,7 +159,7 @@ { 'album_name': 'Dear John', - 'artist': 'Loney Dear' + 'artist': 'Loney Dear', 'tracks': [ 'Airport Surroundings', 'Everything Turns to You', @@ -183,7 +184,7 @@ This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer: class AlbumSerializer(serializers.HyperlinkedModelSerializer): - track_listing = HyperlinkedIdentityField(view_name='track-list') + track_listing = serializers.HyperlinkedIdentityField(view_name='track-list') class Meta: model = Album @@ -193,7 +194,7 @@ { 'album_name': 'The Eraser', - 'artist': 'Thom Yorke' + 'artist': 'Thom Yorke', 'track_listing': 'http://www.example.com/api/track_list/12/', } @@ -201,7 +202,7 @@ **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 `-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. @@ -213,8 +214,6 @@ If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. -Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style. - ## Example For example, the following serializer: @@ -235,7 +234,7 @@ { 'album_name': 'The Grey Album', - 'artist': 'Danger Mouse' + 'artist': 'Danger Mouse', 'tracks': [ {'order': 1, 'title': 'Public Service Announcement'}, {'order': 2, 'title': 'What More Can I Say'}, @@ -272,7 +271,7 @@ { 'album_name': 'Sometimes I Wish We Were an Eagle', - 'artist': 'Bill Callahan' + 'artist': 'Bill Callahan', 'tracks': [ 'Track 1: Jim Cain (04:39)', 'Track 2: Eid Ma Clack Shaw (04:19)', @@ -422,7 +421,7 @@ def get_object(self, queryset, view_name, view_args, view_kwargs): account = view_kwargs['account'] slug = view_kwargs['slug'] - return queryset.get(account=account, slug=sug) + return queryset.get(account=account, slug=slug) --- @@ -443,7 +442,19 @@ 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 -Nru djangorestframework-2.3.7/docs/api-guide/renderers.md djangorestframework-2.3.12/docs/api-guide/renderers.md --- djangorestframework-2.3.7/docs/api-guide/renderers.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/renderers.md 2014-01-15 14:27:41.000000000 +0000 @@ -30,11 +30,16 @@ You can also set the renderers used for an individual view, or viewset, using the `APIView` class based views. + from django.contrib.auth.models import User + from rest_framework.renderers import JSONRenderer, YAMLRenderer + from rest_framework.response import Response + from rest_framework.views import APIView + class UserCountView(APIView): """ - A view that returns the count of active users, in JSON or JSONp. + A view that returns the count of active users, in JSON or YAML. """ - renderer_classes = (JSONRenderer, JSONPRenderer) + renderer_classes = (JSONRenderer, YAMLRenderer) def get(self, request, format=None): user_count = User.objects.filter(active=True).count() @@ -83,7 +88,7 @@ **.format**: `'.json'` -**.charset**: `utf-8` +**.charset**: `None` ## UnicodeJSONRenderer @@ -105,7 +110,7 @@ **.format**: `'.json'` -**.charset**: `utf-8` +**.charset**: `None` ## JSONPRenderer @@ -113,7 +118,13 @@ 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` @@ -162,14 +173,14 @@ 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') @@ -207,6 +218,20 @@ See also: `TemplateHTMLRenderer` +## HTMLFormRenderer + +Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `
` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages. + +Note that the template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely. + +**.media_type**: `text/html` + +**.format**: `'.form'` + +**.charset**: `utf-8` + +**.template**: `'rest_framework/form.html'` + ## BrowsableAPIRenderer Renders data into HTML for the Browsable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page. @@ -217,6 +242,8 @@ **.charset**: `utf-8` +**.template**: `'rest_framework/api.html'` + #### Customizing BrowsableAPIRenderer By default the response content will be rendered with the highest priority renderer apart from `BrowseableAPIRenderer`. If you need to customize this behavior, for example to use HTML as the default return format, but use JSON in the browsable API, you can do so by overriding the `get_default_renderer()` method. For example: @@ -290,12 +317,15 @@ Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding. -If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string. +If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. + +In some cases you may also want to set the `render_style` attribute to `'binary'`. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string. class JPEGRenderer(renderers.BaseRenderer): media_type = 'image/jpeg' format = 'jpg' charset = None + render_style = 'binary' def render(self, data, media_type=None, renderer_context=None): return data @@ -385,12 +415,22 @@ 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 @@ -400,5 +440,10 @@ [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 -Nru djangorestframework-2.3.7/docs/api-guide/requests.md djangorestframework-2.3.12/docs/api-guide/requests.md --- djangorestframework-2.3.7/docs/api-guide/requests.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/requests.md 2014-01-15 14:27:41.000000000 +0000 @@ -117,7 +117,7 @@ # Standard HttpRequest attributes -As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` dictionary is available as normal. +As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` and `request.session` dictionaries are available as normal. Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition. diff -Nru djangorestframework-2.3.7/docs/api-guide/reverse.md djangorestframework-2.3.12/docs/api-guide/reverse.md --- djangorestframework-2.3.7/docs/api-guide/reverse.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/reverse.md 2014-01-15 14:27:41.000000000 +0000 @@ -27,13 +27,13 @@ You should **include the request as a keyword argument** to the function, for example: - import datetime from rest_framework.reverse import reverse from rest_framework.views import APIView + from django.utils.timezone import now class APIRootView(APIView): def get(self, request): - year = datetime.datetime.now().year + year = now().year data = { ... 'year-summary-url': reverse('year-summary', args=[year], request=request) diff -Nru djangorestframework-2.3.7/docs/api-guide/routers.md djangorestframework-2.3.12/docs/api-guide/routers.md --- djangorestframework-2.3.7/docs/api-guide/routers.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/routers.md 2014-01-15 14:27:41.000000000 +0000 @@ -12,7 +12,9 @@ ## 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 router = routers.SimpleRouter() router.register(r'users', UserViewSet) @@ -35,11 +37,26 @@ * 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. For example, given a method like this on the `UserViewSet` class: + from myapp.permissions import IsAdminOrIsSelf + from rest_framework.decorators import action + @action(permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): ... @@ -120,6 +137,8 @@ The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention. + from rest_framework.routers import Route, SimpleRouter + class ReadOnlyRouter(SimpleRouter): """ A router for read-only APIs, which doesn't use trailing slashes. @@ -143,4 +162,24 @@ 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 -Nru djangorestframework-2.3.7/docs/api-guide/serializers.md djangorestframework-2.3.12/docs/api-guide/serializers.md --- djangorestframework-2.3.7/docs/api-guide/serializers.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/serializers.md 2014-01-15 14:27:41.000000000 +0000 @@ -28,6 +28,8 @@ Declaring a serializer looks very similar to declaring a form: + from rest_framework import serializers + class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=200) @@ -59,14 +61,34 @@ At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`. + from rest_framework.renderers import JSONRenderer + json = JSONRenderer().render(serializer.data) json # '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}' +### Customizing field representation + +Sometimes when serializing objects, you may not want to represent everything exactly the way it is in your model. + +If you need to customize the serialized value of a particular field, you can do this by creating a `transform_` method. For example if you needed to render some markdown from a text field: + + description = serializers.TextField() + description_html = serializers.TextField(source='description', read_only=True) + + def transform_description_html(self, obj, value): + from django.contrib.markup.templatetags.markup import markdown + return markdown(value) + +These methods are essentially the reverse of `validate_` (see *Validation* below.) + ## Deserializing objects Deserialization is similar. First we parse a stream into Python native datatypes... + from StringIO import StringIO + from rest_framework.parsers import JSONParser + stream = StringIO(json) data = JSONParser().parse(stream) @@ -77,16 +99,15 @@ # True serializer.object # - >>> serializer.deserialize('json', stream) When deserializing data, we can either create a new instance, or update an existing instance. serializer = CommentSerializer(data=data) # Create new instance - serializer = CommentSerializer(comment, data=data) # Update `instance` + serializer = CommentSerializer(comment, data=data) # Update `comment` By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates. - serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data + serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `comment` with partial data ## Validation @@ -177,7 +198,7 @@ content = serializers.CharField(max_length=200) created = serializers.DateTimeField() -Similarly if a nested representation should be a list of items, you should the `many=True` flag to the nested serialized. +Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized. class CommentSerializer(serializers.Serializer): user = UserSerializer(required=False) @@ -185,11 +206,13 @@ content = serializers.CharField(max_length=200) created = serializers.DateTimeField() ---- - -**Note**: Nested serializers are only suitable for read-only representations, as there are cases where they would have ambiguous or non-obvious behavior if used when updating instances. For read-write representations you should always use a flat representation, by using one of the `RelatedField` subclasses. +Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object. ---- + serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'}) + serializer.is_valid() + # False + serializer.errors + # {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']} ## Dealing with multiple objects @@ -241,7 +264,7 @@ serializer = BookSerializer(queryset, data=data, many=True) serializer.is_valid() # True - serialize.save() # `.save()` will be called on each updated or newly created instance. + serializer.save() # `.save()` will be called on each updated or newly created instance. By default bulk updates will be limited to updating instances that already exist in the provided queryset. @@ -253,7 +276,7 @@ serializer.save() # `.save()` will be called on updated or newly created instances. # `.delete()` will be called on any other items in the `queryset`. -Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects. +Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects. #### How identity is determined when performing bulk updates @@ -293,8 +316,7 @@ The context dictionary can be used within any serializer field logic, such as a custom `.to_native()` method, by accessing the `self.context` attribute. ---- - +- # ModelSerializer Often you'll want serializer classes that map closely to model definitions. @@ -337,6 +359,8 @@ The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation. +If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself. + ## Specifying which fields should be read-only You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so: @@ -349,6 +373,25 @@ Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option. +## Specifying which fields should be write-only + +You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so: + + class CreateUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('email', 'username', 'password') + write_only_fields = ('password',) # Note: Password field is write-only + + def restore_object(self, attrs, instance=None): + """ + Instantiate a new User instance. + """ + assert instance is None, 'Cannot update users with CreateUserSerializer' + user = User(email=attrs['email'], username=attrs['username']) + user.set_password(attrs['password']) + return user + ## Specifying fields explicitly You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class. @@ -401,7 +444,7 @@ 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: @@ -421,6 +464,29 @@ model = Account fields = ('url', 'account_name', 'users', 'created') +## Overiding the URL field behavior + +The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting. + +You can also override this on a per-serializer basis by using the `url_field_name` option on the serializer, like so: + + class AccountSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Account + fields = ('account_url', 'account_name', 'users', 'created') + url_field_name = 'account_url' + +**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method. + +You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so: + + class AccountSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Account + fields = ('account_url', 'account_name', 'users', 'created') + view_name = 'account_detail' + lookup_field='account_name' + --- # Advanced serializer usage diff -Nru djangorestframework-2.3.7/docs/api-guide/settings.md djangorestframework-2.3.12/docs/api-guide/settings.md --- djangorestframework-2.3.7/docs/api-guide/settings.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/settings.md 2014-01-15 14:27:41.000000000 +0000 @@ -25,7 +25,7 @@ you should use the `api_settings` object. For example. from rest_framework.settings import api_settings - + print api_settings.DEFAULT_AUTHENTICATION_CLASSES The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. @@ -127,6 +127,35 @@ The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size. +For example, given the following settings: + + REST_FRAMEWORK = { + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size', + } + +A client would be able to modify the pagination size by using the `page_size` query parameter. For example: + + GET http://example.com/api/accounts?page_size=25 + +Default: `None` + +#### MAX_PAGINATE_BY + +The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied. + +For example, given the following settings: + + REST_FRAMEWORK = { + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size', + 'MAX_PAGINATE_BY': 100 + } + +A client request like the following would return a paginated list of up to 100 items. + + GET http://example.com/api/accounts?page_size=999 + Default: `None` --- @@ -274,8 +303,62 @@ --- +## View names and descriptions + +**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.** + +#### VIEW_NAME_FUNCTION + +A string representing the function that should be used when generating view names. + +This should be a function with the following signature: + + view_name(cls, suffix=None) + +* `cls`: The view class. Typically the name function would inspect the name of the class when generating a descriptive name, by accessing `cls.__name__`. +* `suffix`: The optional suffix used when differentiating individual views in a viewset. + +Default: `'rest_framework.views.get_view_name'` + +#### VIEW_DESCRIPTION_FUNCTION + +A string representing the function that should be used when generating view descriptions. + +This setting can be changed to support markup styles other than the default markdown. For example, you can use it to support `rst` markup in your view docstrings being output in the browsable API. + +This should be a function with the following signature: + + view_description(cls, html=False) + +* `cls`: The view class. Typically the description function would inspect the docstring of the class when generating a description, by accessing `cls.__doc__` +* `html`: A boolean indicating if HTML output is required. `True` when used in the browsable API, and `False` when used in generating `OPTIONS` responses. + +Default: `'rest_framework.views.get_view_description'` + +--- + ## Miscellaneous settings +#### EXCEPTION_HANDLER + +A string representing the function that should be used when returning a response for any given exception. If the function returns `None`, a 500 error will be raised. + +This setting can be changed to support error responses other than the default `{"detail": "Failure..."}` responses. For example, you can use it to provide API responses like `{"errors": [{"message": "Failure...", "code": ""} ...]}`. + +This should be a function with the following signature: + + exception_handler(exc) + +* `exc`: The exception. + +Default: `'rest_framework.views.exception_handler'` + +#### URL_FIELD_NAME + +A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`. + +Default: `'url'` + #### FORMAT_SUFFIX_KWARG The name of a parameter in the URL conf that may be used to provide a format suffix. diff -Nru djangorestframework-2.3.7/docs/api-guide/status-codes.md djangorestframework-2.3.12/docs/api-guide/status-codes.md --- djangorestframework-2.3.7/docs/api-guide/status-codes.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/status-codes.md 2014-01-15 14:27:41.000000000 +0000 @@ -9,6 +9,7 @@ Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable. from rest_framework import status + from rest_framework.response import Response def empty_view(self): content = {'please move along': 'nothing to see here'} @@ -16,6 +17,18 @@ 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]. @@ -89,6 +102,15 @@ 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 -Nru djangorestframework-2.3.7/docs/api-guide/testing.md djangorestframework-2.3.12/docs/api-guide/testing.md --- djangorestframework-2.3.7/docs/api-guide/testing.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/testing.md 2014-01-15 14:27:41.000000000 +0000 @@ -2,7 +2,7 @@ # Testing -> Code without tests is broken as designed +> Code without tests is broken as designed. > > — [Jacob Kaplan-Moss][cite] @@ -16,6 +16,8 @@ The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. + from rest_framework.test import APIRequestFactory + # Using the standard RequestFactory API to create a form POST request factory = APIRequestFactory() request = factory.post('/notes/', {'title': 'new idea'}) @@ -49,6 +51,8 @@ Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself: + from django.test.client import encode_multipart, RequestFactory + factory = RequestFactory() data = {'title': 'remember to email dave'} content = encode_multipart('BoUnDaRyStRiNg', data) @@ -72,6 +76,12 @@ The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set. +For example, when forcibly authenticating using a token, you might do something like the following: + + user = User.objects.get(username='olivia') + request = factory.get('/accounts/django-superstars/') + force_authenticate(request, user=user, token=user.token) + --- **Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called. @@ -105,6 +115,8 @@ The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: + from rest_framework.test import APIClient + client = APIClient() client.post('/notes/', {'title': 'new idea'}, format='json') @@ -131,8 +143,11 @@ The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. + from rest_framework.authtoken.models import Token + from rest_framework.test import APIClient + # Include an appropriate `Authorization:` header on all requests. - token = Token.objects.get(username='lauren') + token = Token.objects.get(user__username='lauren') client = APIClient() client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) diff -Nru djangorestframework-2.3.7/docs/api-guide/throttling.md djangorestframework-2.3.12/docs/api-guide/throttling.md --- djangorestframework-2.3.7/docs/api-guide/throttling.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/throttling.md 2014-01-15 14:27:41.000000000 +0000 @@ -43,6 +43,10 @@ You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. + from rest_framework.response import Response + from rest_framework.throttling import UserRateThrottle + from rest_framework.views import APIView + class ExampleView(APIView): throttle_classes = (UserRateThrottle,) @@ -55,7 +59,7 @@ Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @throttle_classes(UserRateThrottle) + @throttle_classes([UserRateThrottle]) def example_view(request, format=None): content = { 'status': 'request was permitted' @@ -66,6 +70,13 @@ The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. +If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example: + + class CustomAnonRateThrottle(AnonRateThrottle): + cache = get_cache('alternate') + +You'll need to rememeber to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. + --- # API Reference diff -Nru djangorestframework-2.3.7/docs/api-guide/views.md djangorestframework-2.3.12/docs/api-guide/views.md --- djangorestframework-2.3.7/docs/api-guide/views.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/views.md 2014-01-15 14:27:41.000000000 +0000 @@ -168,5 +168,5 @@ [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 -Nru djangorestframework-2.3.7/docs/api-guide/viewsets.md djangorestframework-2.3.12/docs/api-guide/viewsets.md --- djangorestframework-2.3.7/docs/api-guide/viewsets.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/api-guide/viewsets.md 2014-01-15 14:27:41.000000000 +0000 @@ -19,6 +19,12 @@ Let's define a simple viewset that can be used to list or retrieve all the users in the system. + from django.contrib.auth.models import User + from django.shortcuts import get_object_or_404 + from myapps.serializers import UserSerializer + from rest_framework import viewsets + from rest_framework.response import Response + class UserViewSet(viewsets.ViewSet): """ A simple ViewSet that for listing or retrieving users. @@ -41,6 +47,9 @@ Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated. + from myapp.views import UserViewSet + from rest_framework.routers import DefaultRouter + router = DefaultRouter() router.register(r'users', UserViewSet) urlpatterns = router.urls @@ -133,6 +142,10 @@ @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): ... + +The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` + + --- # API Reference @@ -157,7 +170,7 @@ #### 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): """ Binary files /tmp/olULvHXwLg/djangorestframework-2.3.7/docs/img/logo.png and /tmp/9Ggdcs1PT3/djangorestframework-2.3.12/docs/img/logo.png differ Binary files /tmp/olULvHXwLg/djangorestframework-2.3.7/docs/img/travis-status.png and /tmp/9Ggdcs1PT3/djangorestframework-2.3.12/docs/img/travis-status.png differ diff -Nru djangorestframework-2.3.7/docs/index.md djangorestframework-2.3.12/docs/index.md --- djangorestframework-2.3.7/docs/index.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/index.md 2014-01-15 14:27:41.000000000 +0000 @@ -1,15 +1,30 @@ -

+

- +

-# Django REST framework +--- + +

+

Django REST Framework

-**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 +35,16 @@ * [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: @@ -42,6 +60,7 @@ * [django-filter][django-filter] (0.5.4+) - Filtering support. * [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support. * [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support. +* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. **Note**: The `oauth2` Python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible. @@ -61,7 +80,7 @@ INSTALLED_APPS = ( ... - 'rest_framework', + 'rest_framework', ) If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file. @@ -99,7 +118,7 @@ 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 @@ -110,8 +129,8 @@ class GroupViewSet(viewsets.ModelViewSet): 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) @@ -139,6 +158,8 @@ * [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. @@ -164,6 +185,7 @@ * [Returning URLs][reverse] * [Exceptions][exceptions] * [Status codes][status] +* [Testing][testing] * [Settings][settings] ## Topics @@ -175,6 +197,7 @@ * [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] @@ -193,13 +216,13 @@ ./rest_framework/runtests/runtests.py -To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`: +To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`: tox ## Support -For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. +For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. [Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options. @@ -216,32 +239,33 @@ ## 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 +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -Redistributions of source code must retain the above copyright notice, this +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [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 @@ -249,14 +273,15 @@ [oauth2]: https://github.com/simplegeo/python-oauth2 [django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider +[django-guardian]: https://github.com/lukaszb/django-guardian [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 @@ -288,6 +313,7 @@ [reverse]: api-guide/reverse.md [exceptions]: api-guide/exceptions.md [status]: api-guide/status-codes.md +[testing]: api-guide/testing.md [settings]: api-guide/settings.md [documenting-your-api]: topics/documenting-your-api.md @@ -305,6 +331,7 @@ [tox]: http://testrun.org/tox/latest/ [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[botbot]: https://botbot.me/freenode/restframework/ [stack-overflow]: http://stackoverflow.com/ [django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework [django-tag]: http://stackoverflow.com/questions/tagged/django diff -Nru djangorestframework-2.3.7/docs/template.html djangorestframework-2.3.12/docs/template.html --- djangorestframework-2.3.7/docs/template.html 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/template.html 2014-01-15 14:27:41.000000000 +0000 @@ -4,6 +4,7 @@ {{ title }} + @@ -89,6 +90,7 @@
  • Returning URLs
  • Exceptions
  • Status codes
  • +
  • Testing
  • Settings
  • @@ -100,6 +102,7 @@
  • Browser enhancements
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • +
  • Contributing to REST framework
  • 2.0 Announcement
  • 2.2 Announcement
  • 2.3 Announcement
  • @@ -166,7 +169,13 @@
    +
    + +{{ ad_block }} + +
    + +
    diff -Nru djangorestframework-2.3.7/docs/topics/2.2-announcement.md djangorestframework-2.3.12/docs/topics/2.2-announcement.md --- djangorestframework-2.3.7/docs/topics/2.2-announcement.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/2.2-announcement.md 2014-01-15 14:27:41.000000000 +0000 @@ -151,7 +151,7 @@ [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 -Nru djangorestframework-2.3.7/docs/topics/ajax-csrf-cors.md djangorestframework-2.3.12/docs/topics/ajax-csrf-cors.md --- djangorestframework-2.3.7/docs/topics/ajax-csrf-cors.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/ajax-csrf-cors.md 2014-01-15 14:27:41.000000000 +0000 @@ -6,7 +6,7 @@ ## Javascript clients -If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. +If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. diff -Nru djangorestframework-2.3.7/docs/topics/browsable-api.md djangorestframework-2.3.12/docs/topics/browsable-api.md --- djangorestframework-2.3.7/docs/topics/browsable-api.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/browsable-api.md 2014-01-15 14:27:41.000000000 +0000 @@ -115,6 +115,7 @@ * `name` : The name of the resource * `post_form` : A form instance for use by the POST form (if allowed) * `put_form` : A form instance for use by the PUT form (if allowed) +* `display_edit_forms` : A boolean indicating whether or not POST, PUT and PATCH forms will be displayed * `request` : The request object * `response` : The response object * `version` : The version of Django REST Framework @@ -122,6 +123,8 @@ * `FORMAT_PARAM` : The view can accept a format override * `METHOD_PARAM` : The view can accept a method override +You can override the `BrowsableAPIRenderer.get_context()` method to customise the context that gets passed to the template. + #### Not using base.html For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. diff -Nru djangorestframework-2.3.7/docs/topics/contributing.md djangorestframework-2.3.12/docs/topics/contributing.md --- djangorestframework-2.3.7/docs/topics/contributing.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/contributing.md 2014-01-15 14:27:41.000000000 +0000 @@ -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 @@ * 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 @@ --- - **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 -Nru djangorestframework-2.3.7/docs/topics/credits.md djangorestframework-2.3.12/docs/topics/credits.md --- djangorestframework-2.3.7/docs/topics/credits.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/credits.md 2014-01-15 14:27:41.000000000 +0000 @@ -157,6 +157,31 @@ * Dan Stephenson - [etos] * Martin Clement - [martync] * Jeremy Satterfield - [jsatt] +* Christopher Paolini - [chrispaolini] +* Filipe A Ximenes - [filipeximenes] +* Ramiro Morales - [ramiro] +* Krzysztof Jurewicz - [krzysiekj] +* Eric Buehl - [ericbuehl] +* Kristian Øllegaard - [kristianoellegaard] +* Alexander Akhmetov - [alexander-akhmetov] +* Andrey Antukh - [niwibe] +* Mathieu Pillard - [diox] +* Edmond Wong - [edmondwong] +* Ben Reilly - [bwreilly] +* Tai Lee - [mrmachine] +* Markus Kaiserswerth - [mkai] +* Henry Clifford - [hcliff] +* Thomas Badaud - [badale] +* Colin Huang - [tamakisquare] +* Ross McFarland - [ross] +* 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. @@ -350,3 +375,28 @@ [etos]: https://github.com/etos [martync]: https://github.com/martync [jsatt]: https://github.com/jsatt +[chrispaolini]: https://github.com/chrispaolini +[filipeximenes]: https://github.com/filipeximenes +[ramiro]: https://github.com/ramiro +[krzysiekj]: https://github.com/krzysiekj +[ericbuehl]: https://github.com/ericbuehl +[kristianoellegaard]: https://github.com/kristianoellegaard +[alexander-akhmetov]: https://github.com/alexander-akhmetov +[niwibe]: https://github.com/niwibe +[diox]: https://github.com/diox +[edmondwong]: https://github.com/edmondwong +[bwreilly]: https://github.com/bwreilly +[mrmachine]: https://github.com/mrmachine +[mkai]: https://github.com/mkai +[hcliff]: https://github.com/hcliff +[badale]: https://github.com/badale +[tamakisquare]: https://github.com/tamakisquare +[ross]: https://github.com/ross +[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 -Nru djangorestframework-2.3.7/docs/topics/release-notes.md djangorestframework-2.3.12/docs/topics/release-notes.md --- djangorestframework-2.3.7/docs/topics/release-notes.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/release-notes.md 2014-01-15 14:27:41.000000000 +0000 @@ -40,6 +40,81 @@ ## 2.3.x series +### 2.3.12 + +**Date**: 15th January 2014 + +* **Security fix**: `OrderingField` now only allows ordering on readable serializer fields, or on fields explicitly specified using `ordering_fields`. This prevents users being able to order by fields that are not visible in the API, and exploiting the ordering of sensitive data such as password hashes. +* Bugfix: `write_only = True` fields now display in the browsable API. + +### 2.3.11 + +**Date**: 14th January 2014 + +* Added `write_only` serializer field argument. +* Added `write_only_fields` option to `ModelSerializer` classes. +* 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_` 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 + +**Date**: 11th September 2013 + +* Added `DjangoObjectPermissions`, and `DjangoObjectPermissionsFilter`. +* Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. +* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. +* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. +* Added `cache` attribute to throttles to allow overriding of default cache. +* 'Raw data' tab in browsable API now contains pre-populated data. +* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. +* Bugfix: `required=True` argument fixed for boolean serializer fields. +* 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 **Date**: 16th August 2013 diff -Nru djangorestframework-2.3.7/docs/topics/writable-nested-serializers.md djangorestframework-2.3.12/docs/topics/writable-nested-serializers.md --- djangorestframework-2.3.7/docs/topics/writable-nested-serializers.md 1970-01-01 00:00:00.000000000 +0000 +++ djangorestframework-2.3.12/docs/topics/writable-nested-serializers.md 2014-01-15 14:27:41.000000000 +0000 @@ -0,0 +1,47 @@ +> To save HTTP requests, it may be convenient to send related documents along with the request. +> +> — [JSON API specification for Ember Data][cite]. + +# Writable nested serializers + +Although flat data structures serve to properly delineate between the individual entities in your service, there are cases where it may be more appropriate or convenient to use nested data structures. + +Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependancies between the various model instances, and the need to save or delete multiple instances in a single action. + +## One-to-many data structures + +*Example of a **read-only** nested serializer. Nothing complex to worry about here.* + + class ToDoItemSerializer(serializers.ModelSerializer): + class Meta: + model = ToDoItem + fields = ('text', 'is_completed') + + class ToDoListSerializer(serializers.ModelSerializer): + items = ToDoItemSerializer(many=True, read_only=True) + + class Meta: + model = ToDoList + fields = ('title', 'items') + +Some example output from our serializer. + + { + 'title': 'Leaving party preperations', + 'items': { + {'text': 'Compile playlist', 'is_completed': True}, + {'text': 'Send invites', 'is_completed': False}, + {'text': 'Clean house', 'is_completed': False} + } + } + +Let's take a look at updating our nested one-to-many data structure. + +### Validation errors + +### Adding and removing items + +### Making PATCH requests + + +[cite]: http://jsonapi.org/format/#url-based-json-api \ No newline at end of file diff -Nru djangorestframework-2.3.7/docs/tutorial/1-serialization.md djangorestframework-2.3.12/docs/tutorial/1-serialization.md --- djangorestframework-2.3.7/docs/tutorial/1-serialization.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/1-serialization.md 2014-01-15 14:27:41.000000000 +0000 @@ -17,9 +17,8 @@ Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on. :::bash - mkdir ~/env - virtualenv ~/env/tutorial - source ~/env/tutorial/bin/activate + virtualenv env + source env/bin/activate Now that we're inside a virtualenv environment, we can install our package requirements. @@ -183,9 +182,11 @@ 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. @@ -225,7 +226,7 @@ We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`. -Edit the `snippet/views.py` file, and add the following. +Edit the `snippets/views.py` file, and add the following. from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt @@ -261,8 +262,7 @@ 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 +288,7 @@ 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 -Nru djangorestframework-2.3.7/docs/tutorial/2-requests-and-responses.md djangorestframework-2.3.12/docs/tutorial/2-requests-and-responses.md --- djangorestframework-2.3.7/docs/tutorial/2-requests-and-responses.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/2-requests-and-responses.md 2014-01-15 14:27:41.000000000 +0000 @@ -35,7 +35,7 @@ Okay, let's go ahead and start using these new components to write a few views. -We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. +We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. from rest_framework import status from rest_framework.decorators import api_view @@ -59,12 +59,11 @@ 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. -Here is the view for an individual snippet. +Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): @@ -85,8 +84,7 @@ 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() @@ -147,7 +145,7 @@ # POST using form data curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" - {"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"} + {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"} # POST using JSON curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" diff -Nru djangorestframework-2.3.7/docs/tutorial/3-class-based-views.md djangorestframework-2.3.12/docs/tutorial/3-class-based-views.md --- djangorestframework-2.3.7/docs/tutorial/3-class-based-views.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/3-class-based-views.md 2014-01-15 14:27:41.000000000 +0000 @@ -4,7 +4,7 @@ ## Rewriting our API using class based views -We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring. +We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring of `views.py`. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -30,7 +30,7 @@ return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view. +So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. class SnippetDetail(APIView): """ @@ -62,7 +62,7 @@ That's looking good. Again, it's still pretty similar to the function based view right now. -We'll also need to refactor our URLconf slightly now we're using class based views. +We'll also need to refactor our `urls.py` slightly now we're using class based views. from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns @@ -83,7 +83,7 @@ The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. -Let's take a look at how we can compose our views by using the mixin classes. +Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -126,7 +126,7 @@ ## Using generic class based views -Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use. +Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down our `views.py` module even more. from snippets.models import Snippet from snippets.serializers import SnippetSerializer diff -Nru djangorestframework-2.3.7/docs/tutorial/4-authentication-and-permissions.md djangorestframework-2.3.12/docs/tutorial/4-authentication-and-permissions.md --- djangorestframework-2.3.7/docs/tutorial/4-authentication-and-permissions.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/4-authentication-and-permissions.md 2014-01-15 14:27:41.000000000 +0000 @@ -12,7 +12,7 @@ We're going to make a couple of changes to our `Snippet` model class. First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code. -Add the following two fields to the model. +Add the following two fields to the `Snippet` model in `models.py`. owner = models.ForeignKey('auth.User', related_name='snippets') highlighted = models.TextField() @@ -52,7 +52,7 @@ ## Adding endpoints for our User models -Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy: +Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add: from django.contrib.auth.models import User @@ -65,7 +65,10 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it. -We'll also add a couple of views. 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. +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() @@ -75,8 +78,12 @@ class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer + +Make sure to also import the `UserSerializer` class -Finally we need to add those views into the API, by referencing them from the URL conf. + from snippets.serializers import UserSerializer + +Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), @@ -94,7 +101,7 @@ ## Updating our serializer -Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition: +Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`: owner = serializers.Field(source='owner.username') @@ -160,10 +167,10 @@ 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 -Nru djangorestframework-2.3.7/docs/tutorial/6-viewsets-and-routers.md djangorestframework-2.3.12/docs/tutorial/6-viewsets-and-routers.md --- djangorestframework-2.3.7/docs/tutorial/6-viewsets-and-routers.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/6-viewsets-and-routers.md 2014-01-15 14:27:41.000000000 +0000 @@ -61,6 +61,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views. from snippets.views import SnippetViewSet, UserViewSet + from rest_framework import renderers snippet_list = SnippetViewSet.as_view({ 'get': 'list', @@ -101,6 +102,7 @@ Here's our re-wired `urls.py` file. + from django.conf.urls import patterns, url, include from snippets import views from rest_framework.routers import DefaultRouter diff -Nru djangorestframework-2.3.7/docs/tutorial/quickstart.md djangorestframework-2.3.12/docs/tutorial/quickstart.md --- djangorestframework-2.3.7/docs/tutorial/quickstart.md 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/docs/tutorial/quickstart.md 2014-01-15 14:27:41.000000000 +0000 @@ -12,7 +12,7 @@ # Create a virtualenv to isolate our package dependencies locally virtualenv env - source env/bin/activate + source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Django REST framework into the virtualenv pip install django @@ -85,10 +85,14 @@ queryset = Group.objects.all() serializer_class = GroupSerializer -Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`. +Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. 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 @@ 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 -Nru djangorestframework-2.3.7/mkdocs.py djangorestframework-2.3.12/mkdocs.py --- djangorestframework-2.3.7/mkdocs.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/mkdocs.py 2014-01-15 14:27:41.000000000 +0000 @@ -18,8 +18,8 @@ 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 = '' @@ -69,6 +69,7 @@ 'api-guide/reverse.md', 'api-guide/exceptions.md', 'api-guide/status-codes.md', + 'api-guide/testing.md', 'api-guide/settings.md', 'topics/documenting-your-api.md', 'topics/ajax-csrf-cors.md', @@ -89,7 +90,10 @@ 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: @@ -140,8 +144,12 @@ 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) @@ -151,6 +159,13 @@ 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 }}', """

    The team behind REST framework is launching a new API service.

    +

    If you want to be first in line when we start issuing invitations, please sign up here.

    """) + else: + output = output.replace('{{ ad_block }}', '') if prev_url: output = output.replace('{{ prev_url }}', prev_url) diff -Nru djangorestframework-2.3.7/optionals.txt djangorestframework-2.3.12/optionals.txt --- djangorestframework-2.3.7/optionals.txt 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/optionals.txt 2014-01-15 14:27:41.000000000 +0000 @@ -2,6 +2,6 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/__init__.py djangorestframework-2.3.12/rest_framework/__init__.py --- djangorestframework-2.3.7/rest_framework/__init__.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/__init__.py 2014-01-15 14:27:41.000000000 +0000 @@ -1,6 +1,20 @@ -__version__ = '2.3.7' +""" +______ _____ _____ _____ __ _ +| ___ \ ___/ ___|_ _| / _| | | +| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ +| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / +| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < +\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| +""" -VERSION = __version__ # synonym +__title__ = 'Django REST framework' +__version__ = '2.3.12' +__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 -Nru djangorestframework-2.3.7/rest_framework/authentication.py djangorestframework-2.3.12/rest_framework/authentication.py --- djangorestframework-2.3.7/rest_framework/authentication.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/authentication.py 2014-01-15 14:27:41.000000000 +0000 @@ -9,7 +9,7 @@ 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 @@ """ 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 -Nru djangorestframework-2.3.7/rest_framework/authtoken/models.py djangorestframework-2.3.12/rest_framework/authtoken/models.py --- djangorestframework-2.3.7/rest_framework/authtoken/models.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/authtoken/models.py 2014-01-15 14:27:41.000000000 +0000 @@ -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 -Nru djangorestframework-2.3.7/rest_framework/compat.py djangorestframework-2.3.12/rest_framework/compat.py --- djangorestframework-2.3.7/rest_framework/compat.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/compat.py 2014-01-15 14:27:41.000000000 +0000 @@ -7,6 +7,7 @@ from __future__ import unicode_literals import django +import inspect from django.core.exceptions import ImproperlyConfigured from django.conf import settings @@ -47,6 +48,12 @@ except ImportError: django_filters = None +# guardian is optional +try: + import guardian +except ImportError: + guardian = None + # cStringIO only if it's available, otherwise StringIO try: @@ -63,6 +70,13 @@ 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: @@ -74,6 +88,14 @@ Image = None +def get_model_name(model_cls): + try: + return model_cls._meta.model_name + except AttributeError: + # < 1.6 used module_name instead of model_name + return model_cls._meta.module_name + + def get_concrete_model(model_cls): try: return model_cls._meta.concrete_model @@ -82,13 +104,6 @@ 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: @@ -515,9 +530,23 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/exceptions.py djangorestframework-2.3.12/rest_framework/exceptions.py --- djangorestframework-2.3.7/rest_framework/exceptions.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/exceptions.py 2014-01-15 14:27:41.000000000 +0000 @@ -6,6 +6,7 @@ """ from __future__ import unicode_literals from rest_framework import status +import math class APIException(Exception): @@ -13,40 +14,32 @@ Base class for REST framework exceptions. Subclasses should provide `.status_code` and `.detail` properties. """ - pass + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = '' + + def __init__(self, detail=None): + self.detail = detail or self.default_detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = 'Malformed request.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Incorrect authentication credentials.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Authentication credentials were not provided.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = 'You do not have permission to perform this action.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED @@ -75,14 +68,14 @@ class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS - default_detail = "Request was throttled." + default_detail = 'Request was throttled.' extra_detail = "Expected available in %d second%s." def __init__(self, wait=None, detail=None): - import math - self.wait = wait and math.ceil(wait) or None - if wait is not None: - format = detail or self.default_detail + self.extra_detail - self.detail = format % (self.wait, self.wait != 1 and 's' or '') - else: + if wait is None: self.detail = detail or self.default_detail + self.wait = None + else: + format = (detail or self.default_detail) + self.extra_detail + self.detail = format % (wait, wait != 1 and 's' or '') + self.wait = math.ceil(wait) diff -Nru djangorestframework-2.3.7/rest_framework/fields.py djangorestframework-2.3.12/rest_framework/fields.py --- djangorestframework-2.3.7/rest_framework/fields.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/fields.py 2014-01-15 14:27:41.000000000 +0000 @@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError from django.conf import settings from django.db.models.fields import BLANK_CHOICE_DASH +from django.http import QueryDict from django.forms import widgets from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ @@ -122,6 +123,7 @@ use_files = False form_field_class = forms.CharField type_label = 'field' + widget = None def __init__(self, source=None, label=None, help_text=None): self.parent = None @@ -133,9 +135,29 @@ if label is not None: self.label = smart_text(label) + else: + self.label = None if help_text is not None: self.help_text = strip_multiple_choice_msg(smart_text(help_text)) + else: + self.help_text = None + + self._errors = [] + self._value = None + self._name = None + + @property + def errors(self): + return self._errors + + def widget_html(self): + if not self.widget: + return '' + return self.widget.render(self._name, self._value) + + def label_tag(self): + return '' % (self._name, self.label) def initialize(self, parent, field_name): """ @@ -224,6 +246,7 @@ """ Base for read/write fields. """ + write_only = False default_validators = [] default_error_messages = { 'required': _('This field is required.'), @@ -233,7 +256,7 @@ default = None def __init__(self, source=None, label=None, help_text=None, - read_only=False, required=None, + read_only=False, write_only=False, required=None, validators=[], error_messages=None, widget=None, default=None, blank=None): @@ -247,6 +270,10 @@ super(WritableField, self).__init__(source=source, label=label, help_text=help_text) self.read_only = read_only + self.write_only = write_only + + assert not (read_only and write_only), "Cannot set read_only=True and write_only=True" + if required is None: self.required = not(read_only) else: @@ -296,6 +323,11 @@ if errors: raise ValidationError(errors) + def field_to_native(self, obj, field_name): + if self.write_only: + return None + return super(WritableField, self).field_to_native(obj, field_name) + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, @@ -305,9 +337,13 @@ return try: + data = data or {} if self.use_files: files = files or {} - native = files[field_name] + try: + native = files[field_name] + except KeyError: + native = data[field_name] else: native = data[field_name] except KeyError: @@ -399,10 +435,15 @@ } empty = False - # Note: we set default to `False` in order to fill in missing value not - # supplied by html form. TODO: Fix so that only html form input gets - # this behavior. - default = False + 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) and self.default is None: + self.default = False + + return super(BooleanField, self).field_from_native( + data, files, field_name, into + ) def from_native(self, value): if value in ('true', 't', 'True', '1'): @@ -466,6 +507,7 @@ } def __init__(self, choices=(), *args, **kwargs): + self.empty = kwargs.pop('empty', '') super(ChoiceField, self).__init__(*args, **kwargs) self.choices = choices if not self.required: @@ -482,6 +524,11 @@ 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. @@ -505,6 +552,12 @@ return True return False + def from_native(self, 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): type_name = 'EmailField' @@ -742,6 +795,7 @@ type_name = 'IntegerField' type_label = 'integer' form_field_class = forms.IntegerField + empty = 0 default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -773,6 +827,7 @@ type_name = 'FloatField' type_label = 'float' form_field_class = forms.FloatField + empty = 0 default_error_messages = { 'invalid': _("'%s' value must be a float."), @@ -793,6 +848,7 @@ type_name = 'DecimalField' type_label = 'decimal' form_field_class = forms.DecimalField + empty = Decimal('0') default_error_messages = { 'invalid': _('Enter a number.'), @@ -925,7 +981,7 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/filters.py djangorestframework-2.3.12/rest_framework/filters.py --- djangorestframework-2.3.7/rest_framework/filters.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/filters.py 2014-01-15 14:27:41.000000000 +0000 @@ -3,8 +3,9 @@ returned by list views. """ from __future__ import unicode_literals +from django.core.exceptions import ImproperlyConfigured from django.db import models -from rest_framework.compat import django_filters, six +from rest_framework.compat import django_filters, six, guardian, get_model_name from functools import reduce import operator @@ -53,6 +54,7 @@ class Meta: model = queryset.model fields = filter_fields + order_by = True return AutoFilterSet return None @@ -106,6 +108,7 @@ class OrderingFilter(BaseFilterBackend): ordering_param = 'ordering' # The URL query parameter used for the ordering. + ordering_fields = None def get_ordering(self, request): """ @@ -121,16 +124,34 @@ return (ordering,) return ordering - def remove_invalid_fields(self, queryset, ordering): - field_names = [field.name for field in queryset.model._meta.fields] - return [term for term in ordering if term.lstrip('-') in field_names] + def remove_invalid_fields(self, queryset, ordering, view): + valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) + + if valid_fields is None: + # Default to allowing filtering on serializer fields + serializer_class = getattr(view, 'serializer_class') + if serializer_class is None: + msg = ("Cannot use %s on a view which does not have either a " + "'serializer_class' or 'ordering_fields' attribute.") + raise ImproperlyConfigured(msg % self.__class__.__name__) + valid_fields = [ + field.source or field_name + for field_name, field in serializer_class().fields.items() + if not getattr(field, 'write_only', False) + ] + elif valid_fields == '__all__': + # View explictly allows filtering on any model field + valid_fields = [field.name for field in queryset.model._meta.fields] + valid_fields += queryset.query.aggregates.keys() + + return [term for term in ordering if term.lstrip('-') in valid_fields] def filter_queryset(self, request, queryset, view): ordering = self.get_ordering(request) if ordering: # Skip any incorrect parameters - ordering = self.remove_invalid_fields(queryset, ordering) + ordering = self.remove_invalid_fields(queryset, ordering, view) if not ordering: # Use 'ordering' attribute by default @@ -140,3 +161,24 @@ return queryset.order_by(*ordering) return queryset + + +class DjangoObjectPermissionsFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + def __init__(self): + assert guardian, 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed' + + perm_format = '%(app_label)s.view_%(model_name)s' + + def filter_queryset(self, request, queryset, view): + user = request.user + model_cls = queryset.model + kwargs = { + 'app_label': model_cls._meta.app_label, + 'model_name': get_model_name(model_cls) + } + permission = self.perm_format % kwargs + return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff -Nru djangorestframework-2.3.7/rest_framework/generics.py djangorestframework-2.3.12/rest_framework/generics.py --- djangorestframework-2.3.7/rest_framework/generics.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/generics.py 2014-01-15 14:27:41.000000000 +0000 @@ -14,13 +14,24 @@ import warnings -def get_object_or_404(queryset, **filter_kwargs): +def strict_positive_int(integer_string, cutoff=None): + """ + Cast a string to a strictly positive integer. + """ + ret = int(integer_string) + if ret <= 0: + raise ValueError() + if cutoff: + ret = min(ret, cutoff) + return ret + +def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to raise 404 if the filter_kwargs don't match the required types. """ try: - return _get_object_or_404(queryset, **filter_kwargs) + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) except (TypeError, ValueError): raise Http404 @@ -43,10 +54,12 @@ # If you want to use object lookups other than pk, set this attribute. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' + lookup_url_kwarg = None # Pagination settings paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM + max_paginate_by = api_settings.MAX_PAGINATE_BY pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS page_kwarg = 'page' @@ -135,8 +148,8 @@ page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) page = page_kwarg or page_query_param or 1 try: - page_number = int(page) - except ValueError: + page_number = paginator.validate_number(page) + except InvalidPage: if page == 'last': page_number = paginator.num_pages else: @@ -162,6 +175,14 @@ method if you want to apply the configured filtering backend to the default queryset. """ + for backend in self.get_filter_backends(): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def get_filter_backends(self): + """ + Returns the list of filter backends that this view requires. + """ filter_backends = self.filter_backends or [] if not filter_backends and self.filter_backend: warnings.warn( @@ -172,10 +193,8 @@ PendingDeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] + return filter_backends - for backend in filter_backends: - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset ######################## ### The following methods provide default implementations @@ -196,9 +215,11 @@ PendingDeprecationWarning, stacklevel=2) if self.paginate_by_param: - query_params = self.request.QUERY_PARAMS try: - return int(query_params[self.paginate_by_param]) + return strict_positive_int( + self.request.QUERY_PARAMS[self.paginate_by_param], + cutoff=self.max_paginate_by + ) except (KeyError, ValueError): pass @@ -264,9 +285,11 @@ pass # Deprecation warning # Perform the lookup filtering. + # Note that `pk` and `slug` are deprecated styles of lookup filtering. + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - lookup = self.kwargs.get(self.lookup_field, None) if lookup is not None: filter_kwargs = {self.lookup_field: lookup} @@ -321,6 +344,18 @@ """ 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. @@ -342,8 +377,15 @@ self.check_permissions(cloned_request) # Test object permissions if method == 'PUT': - self.get_object() - except (exceptions.APIException, PermissionDenied, Http404): + try: + self.get_object() + except Http404: + # Http404 should be acceptable and the serializer + # metadata should be populated. Except this so the + # outer "else" clause of the try-except-else block + # will be executed. + pass + except (exceptions.APIException, PermissionDenied): pass else: # If user has appropriate permissions for the view, include diff -Nru djangorestframework-2.3.7/rest_framework/mixins.py djangorestframework-2.3.12/rest_framework/mixins.py --- djangorestframework-2.3.7/rest_framework/mixins.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/mixins.py 2014-01-15 14:27:41.000000000 +0000 @@ -6,10 +6,12 @@ """ 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 from rest_framework.request import clone_request +from rest_framework.settings import api_settings import warnings @@ -59,7 +61,7 @@ def get_success_headers(self, data): try: - return {'Location': data['url']} + return {'Location': data[api_settings.URL_FIELD_NAME]} except (TypeError, KeyError): return {} @@ -127,7 +129,12 @@ 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) @@ -142,18 +149,24 @@ try: return self.get_object() except Http404: - # If this is a PUT-as-create operation, we need to ensure that - # we have relevant permissions, as if this was a POST request. - # This will either raise a PermissionDenied exception, - # or simply return None - self.check_permissions(clone_request(self.request, 'POST')) + if self.request.method == 'PUT': + # For PUT-as-create operation, we need to ensure that we have + # relevant permissions, as if this was a POST request. This + # will either raise a PermissionDenied exception, or simply + # return None. + self.check_permissions(clone_request(self.request, 'POST')) + else: + # PATCH requests where the object does not exist should still + # return a 404 response. + raise def pre_save(self, obj): """ Set any attributes on the object that are implicit in the request. """ # pk and/or slug attributes are implicit in the URL. - lookup = self.kwargs.get(self.lookup_field, None) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) slug_field = slug and self.slug_field or None @@ -180,5 +193,7 @@ """ 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 -Nru djangorestframework-2.3.7/rest_framework/parsers.py djangorestframework-2.3.12/rest_framework/parsers.py --- djangorestframework-2.3.7/rest_framework/parsers.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/parsers.py 2014-01-15 14:27:41.000000000 +0000 @@ -10,9 +10,9 @@ from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter -from rest_framework.compat import yaml, etree +from rest_framework.compat import etree, six, yaml from rest_framework.exceptions import ParseError -from rest_framework.compat import six +from rest_framework import renderers import json import datetime import decimal @@ -47,6 +47,7 @@ """ media_type = 'application/json' + renderer_class = renderers.UnicodeJSONRenderer def parse(self, stream, media_type=None, parser_context=None): """ @@ -82,7 +83,7 @@ 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): @@ -121,7 +122,8 @@ parser_context = parser_context or {} request = parser_context['request'] encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) - meta = request.META + meta = request.META.copy() + meta['CONTENT_TYPE'] = media_type upload_handlers = request.upload_handlers try: @@ -129,7 +131,7 @@ data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % six.u(exc)) + raise ParseError('Multipart form parse error - %s' % str(exc)) class XMLParser(BaseParser): @@ -151,7 +153,7 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/permissions.py djangorestframework-2.3.12/rest_framework/permissions.py --- djangorestframework-2.3.7/rest_framework/permissions.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/permissions.py 2014-01-15 14:27:41.000000000 +0000 @@ -7,7 +7,9 @@ SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -from rest_framework.compat import oauth2_provider_scope, oauth2_constants +from django.http import Http404 +from rest_framework.compat import (get_model_name, oauth2_provider_scope, + oauth2_constants) class BasePermission(object): @@ -52,9 +54,7 @@ """ 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): @@ -63,9 +63,7 @@ """ 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): @@ -74,11 +72,9 @@ """ 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): @@ -115,7 +111,7 @@ """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } return [perm % kwargs for perm in self.perms_map[method]] @@ -136,11 +132,9 @@ 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): @@ -151,6 +145,65 @@ authenticated_users_only = False +class DjangoObjectPermissions(DjangoModelPermissions): + """ + The request is authenticated using Django's object-level permissions. + It requires an object-permissions-enabled backend, such as Django Guardian. + + It ensures that the user is authenticated, and has the appropriate + `add`/`change`/`delete` permissions on the object using .has_perms. + + This permission can only be applied against view classes that + provide a `.model` or `.queryset` attribute. + """ + + perms_map = { + 'GET': [], + 'OPTIONS': [], + 'HEAD': [], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + + def get_required_object_permissions(self, method, model_cls): + kwargs = { + 'app_label': model_cls._meta.app_label, + 'model_name': get_model_name(model_cls) + } + return [perm % kwargs for perm in self.perms_map[method]] + + def has_object_permission(self, request, view, obj): + model_cls = getattr(view, 'model', None) + queryset = getattr(view, 'queryset', None) + + if model_cls is None and queryset is not None: + model_cls = queryset.model + + perms = self.get_required_object_permissions(request.method, model_cls) + user = request.user + + if not user.has_perms(perms, obj): + # If the user does not have permissions we need to determine if + # they have read permissions to see 403, or not, and simply see + # a 404 reponse. + + if request.method in ('GET', 'OPTIONS', 'HEAD'): + # Read permissions already checked and failed, no need + # to make another lookup. + raise Http404 + + read_perms = self.get_required_object_permissions('GET', model_cls) + if not user.has_perms(read_perms, obj): + raise Http404 + + # Has read permissions. + return False + + return True + + class TokenHasReadWriteScope(BasePermission): """ The request is authenticated as a user and the token used has the right scope diff -Nru djangorestframework-2.3.7/rest_framework/relations.py djangorestframework-2.3.12/rest_framework/relations.py --- djangorestframework-2.3.7/rest_framework/relations.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/relations.py 2014-01-15 14:27:41.000000000 +0000 @@ -65,16 +65,11 @@ def initialize(self, parent, field_name): super(RelatedField, self).initialize(parent, field_name) if self.queryset is None and not self.read_only: - try: - manager = getattr(self.parent.opts.model, self.source or field_name) - if hasattr(manager, 'related'): # Forward - self.queryset = manager.related.model._default_manager.all() - else: # Reverse - self.queryset = manager.field.rel.to._default_manager.all() - except Exception: - msg = ('Serializer related fields must include a `queryset`' + - ' argument or set `read_only=True') - raise Exception(msg) + manager = getattr(self.parent.opts.model, self.source or field_name) + if hasattr(manager, 'related'): # Forward + self.queryset = manager.related.model._default_manager.all() + else: # Reverse + self.queryset = manager.field.rel.to._default_manager.all() ### We need this stuff to make form choices work... @@ -134,9 +129,9 @@ value = obj for component in source.split('.'): - value = get_component(value, component) if value is None: break + value = get_component(value, component) except ObjectDoesNotExist: return None @@ -244,6 +239,8 @@ source = self.source or field_name queryset = obj for component in source.split('.'): + if queryset is None: + return [] queryset = get_component(queryset, component) # Forward relationship @@ -262,7 +259,7 @@ # RelatedObject (reverse relationship) try: pk = getattr(obj, self.source or field_name).pk - except ObjectDoesNotExist: + except (ObjectDoesNotExist, AttributeError): return None # Forward relationship @@ -567,8 +564,13 @@ May raise a `NoReverseMatch` if the `view_name` and `lookup_field` attributes are not configured to correctly match the URL conf. """ - lookup_field = getattr(obj, self.lookup_field) + lookup_field = getattr(obj, self.lookup_field, None) kwargs = {self.lookup_field: lookup_field} + + # Handle unsaved object case + if lookup_field is None: + return None + try: return reverse(view_name, kwargs=kwargs, request=request, format=format) except NoReverseMatch: diff -Nru djangorestframework-2.3.7/rest_framework/renderers.py djangorestframework-2.3.12/rest_framework/renderers.py --- djangorestframework-2.3.7/rest_framework/renderers.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/renderers.py 2014-01-15 14:27:41.000000000 +0000 @@ -20,12 +20,12 @@ 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 clone_request +from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.formatting import get_view_name, get_view_description -from rest_framework import exceptions, parsers, status, VERSION +from rest_framework import exceptions, status, VERSION class BaseRenderer(object): @@ -37,6 +37,7 @@ media_type = None format = None charset = 'utf-8' + render_style = 'text' def render(self, data, accepted_media_type=None, renderer_context=None): raise NotImplemented('Renderer class requires .render() to be implemented') @@ -52,16 +53,17 @@ format = 'json' encoder_class = encoders.JSONEncoder ensure_ascii = True - charset = 'utf-8' - # Note that JSON encodings must be utf-8, utf-16 or utf-32. + charset = None + # JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32. # See: http://www.ietf.org/rfc/rfc4627.txt + # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/ def render(self, data, accepted_media_type=None, renderer_context=None): """ Render `data` into JSON. """ if data is None: - return '' + return bytes() # If 'indent' is provided in the context, then pretty print the result. # E.g. If we're being called by the BrowsableAPIRenderer. @@ -86,13 +88,12 @@ # and may (or may not) be unicode. # On python 3.x json.dumps() returns unicode strings. if isinstance(ret, six.text_type): - return bytes(ret.encode(self.charset)) + return bytes(ret.encode('utf-8')) return ret class UnicodeJSONRenderer(JSONRenderer): ensure_ascii = False - charset = 'utf-8' """ Renderer which serializes to JSON. Does *not* apply JSON's character escaping for non-ascii characters. @@ -109,6 +110,7 @@ format = 'jsonp' callback_parameter = 'callback' default_callback = 'callback' + charset = 'utf-8' def get_callback(self, renderer_context): """ @@ -271,7 +273,9 @@ return [self.template_name] elif hasattr(view, 'get_template_names'): return view.get_template_names() - raise ImproperlyConfigured('Returned a template response with no template_name') + elif hasattr(view, 'template_name'): + return [view.template_name] + raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response') def get_exception_template(self, response): template_names = [name % {'status_code': response.status_code} @@ -317,6 +321,34 @@ return data +class HTMLFormRenderer(BaseRenderer): + """ + Renderers serializer data into an HTML form. + + If the serializer was instantiated without an object then this will + return an HTML form not bound to any object, + otherwise it will return an HTML form with the appropriate initial data + populated from the object. + + Note that rendering of field and form errors is not currently supported. + """ + media_type = 'text/html' + format = 'form' + template = 'rest_framework/form.html' + charset = 'utf-8' + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render serializer data and return an HTML form, as a string. + """ + renderer_context = renderer_context or {} + request = renderer_context['request'] + + template = loader.get_template(self.template) + context = RequestContext(request, {'form': data}) + return template.render(context) + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. @@ -325,6 +357,7 @@ format = 'api' template = 'rest_framework/api.html' charset = 'utf-8' + form_renderer_class = HTMLFormRenderer def get_default_renderer(self, view): """ @@ -333,8 +366,13 @@ """ renderers = [renderer for renderer in view.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer)] + non_template_renderers = [renderer for renderer in renderers + if not hasattr(renderer, 'get_template_names')] + if not renderers: return None + elif non_template_renderers: + return non_template_renderers[0]() return renderers[0]() def get_content(self, renderer, data, @@ -349,7 +387,10 @@ renderer_context['indent'] = 4 content = renderer.render(data, accepted_media_type, renderer_context) - if renderer.charset is None: + render_style = getattr(renderer, 'render_style', 'text') + assert render_style in ['text', 'binary'], 'Expected .render_style ' \ + '"text" or "binary", but got "%s"' % render_style + if render_style == 'binary': return '[%d bytes of binary content]' % len(content) return content @@ -372,202 +413,180 @@ return False # Doesn't have permissions return True - def serializer_to_form_fields(self, serializer): - fields = {} - for k, v in serializer.get_fields().items(): - if getattr(v, 'read_only', True): - continue - - kwargs = {} - kwargs['required'] = v.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(v, 'choices', None) is not None: - kwargs['choices'] = v.choices - - if getattr(v, 'regex', None) is not None: - kwargs['regex'] = v.regex - - if getattr(v, 'widget', None): - widget = copy.deepcopy(v.widget) - kwargs['widget'] = widget - - if getattr(v, 'default', None) is not None: - kwargs['initial'] = v.default - - if getattr(v, 'label', None) is not None: - kwargs['label'] = v.label - - if getattr(v, 'help_text', None) is not None: - kwargs['help_text'] = v.help_text - - fields[k] = v.form_field_class(**kwargs) - - return fields - - def _get_form(self, view, method, request): - # We need to impersonate a request with the correct method, - # so that eg. any dynamic get_serializer_class methods return the - # correct form for each method. - restore = view.request - request = clone_request(request, method) - view.request = request - try: - return self.get_form(view, method, request) - finally: - view.request = restore - - def _get_raw_data_form(self, view, method, request, media_types): - # We need to impersonate a request with the correct method, - # so that eg. any dynamic get_serializer_class methods return the - # correct form for each method. - restore = view.request - request = clone_request(request, method) - view.request = request - try: - return self.get_raw_data_form(view, method, request, media_types) - finally: - view.request = restore - - def get_form(self, view, method, request): + def get_rendered_html_form(self, view, method, request): """ - Get a form, possibly bound to either the input or output data. - In the absence on of the Resource having an associated form then - provide a form that can be used to submit arbitrary content. + Return a string representing a rendered HTML form, possibly bound to + either the input or output data. + + In the absence of the View having an associated form then return None. """ - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): - return + if request.method == method: + try: + data = request.DATA + files = request.FILES + except ParseError: + data = None + files = None + else: + data = None + files = None - if method in ('DELETE', 'OPTIONS'): - return True # Don't actually need to return a form + with override_method(view, request, method) as request: + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): + return - if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: - return + if method in ('DELETE', 'OPTIONS'): + return True # Don't actually need to return a form - serializer = view.get_serializer(instance=obj) - fields = self.serializer_to_form_fields(serializer) + if (not getattr(view, 'get_serializer', None) + or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): + return - # Creating an on the fly form see: - # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields) - data = (obj is not None) and serializer.data or None - form_instance = OnTheFlyForm(data) - return form_instance + serializer = view.get_serializer(instance=obj, data=data, files=files) + serializer.is_valid() + data = serializer.data - def get_raw_data_form(self, view, method, request, media_types): + form_renderer = self.form_renderer_class() + return form_renderer.render(data, self.accepted_media_type, self.renderer_context) + + def get_raw_data_form(self, view, method, request): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms. (Which are typically application/x-www-form-urlencoded) """ + with override_method(view, request, method) as request: + # If we're not using content overloading there's no point in + # supplying a generic form, as the view won't treat the form's + # value as the content of the request. + if not (api_settings.FORM_CONTENT_OVERRIDE + and api_settings.FORM_CONTENTTYPE_OVERRIDE): + return None + + # Check permissions + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): + return + + # If possible, serialize the initial content for the generic form + default_parser = view.parser_classes[0] + renderer_class = getattr(default_parser, 'renderer_class', None) + if (hasattr(view, 'get_serializer') and renderer_class): + # View has a serializer defined and parser class has a + # corresponding renderer that can be used to render the data. + + # Get a read-only version of the serializer + serializer = view.get_serializer(instance=obj) + if obj is None: + for name, field in serializer.fields.items(): + if getattr(field, 'read_only', None): + del serializer.fields[name] + + # Render the raw data content + renderer = renderer_class() + accepted = self.accepted_media_type + context = self.renderer_context.copy() + context['indent'] = 4 + content = renderer.render(serializer.data, accepted, context) + else: + content = None + + # Generate a generic form that includes a content type field, + # and a content field. + content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE + content_field = api_settings.FORM_CONTENT_OVERRIDE + + media_types = [parser.media_type for parser in view.parser_classes] + choices = [(media_type, media_type) for media_type in media_types] + initial = media_types[0] + + # NB. http://jacobian.org/writing/dynamic-form-generation/ + class GenericContentForm(forms.Form): + def __init__(self): + super(GenericContentForm, self).__init__() + + self.fields[content_type_field] = forms.ChoiceField( + label='Media type', + choices=choices, + initial=initial + ) + self.fields[content_field] = forms.CharField( + label='Content', + widget=forms.Textarea, + initial=content + ) - # If we're not using content overloading there's no point in supplying a generic form, - # as the view won't treat the form's value as the content of the request. - if not (api_settings.FORM_CONTENT_OVERRIDE - and api_settings.FORM_CONTENTTYPE_OVERRIDE): - return None - - # Check permissions - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): - return - - content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE - content_field = api_settings.FORM_CONTENT_OVERRIDE - choices = [(media_type, media_type) for media_type in media_types] - initial = media_types[0] - - # NB. http://jacobian.org/writing/dynamic-form-generation/ - class GenericContentForm(forms.Form): - def __init__(self): - super(GenericContentForm, self).__init__() - - self.fields[content_type_field] = forms.ChoiceField( - label='Media type', - choices=choices, - initial=initial - ) - self.fields[content_field] = forms.CharField( - label='Content', - widget=forms.Textarea - ) - - return GenericContentForm() + return GenericContentForm() def get_name(self, view): - return get_view_name(view.__class__, getattr(view, 'suffix', None)) + return view.get_view_name() def get_description(self, view): - return get_view_description(view.__class__, html=True) + return view.get_view_description(html=True) def get_breadcrumbs(self, request): return get_breadcrumbs(request.path) - def render(self, data, accepted_media_type=None, renderer_context=None): + def get_context(self, data, accepted_media_type, renderer_context): """ - Render the HTML for the browsable API representation. + Returns the context used to render. """ - accepted_media_type = accepted_media_type or '' - renderer_context = renderer_context or {} - view = renderer_context['view'] request = renderer_context['request'] response = renderer_context['response'] - media_types = [parser.media_type for parser in view.parser_classes] renderer = self.get_default_renderer(view) - content = self.get_content(renderer, data, accepted_media_type, renderer_context) - put_form = self._get_form(view, 'PUT', request) - post_form = self._get_form(view, 'POST', request) - patch_form = self._get_form(view, 'PATCH', request) - delete_form = self._get_form(view, 'DELETE', request) - options_form = self._get_form(view, 'OPTIONS', request) - - raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types) - raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types) - raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types) + raw_data_post_form = self.get_raw_data_form(view, 'POST', request) + raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) + raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form - name = self.get_name(view) - description = self.get_description(view) - breadcrumb_list = self.get_breadcrumbs(request) - - template = loader.get_template(self.template) - context = RequestContext(request, { - 'content': content, + context = { + 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, 'request': request, 'response': response, - 'description': description, - 'name': name, + 'description': self.get_description(view), + 'name': self.get_name(view), 'version': VERSION, - 'breadcrumblist': breadcrumb_list, + 'breadcrumblist': self.get_breadcrumbs(request), 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], - 'put_form': put_form, - 'post_form': post_form, - 'patch_form': patch_form, - 'delete_form': delete_form, - 'options_form': options_form, + 'put_form': self.get_rendered_html_form(view, 'PUT', request), + 'post_form': self.get_rendered_html_form(view, 'POST', request), + 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), + 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, 'raw_data_post_form': raw_data_post_form, 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, + 'display_edit_forms': bool(response.status_code != 403), + 'api_settings': api_settings - }) + } + return context + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render the HTML for the browsable API representation. + """ + self.accepted_media_type = accepted_media_type or '' + self.renderer_context = renderer_context or {} + template = loader.get_template(self.template) + context = self.get_context(data, accepted_media_type, renderer_context) + context = RequestContext(renderer_context['request'], context) ret = template.render(context) # Munge DELETE Response code to allow us to return content # (Do this *after* we've rendered the template so that we include # the normal deletion response code in the output) + response = renderer_context['response'] if response.status_code == status.HTTP_204_NO_CONTENT: response.status_code = status.HTTP_200_OK @@ -582,3 +601,4 @@ def render(self, data, accepted_media_type=None, renderer_context=None): return encode_multipart(self.BOUNDARY, data) + diff -Nru djangorestframework-2.3.7/rest_framework/request.py djangorestframework-2.3.12/rest_framework/request.py --- djangorestframework-2.3.7/rest_framework/request.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/request.py 2014-01-15 14:27:41.000000000 +0000 @@ -28,6 +28,29 @@ base_media_type == 'multipart/form-data') +class override_method(object): + """ + A context manager that temporarily overrides the method on a request, + additionally setting the `view.request` attribute. + + Usage: + + with override_method(view, request, 'POST') as request: + ... # Do stuff with `view` and `request` + """ + def __init__(self, view, request, method): + self.view = view + self.request = request + self.method = method + + def __enter__(self): + self.view.request = clone_request(self.request, self.method) + return self.view.request + + def __exit__(self, *args, **kwarg): + self.view.request = self.request + + class Empty(object): """ Placeholder for unset attributes. @@ -200,7 +223,7 @@ def user(self, value): """ Sets the user on the current request. This is necessary to maintain - compatilbility with django.contrib.auth where the user proprety is + compatibility with django.contrib.auth where the user property is set in the login and logout functions. """ self._user = value @@ -311,7 +334,7 @@ self._CONTENT_PARAM in self._data and self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING)) + self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._data, self._files = (Empty, Empty) def _parse(self): @@ -333,7 +356,16 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/response.py djangorestframework-2.3.12/rest_framework/response.py --- djangorestframework-2.3.7/rest_framework/response.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/response.py 2014-01-15 14:27:41.000000000 +0000 @@ -61,6 +61,10 @@ 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 -Nru djangorestframework-2.3.7/rest_framework/routers.py djangorestframework-2.3.12/rest_framework/routers.py --- djangorestframework-2.3.7/rest_framework/routers.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/routers.py 2014-01-15 14:27:41.000000000 +0000 @@ -184,14 +184,24 @@ 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. - """ - base_regex = '(?P<{lookup_field}>[^/]+)' + + 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_prefix}{lookup_field}>[^/]+)' + else: + # Don't consume `.json` style suffixes + 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 -Nru djangorestframework-2.3.7/rest_framework/runtests/settings.py djangorestframework-2.3.12/rest_framework/runtests/settings.py --- djangorestframework-2.3.7/rest_framework/runtests/settings.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/runtests/settings.py 2014-01-15 14:27:41.000000000 +0000 @@ -100,6 +100,9 @@ 'rest_framework', 'rest_framework.authtoken', 'rest_framework.tests', + 'rest_framework.tests.accounts', + 'rest_framework.tests.records', + 'rest_framework.tests.users', ) # OAuth is optional and won't work if there is no oauth_provider & oauth2 @@ -123,6 +126,21 @@ 'provider.oauth2', ) +# guardian is optional +try: + import guardian +except ImportError: + pass +else: + ANONYMOUS_USER_ID = -1 + AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', # default + 'guardian.backends.ObjectPermissionBackend', + ) + INSTALLED_APPS += ( + 'guardian', + ) + STATIC_URL = '/static/' PASSWORD_HASHERS = ( diff -Nru djangorestframework-2.3.7/rest_framework/serializers.py djangorestframework-2.3.12/rest_framework/serializers.py --- djangorestframework-2.3.7/rest_framework/serializers.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/serializers.py 2014-01-15 14:27:41.000000000 +0000 @@ -6,13 +6,14 @@ 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 import copy import datetime +import inspect import types from decimal import Decimal from django.core.paginator import Page @@ -20,6 +21,8 @@ from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model, six +from rest_framework.settings import api_settings + # Note: We do the following so that users of the framework can use this style: # @@ -32,6 +35,38 @@ from rest_framework.fields import * +def _resolve_model(obj): + """ + Resolve supplied `obj` to a Django model class. + + `obj` must be a Django model class itself, or a string + representation of one. Useful in situtations like GH #1225 where + Django may not have resolved a string-based reference to a model in + another model's foreign key definition. + + String representations should have the format: + 'appname.ModelName' + """ + if type(obj) == str and len(obj.split('.')) == 2: + app_name, model_name = obj.split('.') + return models.get_model(app_name, model_name) + elif inspect.isclass(obj) and issubclass(obj, models.Model): + return obj + else: + raise ValueError("{0} is not a Django model".format(obj)) + + +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return '' + return name.replace('_', ' ').capitalize() + + +class RelationsList(list): + _deleted = [] + + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -46,9 +81,13 @@ 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): @@ -161,7 +200,6 @@ self._data = None self._files = None self._errors = None - self._deleted = None if many and instance is not None and not hasattr(instance, '__iter__'): raise ValueError('instance should be a queryset or other iterable with many=True') @@ -253,10 +291,13 @@ for field_name, field in self.fields.items(): if field_name in self._errors: continue + + source = field.source or field_name + if self.partial and source not in attrs: + continue try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: - source = field.source or field_name attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) @@ -298,21 +339,29 @@ Serialize objects -> primitives. """ ret = self._dict_class() - ret.fields = {} + ret.fields = self._dict_class() for field_name, field in self.fields.items(): + if field.read_only and obj is None: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) - ret[key] = value - ret.fields[key] = field + method = getattr(self, 'transform_%s' % field_name, None) + if callable(method): + value = method(obj, value) + if not getattr(field, 'write_only', False): + ret[key] = value + ret.fields[key] = self.augment_field(field, field_name, key, value) + return ret - def from_native(self, data, files): + def from_native(self, data, files=None): """ Deserialize primitives -> objects. """ self._errors = {} + if data is not None or files is not None: attrs = self.restore_fields(data, files) if attrs is not None: @@ -323,22 +372,35 @@ if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + def field_to_native(self, obj, field_name): """ Override default so that the serializer can be used as a nested field across relationships. """ + if self.write_only: + return None + if self.source == '*': return self.to_native(obj) + # Get the raw field value try: source = self.source or field_name value = obj for component in source.split('.'): - value = get_component(value, component) if value is None: break + value = get_component(value, component) except ObjectDoesNotExist: return None @@ -377,11 +439,20 @@ return # Set the serializer object if it exists - obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # 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 @@ -391,7 +462,8 @@ 'data': value, 'context': self.context, 'partial': self.partial, - 'many': self.many + 'many': self.many, + 'allow_add_remove': self.allow_add_remove } serializer = self.__class__(**kwargs) @@ -434,7 +506,7 @@ DeprecationWarning, stacklevel=3) if many: - ret = [] + ret = RelationsList() errors = [] update = self.object is not None @@ -461,8 +533,8 @@ ret.append(self.from_native(item, None)) errors.append(self._errors) - if update: - self._deleted = identity_to_objects.values() + if update and self.allow_add_remove: + ret._deleted = identity_to_objects.values() self._errors = any(errors) and errors or [] else: @@ -512,14 +584,17 @@ """ Save the deserialized object and return it. """ + # Clear cached _data, which may be invalidated by `save()` + self._data = None + if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] + + if self.object._deleted: + [self.delete_object(item) for item in self.object._deleted] else: self.save_object(self.object, **kwargs) - if self.allow_add_remove and self._deleted: - [self.delete_object(item) for item in self._deleted] - return self.object def metadata(self): @@ -546,6 +621,7 @@ super(ModelSerializerOptions, self).__init__(meta) self.model = getattr(meta, 'model', None) self.read_only_fields = getattr(meta, 'read_only_fields', ()) + self.write_only_fields = getattr(meta, 'write_only_fields', ()) class ModelSerializer(Serializer): @@ -572,6 +648,7 @@ models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, models.FileField: FileField, models.ImageField: ImageField, } @@ -608,7 +685,7 @@ if model_field.rel: to_many = isinstance(model_field, models.fields.related.ManyToManyField) - related_model = model_field.rel.to + related_model = _resolve_model(model_field.rel.to) if to_many and not model_field.rel.through._meta.auto_created: has_through_model = True @@ -665,7 +742,9 @@ 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: @@ -682,17 +761,29 @@ # Add the `read_only` flag to any fields that have bee specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: - assert field_name not in self.base_fields.keys(), \ - "field '%s' on serializer '%s' specified in " \ - "`read_only_fields`, but also added " \ - "as an explicit field. Remove it from `read_only_fields`." % \ - (field_name, self.__class__.__name__) - assert field_name in ret, \ - "Non-existant field '%s' specified in `read_only_fields` " \ - "on serializer '%s'." % \ - (field_name, self.__class__.__name__) + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`read_only_fields`, but also added " + "as an explicit field. Remove it from `read_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `read_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) ret[field_name].read_only = True + for field_name in self.opts.write_only_fields: + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`write_only_fields`, but also added " + "as an explicit field. Remove it from `write_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `write_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) + ret[field_name].write_only = True + return ret def get_pk_field(self, model_field): @@ -760,6 +851,8 @@ # 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 @@ -795,9 +888,12 @@ cls = self.opts.model opts = get_concrete_model(cls)._meta exclusions = [field.name for field in opts.fields + opts.many_to_many] + for field_name, field in self.fields.items(): field_name = field.source or field_name - if field_name in exclusions and not field.read_only: + if field_name in exclusions \ + and not field.read_only \ + and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -823,29 +919,39 @@ """ m2m_data = {} related_data = {} + nested_forward_relations = {} meta = self.opts.model._meta # 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) + # Nested forward relations - These need to be marked so we can save + # them before saving the parent model instance. + for field_name in attrs.keys(): + if isinstance(self.fields.get(field_name, None), Serializer): + nested_forward_relations[field_name] = attrs[field_name] + # 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: @@ -857,6 +963,7 @@ # at the point of save. instance._related_data = related_data instance._m2m_data = m2m_data + instance._nested_forward_relations = nested_forward_relations return instance @@ -870,8 +977,16 @@ def save_object(self, obj, **kwargs): """ - Save the deserialized object and return it. + Save the deserialized object. """ + if getattr(obj, '_nested_forward_relations', None): + # Nested relationships need to be saved before we can save the + # parent instance. + for field_name, sub_object in obj._nested_forward_relations.items(): + if sub_object: + self.save_object(sub_object) + setattr(obj, field_name, sub_object) + obj.save(**kwargs) if getattr(obj, '_m2m_data', None): @@ -880,8 +995,31 @@ 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(): - setattr(obj, accessor_name, related) + if isinstance(related, RelationsList): + # Nested reverse fk relationship + for related_item in related: + fk_field = related_fields[accessor_name].field.name + setattr(related_item, fk_field, obj) + self.save_object(related_item) + + # Delete any removed objects + if related._deleted: + [self.delete_object(item) for item in related._deleted] + + elif isinstance(related, models.Model): + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) + else: + # Reverse FK or reverse one-one + setattr(obj, accessor_name, related) del(obj._related_data) @@ -893,6 +1031,7 @@ super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) self.lookup_field = getattr(meta, 'lookup_field', None) + self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME) class HyperlinkedModelSerializer(ModelSerializer): @@ -903,6 +1042,7 @@ _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' _hyperlink_field_class = HyperlinkedRelatedField + _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() @@ -910,13 +1050,13 @@ if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) - if 'url' not in fields: - url_field = HyperlinkedIdentityField( + if self.opts.url_field_name not in fields: + url_field = self._hyperlink_identify_field_class( view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) ret = self._dict_class() - ret['url'] = url_field + ret[self.opts.url_field_name] = url_field ret.update(fields) fields = ret @@ -952,7 +1092,7 @@ We need to override the default, to use the url as the identity. """ try: - return data.get('url', None) + return data.get(self.opts.url_field_name, None) except AttributeError: return None diff -Nru djangorestframework-2.3.7/rest_framework/settings.py djangorestframework-2.3.12/rest_framework/settings.py --- djangorestframework-2.3.7/rest_framework/settings.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/settings.py 2014-01-15 14:27:41.000000000 +0000 @@ -48,7 +48,6 @@ ), 'DEFAULT_THROTTLE_CLASSES': ( ), - 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', @@ -68,11 +67,19 @@ # Pagination 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, + 'MAX_PAGINATE_BY': None, # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # View configuration + 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', + + # Exception handling + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + # Testing 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework.renderers.MultiPartRenderer', @@ -88,6 +95,7 @@ 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', + 'URL_FIELD_NAME': 'url', # Input and output formats 'DATE_INPUT_FORMATS': ( @@ -121,10 +129,13 @@ 'DEFAULT_MODEL_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_FILTER_BACKENDS', + 'EXCEPTION_HANDLER', 'FILTER_BACKEND', 'TEST_REQUEST_RENDERER_CLASSES', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', + 'VIEW_NAME_FUNCTION', + 'VIEW_DESCRIPTION_FUNCTION' ) diff -Nru djangorestframework-2.3.7/rest_framework/static/rest_framework/js/default.js djangorestframework-2.3.12/rest_framework/static/rest_framework/js/default.js --- djangorestframework-2.3.7/rest_framework/static/rest_framework/js/default.js 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/static/rest_framework/js/default.js 2014-01-15 14:27:41.000000000 +0000 @@ -1,13 +1,56 @@ +function getCookie(c_name) +{ + // From http://www.w3schools.com/js/js_cookies.asp + var c_value = document.cookie; + var c_start = c_value.indexOf(" " + c_name + "="); + if (c_start == -1) { + c_start = c_value.indexOf(c_name + "="); + } + if (c_start == -1) { + c_value = null; + } else { + c_start = c_value.indexOf("=", c_start) + 1; + var c_end = c_value.indexOf(";", c_start); + if (c_end == -1) { + c_end = c_value.length; + } + c_value = unescape(c_value.substring(c_start,c_end)); + } + return c_value; +} + +// JSON highlighting. prettyPrint(); +// Bootstrap tooltips. $('.js-tooltip').tooltip({ delay: 1000 }); +// Deal with rounded tab styling after tab clicks. $('a[data-toggle="tab"]:first').on('shown', function (e) { $(e.target).parents('.tabbable').addClass('first-tab-active'); }); $('a[data-toggle="tab"]:not(:first)').on('shown', function (e) { $(e.target).parents('.tabbable').removeClass('first-tab-active'); }); -$('.form-switcher a:first').tab('show'); + +$('a[data-toggle="tab"]').click(function(){ + document.cookie="tabstyle=" + this.name + "; path=/"; +}); + +// Store tab preference in cookies & display appropriate tab on load. +var selectedTab = null; +var selectedTabName = getCookie('tabstyle'); + +if (selectedTabName) { + selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); +} + +if (selectedTab && selectedTab.length > 0) { + // Display whichever tab is selected. + selectedTab.tab('show'); +} else { + // If no tab selected, display rightmost tab. + $('.form-switcher a:first').tab('show'); +} diff -Nru djangorestframework-2.3.7/rest_framework/status.py djangorestframework-2.3.12/rest_framework/status.py --- djangorestframework-2.3.7/rest_framework/status.py 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/status.py 2014-01-15 14:27:41.000000000 +0000 @@ -6,6 +6,23 @@ """ 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 -Nru djangorestframework-2.3.7/rest_framework/templates/rest_framework/base.html djangorestframework-2.3.12/rest_framework/templates/rest_framework/base.html --- djangorestframework-2.3.7/rest_framework/templates/rest_framework/base.html 2013-08-16 13:03:20.000000000 +0000 +++ djangorestframework-2.3.12/rest_framework/templates/rest_framework/base.html 2014-01-15 14:27:41.000000000 +0000 @@ -33,7 +33,7 @@