404
+Page not found
+Try the homepage, or search the documentation.
+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 :::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 @@
+
+
+
+
+
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 @@