mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-03 12:00:12 +03:00
commit
60338978e6
13
.travis.yml
13
.travis.yml
|
@ -7,20 +7,23 @@ python:
|
|||
- "3.3"
|
||||
|
||||
env:
|
||||
- DJANGO="django==1.6"
|
||||
- DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
|
||||
- DJANGO="django==1.6.2"
|
||||
- DJANGO="django==1.5.5"
|
||||
- DJANGO="django==1.4.10"
|
||||
- DJANGO="django==1.3.7"
|
||||
|
||||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install defusedxml==0.3
|
||||
- pip install defusedxml==0.3 Pillow==2.3.0
|
||||
- "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.0; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; 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"
|
||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
|
||||
- "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||
- "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||
- export PYTHONPATH=.
|
||||
|
||||
script:
|
||||
|
@ -28,6 +31,8 @@ script:
|
|||
|
||||
matrix:
|
||||
exclude:
|
||||
- python: "2.6"
|
||||
env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
|
||||
- python: "3.2"
|
||||
env: DJANGO="django==1.4.10"
|
||||
- python: "3.2"
|
||||
|
|
193
CONTRIBUTING.md
Normal file
193
CONTRIBUTING.md
Normal file
|
@ -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
|
38
README.md
38
README.md
|
@ -1,10 +1,10 @@
|
|||
# Django REST framework
|
||||
|
||||
**Awesome web-browseable Web APIs.**
|
||||
|
||||
[![build-status-image]][travis]
|
||||
|
||||
**Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs].
|
||||
**Awesome web-browseable Web APIs.**
|
||||
|
||||
**Note**: Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
|
||||
|
||||
# Overview
|
||||
|
||||
|
@ -99,7 +99,7 @@ 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.
|
||||
|
||||
|
@ -113,7 +113,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
|
||||
# License
|
||||
|
||||
Copyright (c) 2011-2013, Tom Christie
|
||||
Copyright (c) 2011-2014, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -143,21 +143,21 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
|
||||
[index]: http://django-rest-framework.org/
|
||||
[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
|
||||
[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
|
||||
[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
|
||||
[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
|
||||
[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
|
||||
[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
|
||||
[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
|
||||
[routers]: http://django-rest-framework.org/api-guide/routers.html
|
||||
[serializers]: http://django-rest-framework.org/api-guide/serializers.html
|
||||
[authentication]: http://django-rest-framework.org/api-guide/authentication.html
|
||||
[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/
|
||||
|
||||
|
@ -165,7 +165,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[wlonk]: https://twitter.com/wlonk/status/261689665952833536
|
||||
[laserllama]: https://twitter.com/laserllama/status/328688333750407168
|
||||
|
||||
[docs]: http://django-rest-framework.org/
|
||||
[docs]: http://www.django-rest-framework.org/
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[pyyaml]: http://pypi.python.org/pypi/PyYAML
|
||||
|
|
104
docs/404.html
104
docs/404.html
|
@ -3,17 +3,17 @@
|
|||
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<title>Django REST framework - 404 - Page not found</title>
|
||||
<link href="http://django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||
<link rel="canonical" href="http://django-rest-framework.org/404"/>
|
||||
<link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
|
||||
<link rel="canonical" href="http://www.django-rest-framework.org/404"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Django, API, REST, 404 - Page not found">
|
||||
<meta name="author" content="Tom Christie">
|
||||
|
||||
<!-- Le styles -->
|
||||
<link href="http://django-rest-framework.org/css/prettify.css" rel="stylesheet">
|
||||
<link href="http://django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
|
||||
<link href="http://django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
|
||||
<link href="http://django-rest-framework.org/css/default.css" rel="stylesheet">
|
||||
<link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
|
||||
<link href="http://www.django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
|
||||
<link href="http://www.django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
|
||||
<link href="http://www.django-rest-framework.org/css/default.css" rel="stylesheet">
|
||||
|
||||
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
|
@ -50,63 +50,63 @@
|
|||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<a class="brand" href="http://django-rest-framework.org">Django REST framework</a>
|
||||
<a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
|
||||
<div class="nav-collapse collapse">
|
||||
<ul class="nav">
|
||||
<li><a href="http://django-rest-framework.org">Home</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org">Home</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="http://django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
|
||||
<li><a href="http://django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="http://django-rest-framework.org/api-guide/requests">Requests</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/responses">Responses</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/views">Views</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/routers">Routers</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/parsers">Parsers</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/renderers">Renderers</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/serializers">Serializers</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/authentication">Authentication</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/permissions">Permissions</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/throttling">Throttling</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/filtering">Filtering</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/pagination">Pagination</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/testing">Testing</a></li>
|
||||
<li><a href="http://django-rest-framework.org/api-guide/settings">Settings</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/requests">Requests</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/responses">Responses</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/views">Views</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/routers">Routers</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/parsers">Parsers</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/renderers">Renderers</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/serializers">Serializers</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/authentication">Authentication</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/permissions">Permissions</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/throttling">Throttling</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/filtering">Filtering</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/pagination">Pagination</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/testing">Testing</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/api-guide/settings">Settings</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="http://django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/release-notes">Release Notes</a></li>
|
||||
<li><a href="http://django-rest-framework.org/topics/credits">Credits</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/release-notes">Release Notes</a></li>
|
||||
<li><a href="http://www.django-rest-framework.org/topics/credits">Credits</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -160,7 +160,7 @@
|
|||
<div id="main-content" class="span12">
|
||||
<h1 id="404-page-not-found" style="text-align: center">404</h1>
|
||||
<p style="text-align: center"><strong>Page not found</strong></p>
|
||||
<p style="text-align: center">Try the <a href="http://django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
|
||||
<p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
|
||||
</div><!--/span-->
|
||||
</div><!--/row-->
|
||||
</div><!--/.fluid-container-->
|
||||
|
@ -176,9 +176,9 @@
|
|||
<!-- Le javascript
|
||||
================================================== -->
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="http://django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
|
||||
<script src="http://django-rest-framework.org/js/prettify-1.0.js"></script>
|
||||
<script src="http://django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
|
||||
<script src="http://www.django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
|
||||
<script src="http://www.django-rest-framework.org/js/prettify-1.0.js"></script>
|
||||
<script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
|
||||
<script>
|
||||
//$('.side-nav').scrollspy()
|
||||
var shiftWindow = function() { scrollBy(0, -50) };
|
||||
|
|
|
@ -93,7 +93,7 @@ Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the author
|
|||
|
||||
If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`.
|
||||
|
||||
# this can go in either server config, virtual host, directory or .htaccess
|
||||
# this can go in either server config, virtual host, directory or .htaccess
|
||||
WSGIPassAuthorization On
|
||||
|
||||
---
|
||||
|
@ -117,7 +117,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
|||
|
||||
## TokenAuthentication
|
||||
|
||||
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
||||
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
||||
|
||||
To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
||||
|
||||
|
@ -125,7 +125,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y
|
|||
...
|
||||
'rest_framework.authtoken'
|
||||
)
|
||||
|
||||
|
||||
Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below).
|
||||
|
||||
You'll also need to create tokens for your users.
|
||||
|
@ -162,10 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
|
|||
|
||||
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
Token.objects.create(user=instance)
|
||||
|
@ -207,7 +209,7 @@ You can do so by inserting a `needed_by` attribute in your user migration:
|
|||
needed_by = (
|
||||
('authtoken', '0001_initial'),
|
||||
)
|
||||
|
||||
|
||||
def forwards(self):
|
||||
...
|
||||
|
||||
|
@ -280,7 +282,7 @@ Note that the `namespace='oauth2'` argument is required.
|
|||
Finally, sync your database.
|
||||
|
||||
python manage.py syncdb
|
||||
python manage.py migrate
|
||||
python manage.py migrate
|
||||
|
||||
---
|
||||
|
||||
|
@ -366,7 +368,7 @@ The following example will authenticate any incoming request as the user given b
|
|||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('No such user')
|
||||
|
||||
|
||||
return (user, None)
|
||||
|
||||
---
|
||||
|
@ -387,6 +389,18 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp
|
|||
|
||||
The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
|
||||
|
||||
## JSON Web Token Authentication
|
||||
|
||||
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
|
||||
|
||||
## Hawk HTTP Authentication
|
||||
|
||||
The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let you work with [Hawk][hawk] signed requests and responses in your API. [Hawk][hawk] lets two parties securely communicate with each other using messages signed by a shared key. It is based on [HTTP MAC access authentication][mac] (which was based on parts of [OAuth 1.0][oauth-1.0a]).
|
||||
|
||||
## HTTP Signature Authentication
|
||||
|
||||
HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
|
||||
|
||||
[cite]: http://jacobian.org/writing/rest-worst-practices/
|
||||
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
|
||||
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
|
||||
|
@ -411,3 +425,13 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
|
|||
[doac]: https://github.com/Rediker-Software/doac
|
||||
[rediker]: https://github.com/Rediker-Software
|
||||
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
|
||||
[blimp]: https://github.com/GetBlimp
|
||||
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
|
||||
[etoccalino]: https://github.com/etoccalino/
|
||||
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
|
||||
[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||
[http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
|
||||
[hawkrest]: http://hawkrest.readthedocs.org/en/latest/
|
||||
[hawk]: https://github.com/hueniverse/hawk
|
||||
[mohawk]: http://mohawk.readthedocs.org/en/latest/
|
||||
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
|
||||
|
|
|
@ -18,7 +18,7 @@ The handled exceptions are:
|
|||
|
||||
In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error.
|
||||
|
||||
By default all error responses will include a key `details` in the body of the response, but other keys may also be included.
|
||||
By default all error responses will include a key `detail` in the body of the response, but other keys may also be included.
|
||||
|
||||
For example, the following request:
|
||||
|
||||
|
@ -86,7 +86,15 @@ Note that the exception handler will only be called for responses generated by r
|
|||
|
||||
The **base class** for all exceptions raised inside REST framework.
|
||||
|
||||
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.
|
||||
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class.
|
||||
|
||||
For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so:
|
||||
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
class ServiceUnavailable(APIException):
|
||||
status_code = 503
|
||||
default_detail = 'Service temporarily unavailable, try again later.'
|
||||
|
||||
## ParseError
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Serializer fields
|
||||
|
||||
> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format.
|
||||
> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format.
|
||||
>
|
||||
> — [Django documentation][cite]
|
||||
|
||||
|
@ -28,7 +28,13 @@ Defaults to the name of the field.
|
|||
|
||||
### `read_only`
|
||||
|
||||
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
|
||||
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.
|
||||
|
||||
Defaults to `False`
|
||||
|
||||
### `write_only`
|
||||
|
||||
Set this to `True` to ensure that the field may be used when updating or creating an instance, but is not included when serializing the representation.
|
||||
|
||||
Defaults to `False`
|
||||
|
||||
|
@ -41,7 +47,7 @@ Defaults to `True`.
|
|||
|
||||
### `default`
|
||||
|
||||
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||
If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
|
||||
|
||||
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
|
||||
|
||||
|
@ -86,7 +92,7 @@ For example, using the following model.
|
|||
name = models.CharField(max_length=100)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
payment_expiry = models.DateTimeField()
|
||||
|
||||
|
||||
def has_expired(self):
|
||||
return now() > self.payment_expiry
|
||||
|
||||
|
@ -96,8 +102,9 @@ A serializer definition that looked like this:
|
|||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
expired = serializers.Field(source='has_expired')
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('url', 'owner', 'name', 'expired')
|
||||
|
||||
Would produce output similar to:
|
||||
|
@ -105,7 +112,7 @@ Would produce output similar to:
|
|||
{
|
||||
'url': 'http://example.com/api/accounts/3/',
|
||||
'owner': 'http://example.com/api/users/12/',
|
||||
'name': 'FooCorp business account',
|
||||
'name': 'FooCorp business account',
|
||||
'expired': True
|
||||
}
|
||||
|
||||
|
@ -119,7 +126,7 @@ A field that supports both read and write operations. By itself `WritableField`
|
|||
|
||||
## ModelField
|
||||
|
||||
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
||||
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to its associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
|
||||
|
||||
The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
|
||||
|
||||
|
@ -167,13 +174,13 @@ or `django.db.models.fields.TextField`.
|
|||
|
||||
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
|
||||
|
||||
**Signature:** `CharField(max_length=200, min_length=None)`
|
||||
**Signature:** `URLField(max_length=200, min_length=None)`
|
||||
|
||||
## SlugField
|
||||
|
||||
Corresponds to `django.db.models.fields.SlugField`.
|
||||
|
||||
**Signature:** `CharField(max_length=50, min_length=None)`
|
||||
**Signature:** `SlugField(max_length=50, min_length=None)`
|
||||
|
||||
## ChoiceField
|
||||
|
||||
|
@ -217,7 +224,7 @@ In the case of JSON this means the default datetime representation uses the [ECM
|
|||
|
||||
**Signature:** `DateTimeField(format=None, input_formats=None)`
|
||||
|
||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
||||
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
|
||||
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
|
||||
|
||||
DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
|
||||
|
@ -277,7 +284,7 @@ Corresponds to `django.forms.fields.FileField`.
|
|||
**Signature:** `FileField(max_length=None, allow_empty_file=False)`
|
||||
|
||||
- `max_length` designates the maximum length for the file name.
|
||||
|
||||
|
||||
- `allow_empty_file` designates if empty files are allowed.
|
||||
|
||||
## ImageField
|
||||
|
@ -286,7 +293,7 @@ An image representation.
|
|||
|
||||
Corresponds to `django.forms.fields.ImageField`.
|
||||
|
||||
Requires the `PIL` package.
|
||||
Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained.
|
||||
|
||||
Signature and validation is the same as with `FileField`.
|
||||
|
||||
|
@ -301,7 +308,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
|
|||
|
||||
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
|
||||
|
||||
The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation.
|
||||
The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into its initial representation.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -322,12 +329,12 @@ Let's look at an example of serializing a class that represents an RGB color val
|
|||
"""
|
||||
def to_native(self, obj):
|
||||
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
|
||||
|
||||
|
||||
def from_native(self, data):
|
||||
data = data.strip('rgb(').rstrip(')')
|
||||
red, green, blue = [int(col) for col in data.split(',')]
|
||||
return Color(red, green, blue)
|
||||
|
||||
|
||||
|
||||
By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`.
|
||||
|
||||
|
@ -340,8 +347,17 @@ As an example, let's create a field that can be used represent the class name of
|
|||
"""
|
||||
return obj.__class__
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF Compound Fields
|
||||
|
||||
The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
||||
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
|
||||
|
|
|
@ -264,13 +264,17 @@ For example:
|
|||
|
||||
search_fields = ('=username', '=email')
|
||||
|
||||
By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting.
|
||||
|
||||
For more details, see the [Django documentation][search-django-admin].
|
||||
|
||||
---
|
||||
|
||||
## OrderingFilter
|
||||
|
||||
The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example:
|
||||
The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting.
|
||||
|
||||
For example, to order users by username:
|
||||
|
||||
http://example.com/api/users?ordering=username
|
||||
|
||||
|
@ -282,13 +286,37 @@ Multiple orderings may also be specified:
|
|||
|
||||
http://example.com/api/users?ordering=account,username
|
||||
|
||||
### Specifying which fields may be ordered against
|
||||
|
||||
It's recommended that you explicitly specify which fields the API should allowing in the ordering filter. You can do this by setting an `ordering_fields` attribute on the view, like so:
|
||||
|
||||
class UserListView(generics.ListAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering_fields = ('username', 'email')
|
||||
|
||||
This helps prevent unexpected data leakage, such as allowing users to order against a password hash field or other sensitive data.
|
||||
|
||||
If you *don't* specify an `ordering_fields` attribute on the view, the filter class will default to allowing the user to filter on any readable fields on the serializer specified by the `serializer_class` attribute.
|
||||
|
||||
If you are confident that the queryset being used by the view doesn't contain any sensitive data, you can also explicitly specify that a view should allow ordering on *any* model field or queryset aggregate, by using the special value `'__all__'`.
|
||||
|
||||
class BookingsListView(generics.ListAPIView):
|
||||
queryset = Booking.objects.all()
|
||||
serializer_class = BookingSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering_fields = '__all__'
|
||||
|
||||
### Specifying a default ordering
|
||||
|
||||
If an `ordering` attribute is set on the view, this will be used as the default ordering.
|
||||
|
||||
Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
|
||||
|
||||
class UserListView(generics.ListAPIView):
|
||||
queryset = User.objects.all()
|
||||
serializer = UserSerializer
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('username',)
|
||||
|
||||
|
@ -360,6 +388,14 @@ For example, you might need to restrict users to only being able to see objects
|
|||
|
||||
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages provide additional filter implementations.
|
||||
|
||||
## Django REST framework chain
|
||||
|
||||
The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
|
||||
[django-filter]: https://github.com/alex/django-filter
|
||||
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
|
||||
|
@ -368,3 +404,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views,
|
|||
[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
||||
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
|
||||
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
||||
[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain
|
||||
|
|
|
@ -119,7 +119,7 @@ For example:
|
|||
self.check_object_permissions(self.request, obj)
|
||||
return obj
|
||||
|
||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup.
|
||||
Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup.
|
||||
|
||||
#### `get_filter_backends(self)`
|
||||
|
||||
|
@ -163,12 +163,14 @@ For example:
|
|||
return 20
|
||||
return 100
|
||||
|
||||
**Save hooks**:
|
||||
**Save / deletion hooks**:
|
||||
|
||||
The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes.
|
||||
|
||||
* `pre_save(self, obj)` - A hook that is called before saving an object.
|
||||
* `post_save(self, obj, created=False)` - A hook that is called after saving an object.
|
||||
* `pre_delete(self, obj)` - A hook that is called before deleting an object.
|
||||
* `post_delete(self, obj)` - A hook that is called after deleting an object.
|
||||
|
||||
The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
|
||||
|
||||
|
@ -360,11 +362,20 @@ If you are using a mixin across multiple views, you can take this a step further
|
|||
|
||||
Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||
# Third party packages
|
||||
|
||||
The following third party packages provide additional generic view implementations.
|
||||
|
||||
## Django REST Framework bulk
|
||||
|
||||
The [django-rest-framework-bulk package][django-rest-framework-bulk] implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
|
||||
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
|
||||
[GenericAPIView]: #genericapiview
|
||||
[ListModelMixin]: #listmodelmixin
|
||||
[CreateModelMixin]: #createmodelmixin
|
||||
[RetrieveModelMixin]: #retrievemodelmixin
|
||||
[UpdateModelMixin]: #updatemodelmixin
|
||||
[DestroyModelMixin]: #destroymodelmixin
|
||||
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
|
||||
|
|
|
@ -147,4 +147,14 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use
|
|||
pagination_serializer_class = CustomPaginationSerializer
|
||||
paginate_by = 10
|
||||
|
||||
# Third party packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF-extensions
|
||||
|
||||
The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
|
||||
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
|
@ -186,9 +186,15 @@ The following third party packages are also available.
|
|||
|
||||
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
|
||||
|
||||
## CamelCase JSON
|
||||
|
||||
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
|
||||
|
||||
[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
|
||||
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
|
||||
[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers
|
||||
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[vbabiy]: https://github.com/vbabiy
|
||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||
[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
|
|
@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per
|
|||
|
||||
The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
|
||||
|
||||
## REST Condition
|
||||
|
||||
The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
|
||||
|
||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||
[authentication]: authentication.md
|
||||
[throttling]: throttling.md
|
||||
|
@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t
|
|||
[filtering]: filtering.md
|
||||
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
|
||||
[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
|
||||
[rest-condition]: https://github.com/caxap/rest_condition
|
||||
|
|
|
@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o
|
|||
For example, the following serializer.
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
tracks = RelatedField(many=True)
|
||||
tracks = serializers.RelatedField(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
|
@ -134,7 +134,7 @@ By default this field is read-write, although you can change this behavior using
|
|||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `<modelname>-detail`. **required**.
|
||||
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
|
||||
* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
|
||||
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
|
||||
|
@ -202,7 +202,7 @@ This field is always read-only.
|
|||
|
||||
**Arguments**:
|
||||
|
||||
* `view_name` - The view name that should be used as the target of the relationship. **required**.
|
||||
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `<model_name>-detail`. **required**.
|
||||
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
|
||||
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
|
||||
|
||||
|
@ -442,7 +442,19 @@ In the 2.4 release, these parts of the API will be removed entirely.
|
|||
|
||||
For more details see the [2.2 release announcement][2.2-announcement].
|
||||
|
||||
---
|
||||
|
||||
# Third Party Packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF Nested Routers
|
||||
|
||||
The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
|
||||
|
||||
[cite]: http://lwn.net/Articles/193245/
|
||||
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
|
||||
[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
|
||||
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
|
||||
[2.2-announcement]: ../topics/2.2-announcement.md
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
|
|
|
@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan
|
|||
|
||||
The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
|
||||
|
||||
**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
|
||||
---
|
||||
|
||||
**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
|
||||
|
||||
The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
|
||||
|
||||
---
|
||||
|
||||
**.media_type**: `application/javascript`
|
||||
|
||||
|
@ -167,14 +173,14 @@ The template name is determined by (in order of preference):
|
|||
|
||||
An example of a view that uses `TemplateHTMLRenderer`:
|
||||
|
||||
class UserDetail(generics.RetrieveUserAPIView):
|
||||
class UserDetail(generics.RetrieveAPIView):
|
||||
"""
|
||||
A view that returns a templated HTML representations of a given user.
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
renderer_classes = (TemplateHTMLRenderer,)
|
||||
|
||||
def get(self, request, *args, **kwargs)
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return Response({'user': self.object}, template_name='user_detail.html')
|
||||
|
||||
|
@ -413,12 +419,18 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
|
||||
[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package.
|
||||
|
||||
## CamelCase JSON
|
||||
|
||||
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
|
||||
|
||||
|
||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||
[conneg]: content-negotiation.md
|
||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||
[cors]: http://www.w3.org/TR/cors/
|
||||
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||
[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
|
||||
[testing]: testing.md
|
||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
|
@ -428,8 +440,10 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
[messagepack]: http://msgpack.org/
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[mjumbewu]: https://github.com/mjumbewu
|
||||
[vbabiy]: https://github.com/vbabiy
|
||||
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
|
||||
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
|
||||
[ultrajson]: https://github.com/esnme/ultrajson
|
||||
[hzy]: https://github.com/hzy
|
||||
[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
|
||||
[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
|
|
@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo
|
|||
|
||||
## Usage
|
||||
|
||||
Here's an example of a simple URL conf, that uses `DefaultRouter`.
|
||||
Here's an example of a simple URL conf, that uses `SimpleRouter`.
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
|
@ -37,6 +37,18 @@ The example above would generate the following URL patterns:
|
|||
* URL pattern: `^accounts/$` Name: `'account-list'`
|
||||
* URL pattern: `^accounts/{pk}/$` Name: `'account-detail'`
|
||||
|
||||
---
|
||||
|
||||
**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part.
|
||||
|
||||
Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have any `.model` or `.queryset` attribute set. If you try to register that viewset you'll see an error like this:
|
||||
|
||||
'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.model' or '.queryset' attribute.
|
||||
|
||||
This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name.
|
||||
|
||||
---
|
||||
|
||||
### Extra link and actions
|
||||
|
||||
Any methods on the viewset decorated with `@link` or `@action` will also be routed.
|
||||
|
@ -150,4 +162,24 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an
|
|||
|
||||
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
|
||||
|
||||
# Third Party Packages
|
||||
|
||||
The following third party packages are also available.
|
||||
|
||||
## DRF Nested Routers
|
||||
|
||||
The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
|
||||
|
||||
## wq.db
|
||||
|
||||
The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration.
|
||||
|
||||
from wq.db.rest import app
|
||||
from myapp.models import MyModel
|
||||
|
||||
app.router.register_model(MyModel)
|
||||
|
||||
[cite]: http://guides.rubyonrails.org/routing.html
|
||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||
[wq.db]: http://wq.io/wq.db
|
||||
[wq.db-router]: http://wq.io/docs/app.py
|
||||
|
|
|
@ -103,11 +103,11 @@ Deserialization is similar. First we parse a stream into Python native datatype
|
|||
When deserializing data, we can either create a new instance, or update an existing instance.
|
||||
|
||||
serializer = CommentSerializer(data=data) # Create new instance
|
||||
serializer = CommentSerializer(comment, data=data) # Update `instance`
|
||||
serializer = CommentSerializer(comment, data=data) # Update `comment`
|
||||
|
||||
By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
|
||||
|
||||
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
|
||||
serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `comment` with partial data
|
||||
|
||||
## Validation
|
||||
|
||||
|
@ -161,7 +161,7 @@ To do any other validation that requires access to multiple fields, add a method
|
|||
"""
|
||||
Check that the start is before the stop.
|
||||
"""
|
||||
if attrs['start'] < attrs['finish']:
|
||||
if attrs['start'] > attrs['finish']:
|
||||
raise serializers.ValidationError("finish must occur after start")
|
||||
return attrs
|
||||
|
||||
|
@ -208,7 +208,7 @@ Similarly if a nested representation should be a list of items, you should pass
|
|||
|
||||
Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object.
|
||||
|
||||
serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
|
||||
serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
|
||||
serializer.is_valid()
|
||||
# False
|
||||
serializer.errors
|
||||
|
@ -373,6 +373,25 @@ You may wish to specify multiple fields as read-only. Instead of adding each fi
|
|||
|
||||
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
|
||||
|
||||
## Specifying which fields should be write-only
|
||||
|
||||
You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so:
|
||||
|
||||
class CreateUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('email', 'username', 'password')
|
||||
write_only_fields = ('password',) # Note: Password field is write-only
|
||||
|
||||
def restore_object(self, attrs, instance=None):
|
||||
"""
|
||||
Instantiate a new User instance.
|
||||
"""
|
||||
assert instance is None, 'Cannot update users with CreateUserSerializer'
|
||||
user = User(email=attrs['email'], username=attrs['username'])
|
||||
user.set_password(attrs['password'])
|
||||
return user
|
||||
|
||||
## Specifying fields explicitly
|
||||
|
||||
You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
|
||||
|
@ -425,7 +444,7 @@ You can change the field that is used for object lookups by setting the `lookup_
|
|||
fields = ('url', 'account_name', 'users', 'created')
|
||||
lookup_field = 'slug'
|
||||
|
||||
Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
||||
Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
|
||||
|
||||
For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
|
||||
|
||||
|
@ -445,6 +464,29 @@ For more specific requirements such as specifying a different lookup for each fi
|
|||
model = Account
|
||||
fields = ('url', 'account_name', 'users', 'created')
|
||||
|
||||
## Overiding the URL field behavior
|
||||
|
||||
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
|
||||
|
||||
You can also override this on a per-serializer basis by using the `url_field_name` option on the serializer, like so:
|
||||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('account_url', 'account_name', 'users', 'created')
|
||||
url_field_name = 'account_url'
|
||||
|
||||
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
|
||||
|
||||
You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
|
||||
|
||||
class AccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ('account_url', 'account_name', 'users', 'created')
|
||||
view_name = 'account_detail'
|
||||
lookup_field='account_name'
|
||||
|
||||
---
|
||||
|
||||
# Advanced serializer usage
|
||||
|
|
|
@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i
|
|||
|
||||
Default: `None`
|
||||
|
||||
### SEARCH_PARAM
|
||||
|
||||
The name of a query paramater, which can be used to specify the search term used by `SearchFilter`.
|
||||
|
||||
Default: `search`
|
||||
|
||||
#### ORDERING_PARAM
|
||||
|
||||
The name of a query paramater, which can be used to specify the ordering of results returned by `OrderingFilter`.
|
||||
|
||||
Default: `ordering`
|
||||
|
||||
---
|
||||
|
||||
## Authentication settings
|
||||
|
@ -353,6 +365,12 @@ This should be a function with the following signature:
|
|||
|
||||
Default: `'rest_framework.views.exception_handler'`
|
||||
|
||||
#### URL_FIELD_NAME
|
||||
|
||||
A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`.
|
||||
|
||||
Default: `'url'`
|
||||
|
||||
#### FORMAT_SUFFIX_KWARG
|
||||
|
||||
The name of a parameter in the URL conf that may be used to provide a format suffix.
|
||||
|
|
|
@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc
|
|||
|
||||
The full set of HTTP status codes included in the `status` module is listed below.
|
||||
|
||||
The module also includes a set of helper functions for testing if a status code is in a given range.
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
class ExampleTestCase(APITestCase):
|
||||
def test_url_root(self):
|
||||
url = reverse('index')
|
||||
response = self.client.get(url)
|
||||
self.assertTrue(status.is_success(response.status_code))
|
||||
|
||||
|
||||
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
|
||||
and [RFC 6585][rfc6585].
|
||||
|
||||
|
@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s
|
|||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
|
||||
|
||||
## Helper functions
|
||||
|
||||
The following helper functions are available for identifying the category of the response code.
|
||||
|
||||
is_informational() # 1xx
|
||||
is_success() # 2xx
|
||||
is_redirect() # 3xx
|
||||
is_client_error() # 4xx
|
||||
is_server_error() # 5xx
|
||||
|
||||
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
|
||||
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
|
|
|
@ -218,12 +218,12 @@ You can use any of REST framework's test case classes as you would for the regul
|
|||
|
||||
When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response.
|
||||
|
||||
For example, it's easier to inspect `request.data`:
|
||||
For example, it's easier to inspect `response.data`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
|
||||
|
||||
Instead of inspecting the result of parsing `request.content`:
|
||||
Instead of inspecting the result of parsing `response.content`:
|
||||
|
||||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
|
||||
|
|
|
@ -150,7 +150,7 @@ For example, given the following views...
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttling.ScopedRateThrottle'
|
||||
'rest_framework.throttling.ScopedRateThrottle',
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'contacts': '1000/day',
|
||||
|
|
|
@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o
|
|||
|
||||
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
|
||||
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
|
||||
[settings]: api-guide/settings.md
|
||||
[throttling]: api-guide/throttling.md
|
||||
[settings]: settings.md
|
||||
[throttling]: throttling.md
|
||||
|
|
|
@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`,
|
|||
|
||||
#### Example
|
||||
|
||||
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
|
||||
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example:
|
||||
|
||||
class AccountViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
|
@ -225,7 +225,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
|
|||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
A viewset that provides `retrieve`, `update`, and `list` actions.
|
||||
A viewset that provides `retrieve`, `create`, and `list` actions.
|
||||
|
||||
To use it, override the class and set the `.queryset` and
|
||||
`.serializer_class` attributes.
|
||||
|
|
BIN
docs/img/logo.png
Normal file
BIN
docs/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
docs/img/travis-status.png
Normal file
BIN
docs/img/travis-status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
|
@ -1,15 +1,30 @@
|
|||
<p class="badges">
|
||||
<p class="badges" height=20px>
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
|
||||
|
||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
|
||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
|
||||
|
||||
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
|
||||
</p>
|
||||
|
||||
# Django REST framework
|
||||
---
|
||||
|
||||
**Awesome web-browsable Web APIs.**
|
||||
<p>
|
||||
<h1 style="position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
border: 0;">Django REST Framework</h1>
|
||||
|
||||
<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
|
||||
</p>
|
||||
|
||||
<!--
|
||||
# Django REST framework
|
||||
-->
|
||||
|
||||
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
|
||||
|
||||
|
@ -20,13 +35,16 @@ Some reasons you might want to use REST framework:
|
|||
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
|
||||
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||
* [Extensive documentation][index], and [great community support][group].
|
||||
* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite].
|
||||
|
||||
There is a live example API for testing purposes, [available here][sandbox].
|
||||
|
||||
**Below**: *Screenshot from the browsable API*
|
||||
---
|
||||
|
||||
![Screenshot][image]
|
||||
|
||||
**Above**: *Screenshot from the browsable API*
|
||||
|
||||
----
|
||||
|
||||
## Requirements
|
||||
|
||||
REST framework requires the following:
|
||||
|
@ -62,7 +80,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
|
|||
|
||||
INSTALLED_APPS = (
|
||||
...
|
||||
'rest_framework',
|
||||
'rest_framework',
|
||||
)
|
||||
|
||||
If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
|
||||
|
@ -100,7 +118,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
|
|||
We're ready to create our API now.
|
||||
Here's our project's root `urls.py` module:
|
||||
|
||||
from django.conf.urls.defaults import url, patterns, include
|
||||
from django.conf.urls import url, patterns, include
|
||||
from django.contrib.auth.models import User, Group
|
||||
from rest_framework import viewsets, routers
|
||||
|
||||
|
@ -111,8 +129,8 @@ Here's our project's root `urls.py` module:
|
|||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
model = Group
|
||||
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'users', UserViewSet)
|
||||
router.register(r'groups', GroupViewSet)
|
||||
|
@ -140,6 +158,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
|
|||
* [5 - Relationships & hyperlinked APIs][tut-5]
|
||||
* [6 - Viewsets & routers][tut-6]
|
||||
|
||||
There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox].
|
||||
|
||||
## API Guide
|
||||
|
||||
The API guide is your complete reference manual to all the functionality provided by REST framework.
|
||||
|
@ -177,6 +197,7 @@ General guides to using REST framework.
|
|||
* [Browser enhancements][browser-enhancements]
|
||||
* [The Browsable API][browsableapi]
|
||||
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
|
||||
* [Contributing to REST framework][contributing]
|
||||
* [2.0 Announcement][rest-framework-2-announcement]
|
||||
* [2.2 Announcement][2.2-announcement]
|
||||
* [2.3 Announcement][2.3-announcement]
|
||||
|
@ -195,7 +216,7 @@ Run the tests:
|
|||
|
||||
./rest_framework/runtests/runtests.py
|
||||
|
||||
To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
|
||||
To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
|
||||
|
||||
tox
|
||||
|
||||
|
@ -218,32 +239,33 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
|||
|
||||
## License
|
||||
|
||||
Copyright (c) 2011-2013, Tom Christie
|
||||
Copyright (c) 2011-2014, Tom Christie
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
||||
[urlobject]: https://github.com/zacharyvoase/urlobject
|
||||
[mozilla]: http://www.mozilla.org/en-US/about/
|
||||
[eventbrite]: https://www.eventbrite.co.uk/about/
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[yaml]: http://pypi.python.org/pypi/PyYAML
|
||||
[defusedxml]: https://pypi.python.org/pypi/defusedxml
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
|
||||
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
|
||||
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
|
||||
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
|
||||
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
||||
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
|
||||
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
|
||||
|
@ -169,31 +170,12 @@
|
|||
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
|
||||
{{ toc }}
|
||||
<div>
|
||||
<hr>
|
||||
|
||||
<p><strong>The team behind REST framework are launching a new API service.</strong></p>
|
||||
{{ ad_block }}
|
||||
|
||||
<p>If you want to be first in line when we start issuing invitations, please sign up here:</p>
|
||||
|
||||
<!-- Begin MailChimp Signup Form -->
|
||||
<link href="//cdn-images.mailchimp.com/embedcode/slim-081711.css" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
|
||||
/* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
|
||||
We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
|
||||
</style>
|
||||
<div id="mc_embed_signup" style="background: rgb(245, 245, 245)">
|
||||
<form action="http://dabapps.us1.list-manage1.com/subscribe/post?u=cf73a9994eb5b8d8d461b5dfb&id=cb6af8e8bd" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
|
||||
<!-- <label for="mce-EMAIL">Keep me posted!</label>
|
||||
--> <input style="width: 90%" type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
|
||||
<div class="clear"><input class="btn btn-success" type="submit" value="Yes, keep me posted!" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
|
||||
</form>
|
||||
</div>
|
||||
</style></div>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
|
||||
<!--End mc_embed_signup-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re
|
|||
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
|
||||
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
|
||||
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
|
||||
[credits]: http://django-rest-framework.org/topics/credits
|
||||
[credits]: http://www.django-rest-framework.org/topics/credits
|
||||
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
|
||||
[marcgibbons]: https://github.com/marcgibbons/
|
||||
|
|
|
@ -6,50 +6,92 @@
|
|||
|
||||
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
|
||||
|
||||
# Community
|
||||
## Community
|
||||
|
||||
If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users.
|
||||
The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
|
||||
|
||||
You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter.
|
||||
If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
|
||||
|
||||
Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
|
||||
|
||||
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
|
||||
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
|
||||
|
||||
Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
|
||||
|
||||
The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
|
||||
|
||||
# Issues
|
||||
|
||||
It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
||||
It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
|
||||
|
||||
Some tips on good issue reporting:
|
||||
|
||||
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
||||
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
||||
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
|
||||
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
|
||||
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
|
||||
|
||||
## Triaging issues
|
||||
|
||||
Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
|
||||
|
||||
* TODO: Triage
|
||||
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
|
||||
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
|
||||
* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
|
||||
* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
|
||||
* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
|
||||
|
||||
# Development
|
||||
|
||||
To start developing on Django REST framework, clone the repo:
|
||||
|
||||
* git clone & PYTHONPATH
|
||||
* Pep8
|
||||
* Recommend editor that runs pep8
|
||||
git clone git@github.com:tomchristie/django-rest-framework.git
|
||||
|
||||
### Pull requests
|
||||
Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
|
||||
|
||||
* Make pull requests early
|
||||
* Describe branching
|
||||
## Testing
|
||||
|
||||
### Managing compatibility issues
|
||||
To run the tests, clone the repository, and then:
|
||||
|
||||
* Describe compat module
|
||||
# Setup the virtual environment
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -r optionals.txt
|
||||
|
||||
# Testing
|
||||
# Run the tests
|
||||
rest_framework/runtests/runtests.py
|
||||
|
||||
* Running the tests
|
||||
* tox
|
||||
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
|
||||
|
||||
|
@ -77,7 +119,7 @@ Some other tips:
|
|||
|
||||
* Keep paragraphs reasonably short.
|
||||
* Use double spacing after the end of sentences.
|
||||
* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'.
|
||||
* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
|
||||
|
||||
## Markdown style
|
||||
|
||||
|
@ -88,8 +130,8 @@ There are a couple of conventions you should follow when working on the document
|
|||
Headers should use the hash style. For example:
|
||||
|
||||
### Some important topic
|
||||
|
||||
The underline style should not be used. **Don't do this:**
|
||||
|
||||
The underline style should not be used. **Don't do this:**
|
||||
|
||||
Some important topic
|
||||
====================
|
||||
|
@ -99,9 +141,9 @@ The underline style should not be used. **Don't do this:**
|
|||
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.
|
||||
|
@ -117,26 +159,35 @@ Linking in this style means you'll be able to click the hyperlink in your markdo
|
|||
If you want to draw attention to a note or warning, use a pair of enclosing lines, like so:
|
||||
|
||||
---
|
||||
|
||||
**Note:** Make sure you do this thing.
|
||||
|
||||
|
||||
**Note:** A useful documentation note.
|
||||
|
||||
---
|
||||
|
||||
# Third party packages
|
||||
|
||||
* Django reusable app
|
||||
New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
|
||||
|
||||
# Core committers
|
||||
## Getting started
|
||||
|
||||
* Still use pull reqs
|
||||
* Credits
|
||||
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
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[django-packages]: https://www.djangopackages.com/grids/g/api/
|
||||
[code-of-conduct]: https://www.djangoproject.com/conduct/
|
||||
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
|
||||
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
|
||||
[pep-8]: http://www.python.org/dev/peps/pep-0008/
|
||||
[travis-status]: ../img/travis-status.png
|
||||
[pull-requests]: https://help.github.com/articles/using-pull-requests
|
||||
[tox]: http://tox.readthedocs.org/en/latest/
|
||||
[markdown]: http://daringfireball.net/projects/markdown/basics
|
||||
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
|
||||
[mou]: http://mouapp.com/
|
||||
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
|
||||
|
|
|
@ -179,6 +179,10 @@ The following people have helped make REST framework great.
|
|||
* Yamila Moreno - [yamila-moreno]
|
||||
* Rob Hudson - [robhudson]
|
||||
* Alex Good - [alexjg]
|
||||
* Ian Foote - [ian-foote]
|
||||
* Chuck Harmston - [chuckharmston]
|
||||
* Philip Forget - [philipforget]
|
||||
* Artem Mezhenin - [amezhenin]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -394,3 +398,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
|
|||
[yamila-moreno]: https://github.com/yamila-moreno
|
||||
[robhudson]: https://github.com/robhudson
|
||||
[alexjg]: https://github.com/alexjg
|
||||
[ian-foote]: https://github.com/ian-foote
|
||||
[chuckharmston]: https://github.com/chuckharmston
|
||||
[philipforget]: https://github.com/philipforget
|
||||
[amezhenin]: https://github.com/amezhenin
|
||||
|
|
|
@ -40,8 +40,45 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
## 2.3.x series
|
||||
|
||||
### Master
|
||||
### 2.3.13
|
||||
|
||||
**Date**: 6th March 2014
|
||||
|
||||
* Django 1.7 Support.
|
||||
* Fix `default` argument when used with serializer relation fields.
|
||||
* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'.
|
||||
* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`.
|
||||
* Use `urandom` for token generation.
|
||||
* Only use `Vary: Accept` when more than one renderer exists.
|
||||
|
||||
### 2.3.12
|
||||
|
||||
**Date**: 15th January 2014
|
||||
|
||||
* **Security fix**: `OrderingField` now only allows ordering on readable serializer fields, or on fields explicitly specified using `ordering_fields`. This prevents users being able to order by fields that are not visible in the API, and exploiting the ordering of sensitive data such as password hashes.
|
||||
* Bugfix: `write_only = True` fields now display in the browsable API.
|
||||
|
||||
### 2.3.11
|
||||
|
||||
**Date**: 14th January 2014
|
||||
|
||||
* Added `write_only` serializer field argument.
|
||||
* Added `write_only_fields` option to `ModelSerializer` classes.
|
||||
* JSON renderer now deals with objects that implement a dict-like interface.
|
||||
* Fix compatiblity with newer versions of `django-oauth-plus`.
|
||||
* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations.
|
||||
* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied.
|
||||
* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params.
|
||||
|
||||
### 2.3.10
|
||||
|
||||
**Date**: 6th December 2013
|
||||
|
||||
* Add in choices information for ChoiceFields in response to `OPTIONS` requests.
|
||||
* Added `pre_delete()` and `post_delete()` method hooks.
|
||||
* Added status code category helper functions.
|
||||
* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception.
|
||||
* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header.
|
||||
* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400.
|
||||
|
||||
### 2.3.9
|
||||
|
@ -75,6 +112,19 @@ You can determine your currently installed version using `pip freeze`:
|
|||
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
|
||||
* Bugfix: Client sending empty string instead of file now clears `FileField`.
|
||||
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
|
||||
* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*]
|
||||
|
||||
---
|
||||
|
||||
[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
|
||||
|
||||
class DisablePaginationMixin(object):
|
||||
def get_paginate_by(self, queryset=None):
|
||||
if self.request.QUERY_PARAMS[self.paginate_by_param] == '0':
|
||||
return None
|
||||
return super(DisablePaginationMixin, self).get_paginate_by(queryset)
|
||||
|
||||
---
|
||||
|
||||
### 2.3.7
|
||||
|
||||
|
|
|
@ -17,9 +17,8 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
|||
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
|
||||
|
||||
:::bash
|
||||
mkdir ~/env
|
||||
virtualenv ~/env/tutorial
|
||||
source ~/env/tutorial/bin/activate
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
|
||||
Now that we're inside a virtualenv environment, we can install our package requirements.
|
||||
|
||||
|
@ -183,9 +182,11 @@ At this point we've translated the model instance into Python native datatypes.
|
|||
|
||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||
|
||||
import StringIO
|
||||
# This import will use either `StringIO.StringIO` or `io.BytesIO`
|
||||
# as appropriate, depending on if we're running Python 2 or Python 3.
|
||||
from rest_framework.compat import BytesIO
|
||||
|
||||
stream = StringIO.StringIO(content)
|
||||
stream = BytesIO(content)
|
||||
data = JSONParser().parse(stream)
|
||||
|
||||
...then we restore those native datatypes into to a fully populated object instance.
|
||||
|
@ -261,8 +262,7 @@ The root of our API is going to be a view that supports listing all the existing
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JSONResponse(serializer.data, status=201)
|
||||
else:
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
|
||||
Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
|
||||
|
||||
|
@ -288,8 +288,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return JSONResponse(serializer.data)
|
||||
else:
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
return JSONResponse(serializer.errors, status=400)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
snippet.delete()
|
||||
|
|
|
@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
|
||||
|
||||
|
@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module.
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
snippet.delete()
|
||||
|
|
|
@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
|
|||
|
||||
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
|
||||
|
||||
We can add a login view for use with the browsable API, by editing our URLconf once more.
|
||||
We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file.
|
||||
|
||||
Add the following import at the top of the file:
|
||||
|
||||
|
@ -167,10 +167,10 @@ In the snippets app, create a new file, `permissions.py`
|
|||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request,
|
||||
# so we'll always allow GET, HEAD or OPTIONS requests.
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions are only allowed to the owner of the snippet
|
||||
|
||||
# Write permissions are only allowed to the owner of the snippet.
|
||||
return obj.owner == request.user
|
||||
|
||||
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
|
||||
|
|
|
@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior
|
|||
|
||||
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.
|
||||
|
||||
Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute.
|
||||
|
||||
For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications.
|
||||
|
||||
## URLs
|
||||
|
||||
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||
|
@ -169,6 +173,7 @@ Great, that was easy!
|
|||
|
||||
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
|
||||
|
||||
[readme-example-api]: ../#example
|
||||
[image]: ../img/quickstart.png
|
||||
[tutorial]: 1-serialization.md
|
||||
[guide]: ../#api-guide
|
||||
|
|
10
mkdocs.py
10
mkdocs.py
|
@ -18,7 +18,7 @@ if local:
|
|||
suffix = '.html'
|
||||
index = 'index.html'
|
||||
else:
|
||||
base_url = 'http://django-rest-framework.org'
|
||||
base_url = 'http://www.django-rest-framework.org'
|
||||
suffix = ''
|
||||
index = ''
|
||||
|
||||
|
@ -144,7 +144,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
|||
if filename == 'index.md':
|
||||
main_title = 'Django REST framework - APIs made easy'
|
||||
else:
|
||||
main_title = 'Django REST framework - ' + main_title
|
||||
main_title = main_title + ' - Django REST framework'
|
||||
|
||||
if relative_path == 'index.md':
|
||||
canonical_url = base_url
|
||||
|
@ -161,6 +161,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
|||
output = output.replace('{{ page_id }}', filename[:-3])
|
||||
output = output.replace('{{ canonical_url }}', canonical_url)
|
||||
|
||||
if filename =='index.md':
|
||||
output = output.replace('{{ ad_block }}', """<hr><p><strong>The team behind REST framework is launching a new API service.</strong></p>
|
||||
<p>If you want to be first in line when we start issuing invitations, please <a href="http://brightapi.com">sign up here</a>.</p>""")
|
||||
else:
|
||||
output = output.replace('{{ ad_block }}', '')
|
||||
|
||||
if prev_url:
|
||||
output = output.replace('{{ prev_url }}', prev_url)
|
||||
output = output.replace('{{ prev_url_disabled }}', '')
|
||||
|
|
|
@ -2,6 +2,7 @@ markdown>=2.1.0
|
|||
PyYAML>=3.10
|
||||
defusedxml>=0.3
|
||||
django-filter>=0.5.4
|
||||
django-oauth-plus>=2.0
|
||||
django-oauth-plus>=2.2.1
|
||||
oauth2>=1.5.211
|
||||
django-oauth2-provider>=0.2.4
|
||||
Pillow==2.3.0
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
__version__ = '2.3.9'
|
||||
"""
|
||||
______ _____ _____ _____ __ _
|
||||
| ___ \ ___/ ___|_ _| / _| | |
|
||||
| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
|
||||
| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
|
||||
| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
|
||||
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
||||
"""
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
__title__ = 'Django REST framework'
|
||||
__version__ = '2.3.13'
|
||||
__author__ = 'Tom Christie'
|
||||
__license__ = 'BSD 2-Clause'
|
||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||
|
||||
# Version synonym
|
||||
VERSION = __version__
|
||||
|
||||
# Header encoding (see RFC5987)
|
||||
HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||
|
|
|
@ -6,10 +6,11 @@ import base64
|
|||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
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 +282,9 @@ class OAuthAuthentication(BaseAuthentication):
|
|||
"""
|
||||
Checks nonce of request, and return True if valid.
|
||||
"""
|
||||
return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce'])
|
||||
oauth_nonce = oauth_request['oauth_nonce']
|
||||
oauth_timestamp = oauth_request['oauth_timestamp']
|
||||
return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp)
|
||||
|
||||
|
||||
class OAuth2Authentication(BaseAuthentication):
|
||||
|
@ -289,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
OAuth 2 authentication backend using `django-oauth2-provider`
|
||||
"""
|
||||
www_authenticate_realm = 'api'
|
||||
allow_query_params_token = settings.DEBUG
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OAuth2Authentication, self).__init__(*args, **kwargs)
|
||||
|
@ -306,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth or auth[0].lower() != b'bearer':
|
||||
if auth and auth[0].lower() == b'bearer':
|
||||
access_token = auth[1]
|
||||
elif 'access_token' in request.POST:
|
||||
access_token = request.POST['access_token']
|
||||
elif 'access_token' in request.GET and self.allow_query_params_token:
|
||||
access_token = request.GET['access_token']
|
||||
else:
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
|
@ -316,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
msg = 'Invalid bearer header. Token string should not contain spaces.'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
return self.authenticate_credentials(request, auth[1])
|
||||
return self.authenticate_credentials(request, access_token)
|
||||
|
||||
def authenticate_credentials(self, request, access_token):
|
||||
"""
|
||||
|
@ -324,11 +334,11 @@ class OAuth2Authentication(BaseAuthentication):
|
|||
"""
|
||||
|
||||
try:
|
||||
token = oauth2_provider.models.AccessToken.objects.select_related('user')
|
||||
token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user')
|
||||
# provider_now switches to timezone aware datetime when
|
||||
# the oauth2_provider version supports to it.
|
||||
token = token.get(token=access_token, expires__gt=provider_now())
|
||||
except oauth2_provider.models.AccessToken.DoesNotExist:
|
||||
except oauth2_provider.oauth2.models.AccessToken.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token')
|
||||
|
||||
user = token.user
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import uuid
|
||||
import hmac
|
||||
import binascii
|
||||
import os
|
||||
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.
|
||||
|
@ -28,8 +34,7 @@ class Token(models.Model):
|
|||
return super(Token, self).save(*args, **kwargs)
|
||||
|
||||
def generate_key(self):
|
||||
unique = uuid.uuid4()
|
||||
return hmac.new(unique.bytes, digestmod=sha1).hexdigest()
|
||||
return binascii.hexlify(os.urandom(20))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.key
|
||||
|
|
|
@ -7,6 +7,7 @@ versions of django/python, and compatibility wrappers around optional packages.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import django
|
||||
import inspect
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -69,6 +70,13 @@ try:
|
|||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# UserDict moves in Python 3
|
||||
try:
|
||||
from UserDict import UserDict
|
||||
from UserDict import DictMixin
|
||||
except ImportError:
|
||||
from collections import UserDict
|
||||
from collections import MutableMapping as DictMixin
|
||||
|
||||
# Try to import PIL in either of the two ways it can end up installed.
|
||||
try:
|
||||
|
@ -96,13 +104,6 @@ def get_concrete_model(model_cls):
|
|||
return model_cls
|
||||
|
||||
|
||||
# Django 1.5 add support for custom auth user model
|
||||
if django.VERSION >= (1, 5):
|
||||
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
|
||||
else:
|
||||
AUTH_USER_MODEL = 'auth.User'
|
||||
|
||||
|
||||
if django.VERSION >= (1, 5):
|
||||
from django.views.generic import View
|
||||
else:
|
||||
|
@ -456,7 +457,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory
|
|||
from django.test.client import FakePayload
|
||||
try:
|
||||
# In 1.5 the test client uses force_bytes
|
||||
from django.utils.encoding import force_bytes_or_smart_bytes
|
||||
from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
|
||||
except ImportError:
|
||||
# In 1.3 and 1.4 the test client just uses smart_str
|
||||
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
|
||||
|
@ -529,19 +530,30 @@ except ImportError:
|
|||
try:
|
||||
import oauth_provider
|
||||
from oauth_provider.store import store as oauth_provider_store
|
||||
|
||||
# check_nonce's calling signature in django-oauth-plus changes sometime
|
||||
# between versions 2.0 and 2.2.1
|
||||
def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp):
|
||||
check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args
|
||||
if 'timestamp' in check_nonce_args:
|
||||
return oauth_provider_store.check_nonce(
|
||||
request, oauth_request, oauth_nonce, oauth_timestamp
|
||||
)
|
||||
return oauth_provider_store.check_nonce(
|
||||
request, oauth_request, oauth_nonce
|
||||
)
|
||||
|
||||
except (ImportError, ImproperlyConfigured):
|
||||
oauth_provider = None
|
||||
oauth_provider_store = None
|
||||
check_nonce = None
|
||||
|
||||
# OAuth 2 support is optional
|
||||
try:
|
||||
import provider.oauth2 as oauth2_provider
|
||||
from provider.oauth2 import models as oauth2_provider_models
|
||||
from provider.oauth2 import forms as oauth2_provider_forms
|
||||
import provider as oauth2_provider
|
||||
from provider import scope as oauth2_provider_scope
|
||||
from provider import constants as oauth2_constants
|
||||
from provider import __version__ as provider_version
|
||||
if provider_version in ('0.2.3', '0.2.4'):
|
||||
if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
|
||||
# 0.2.3 and 0.2.4 are supported version that do not support
|
||||
# timezone aware datetimes
|
||||
import datetime
|
||||
|
@ -551,8 +563,6 @@ try:
|
|||
from django.utils.timezone import now as provider_now
|
||||
except ImportError:
|
||||
oauth2_provider = None
|
||||
oauth2_provider_models = None
|
||||
oauth2_provider_forms = None
|
||||
oauth2_provider_scope = None
|
||||
oauth2_constants = None
|
||||
provider_now = None
|
||||
|
@ -569,3 +579,23 @@ if six.PY3:
|
|||
else:
|
||||
def is_non_str_iterable(obj):
|
||||
return hasattr(obj, '__iter__')
|
||||
|
||||
|
||||
try:
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
except ImportError:
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
Under Python 3 it does nothing.
|
||||
|
||||
To support Python 2 and 3 with a single code base, define a __str__ method
|
||||
returning text and apply this decorator to the class.
|
||||
"""
|
||||
if '__str__' not in klass.__dict__:
|
||||
raise ValueError("@python_2_unicode_compatible cannot be applied "
|
||||
"to %s because it doesn't define __str__()." %
|
||||
klass.__name__)
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
|
|
@ -6,47 +6,40 @@ In addition Django's built in 403 and 404 exceptions are handled.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
from rest_framework import status
|
||||
import math
|
||||
|
||||
|
||||
class APIException(Exception):
|
||||
"""
|
||||
Base class for REST framework exceptions.
|
||||
Subclasses should provide `.status_code` and `.detail` properties.
|
||||
Subclasses should provide `.status_code` and `.default_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 UnsupportedMediaType(APIException):
|
|||
|
||||
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)
|
||||
|
|
|
@ -246,6 +246,7 @@ class WritableField(Field):
|
|||
"""
|
||||
Base for read/write fields.
|
||||
"""
|
||||
write_only = False
|
||||
default_validators = []
|
||||
default_error_messages = {
|
||||
'required': _('This field is required.'),
|
||||
|
@ -255,7 +256,7 @@ class WritableField(Field):
|
|||
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):
|
||||
|
||||
|
@ -269,6 +270,10 @@ class WritableField(Field):
|
|||
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 +301,11 @@ class WritableField(Field):
|
|||
result.validators = self.validators[:]
|
||||
return result
|
||||
|
||||
def get_default_value(self):
|
||||
if is_simple_callable(self.default):
|
||||
return self.default()
|
||||
return self.default
|
||||
|
||||
def validate(self, value):
|
||||
if value in validators.EMPTY_VALUES and self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
|
@ -318,6 +328,11 @@ class WritableField(Field):
|
|||
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`,
|
||||
|
@ -339,10 +354,7 @@ class WritableField(Field):
|
|||
except KeyError:
|
||||
if self.default is not None and not self.partial:
|
||||
# Note: partial updates shouldn't set defaults
|
||||
if is_simple_callable(self.default):
|
||||
native = self.default()
|
||||
else:
|
||||
native = self.default
|
||||
native = self.get_default_value()
|
||||
else:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
|
@ -428,7 +440,7 @@ class BooleanField(WritableField):
|
|||
def field_from_native(self, data, files, field_name, into):
|
||||
# HTML checkboxes do not explicitly represent unchecked as `False`
|
||||
# we deal with that here...
|
||||
if isinstance(data, QueryDict):
|
||||
if isinstance(data, QueryDict) and self.default is None:
|
||||
self.default = False
|
||||
|
||||
return super(BooleanField, self).field_from_native(
|
||||
|
@ -467,7 +479,8 @@ class URLField(CharField):
|
|||
type_label = 'url'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
if not 'validators' in kwargs:
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
|
@ -514,6 +527,11 @@ class ChoiceField(WritableField):
|
|||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
def metadata(self):
|
||||
data = super(ChoiceField, self).metadata()
|
||||
data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
|
||||
return data
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Validates that the input is in self.choices.
|
||||
|
@ -966,7 +984,7 @@ class ImageField(FileField):
|
|||
return None
|
||||
|
||||
from rest_framework.compat import Image
|
||||
assert Image is not None, 'PIL must be installed for ImageField support'
|
||||
assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.'
|
||||
|
||||
# We need to get a file object for PIL. We might have a path or we might
|
||||
# have to read the data into memory.
|
||||
|
|
|
@ -3,8 +3,10 @@ Provides generic filtering backends that can be used to filter the results
|
|||
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, guardian, get_model_name
|
||||
from rest_framework.settings import api_settings
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
|
@ -68,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
search_param = 'search' # The URL query parameter used for the search.
|
||||
# The URL query parameter used for the search.
|
||||
search_param = api_settings.SEARCH_PARAM
|
||||
|
||||
def get_search_terms(self, request):
|
||||
"""
|
||||
|
@ -106,7 +109,9 @@ class SearchFilter(BaseFilterBackend):
|
|||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
ordering_param = 'ordering' # The URL query parameter used for the ordering.
|
||||
# The URL query parameter used for the ordering.
|
||||
ordering_param = api_settings.ORDERING_PARAM
|
||||
ordering_fields = None
|
||||
|
||||
def get_ordering(self, request):
|
||||
"""
|
||||
|
@ -122,17 +127,34 @@ class OrderingFilter(BaseFilterBackend):
|
|||
return (ordering,)
|
||||
return ordering
|
||||
|
||||
def remove_invalid_fields(self, queryset, ordering):
|
||||
field_names = [field.name for field in queryset.model._meta.fields]
|
||||
field_names += queryset.query.aggregates.keys()
|
||||
return [term for term in ordering if term.lstrip('-') in field_names]
|
||||
def 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
|
||||
|
|
|
@ -344,6 +344,18 @@ class GenericAPIView(views.APIView):
|
|||
"""
|
||||
pass
|
||||
|
||||
def pre_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling before deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling after deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
|
|
|
@ -11,6 +11,7 @@ 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
|
||||
|
||||
|
||||
|
@ -60,7 +61,7 @@ class CreateModelMixin(object):
|
|||
|
||||
def get_success_headers(self, data):
|
||||
try:
|
||||
return {'Location': data['url']}
|
||||
return {'Location': data[api_settings.URL_FIELD_NAME]}
|
||||
except (TypeError, KeyError):
|
||||
return {}
|
||||
|
||||
|
@ -115,30 +116,27 @@ class UpdateModelMixin(object):
|
|||
partial = kwargs.pop('partial', False)
|
||||
self.object = self.get_object_or_none()
|
||||
|
||||
if self.object is None:
|
||||
created = True
|
||||
save_kwargs = {'force_insert': True}
|
||||
success_status_code = status.HTTP_201_CREATED
|
||||
else:
|
||||
created = False
|
||||
save_kwargs = {'force_update': True}
|
||||
success_status_code = status.HTTP_200_OK
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
||||
files=request.FILES, partial=partial)
|
||||
|
||||
if serializer.is_valid():
|
||||
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)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
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)
|
||||
|
||||
if self.object is None:
|
||||
self.object = serializer.save(force_insert=True)
|
||||
self.post_save(self.object, created=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
self.object = serializer.save(force_update=True)
|
||||
self.post_save(self.object, created=False)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
|
@ -192,5 +190,7 @@ class DestroyModelMixin(object):
|
|||
"""
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
self.pre_delete(obj)
|
||||
obj.delete()
|
||||
self.post_delete(obj)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
@ -54,9 +54,7 @@ class IsAuthenticated(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.user and request.user.is_authenticated():
|
||||
return True
|
||||
return False
|
||||
return request.user and request.user.is_authenticated()
|
||||
|
||||
|
||||
class IsAdminUser(BasePermission):
|
||||
|
@ -65,9 +63,7 @@ class IsAdminUser(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.user and request.user.is_staff:
|
||||
return True
|
||||
return False
|
||||
return request.user and request.user.is_staff
|
||||
|
||||
|
||||
class IsAuthenticatedOrReadOnly(BasePermission):
|
||||
|
@ -76,11 +72,9 @@ class IsAuthenticatedOrReadOnly(BasePermission):
|
|||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if (request.method in SAFE_METHODS or
|
||||
request.user and
|
||||
request.user.is_authenticated()):
|
||||
return True
|
||||
return False
|
||||
return (request.method in SAFE_METHODS or
|
||||
request.user and
|
||||
request.user.is_authenticated())
|
||||
|
||||
|
||||
class DjangoModelPermissions(BasePermission):
|
||||
|
@ -138,11 +132,9 @@ class DjangoModelPermissions(BasePermission):
|
|||
|
||||
perms = self.get_required_permissions(request.method, model_cls)
|
||||
|
||||
if (request.user and
|
||||
return (request.user and
|
||||
(request.user.is_authenticated() or not self.authenticated_users_only) and
|
||||
request.user.has_perms(perms)):
|
||||
return True
|
||||
return False
|
||||
request.user.has_perms(perms))
|
||||
|
||||
|
||||
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
|
||||
|
|
|
@ -33,6 +33,7 @@ class RelatedField(WritableField):
|
|||
many_widget = widgets.SelectMultiple
|
||||
form_field_class = forms.ChoiceField
|
||||
many_form_field_class = forms.MultipleChoiceField
|
||||
null_values = (None, '', 'None')
|
||||
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
|
@ -65,16 +66,11 @@ class RelatedField(WritableField):
|
|||
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...
|
||||
|
||||
|
@ -123,6 +119,14 @@ class RelatedField(WritableField):
|
|||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
### Default value handling
|
||||
|
||||
def get_default_value(self):
|
||||
default = super(RelatedField, self).get_default_value()
|
||||
if self.many and default is None:
|
||||
return []
|
||||
return default
|
||||
|
||||
### Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
|
@ -171,11 +175,11 @@ class RelatedField(WritableField):
|
|||
except KeyError:
|
||||
if self.partial:
|
||||
return
|
||||
value = [] if self.many else None
|
||||
value = self.get_default_value()
|
||||
|
||||
if value in (None, '') and self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
elif value in (None, ''):
|
||||
if value in self.null_values:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages['required'])
|
||||
into[(self.source or field_name)] = None
|
||||
elif self.many:
|
||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||
|
|
|
@ -10,6 +10,7 @@ from __future__ import unicode_literals
|
|||
|
||||
import copy
|
||||
import json
|
||||
import django
|
||||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http.multipartparser import parse_header
|
||||
|
@ -20,6 +21,7 @@ from rest_framework.compat import StringIO
|
|||
from rest_framework.compat import six
|
||||
from rest_framework.compat import smart_text
|
||||
from rest_framework.compat import yaml
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
from rest_framework.utils import encoders
|
||||
|
@ -144,7 +146,7 @@ class XMLRenderer(BaseRenderer):
|
|||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders *obj* into serialized XML.
|
||||
Renders `data` into serialized XML.
|
||||
"""
|
||||
if data is None:
|
||||
return ''
|
||||
|
@ -194,7 +196,7 @@ class YAMLRenderer(BaseRenderer):
|
|||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders *obj* into serialized YAML.
|
||||
Renders `data` into serialized YAML.
|
||||
"""
|
||||
assert yaml, 'YAMLRenderer requires pyyaml to be installed'
|
||||
|
||||
|
@ -420,8 +422,12 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
In the absence of the View having an associated form then return None.
|
||||
"""
|
||||
if request.method == method:
|
||||
data = request.DATA
|
||||
files = request.FILES
|
||||
try:
|
||||
data = request.DATA
|
||||
files = request.FILES
|
||||
except ParseError:
|
||||
data = None
|
||||
files = None
|
||||
else:
|
||||
data = None
|
||||
files = None
|
||||
|
@ -538,6 +544,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
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
|
||||
|
||||
response_headers = dict(response.items())
|
||||
renderer_content_type = ''
|
||||
if renderer:
|
||||
renderer_content_type = '%s' % renderer.media_type
|
||||
if renderer.charset:
|
||||
renderer_content_type += ' ;%s' % renderer.charset
|
||||
response_headers['Content-Type'] = renderer_content_type
|
||||
|
||||
context = {
|
||||
'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
'view': view,
|
||||
|
@ -549,6 +563,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
'breadcrumblist': self.get_breadcrumbs(request),
|
||||
'allowed_methods': view.allowed_methods,
|
||||
'available_formats': [renderer.format for renderer in view.renderer_classes],
|
||||
'response_headers': response_headers,
|
||||
|
||||
'put_form': self.get_rendered_html_form(view, 'PUT', request),
|
||||
'post_form': self.get_rendered_html_form(view, 'POST', request),
|
||||
|
@ -592,7 +607,7 @@ class MultiPartRenderer(BaseRenderer):
|
|||
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
format = 'multipart'
|
||||
charset = 'utf-8'
|
||||
BOUNDARY = 'BoUnDaRyStRiNg'
|
||||
BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
|
|
@ -223,7 +223,7 @@ class Request(object):
|
|||
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
|
||||
|
@ -279,10 +279,9 @@ class Request(object):
|
|||
if not _hasattr(self, '_method'):
|
||||
self._method = self._request.method
|
||||
|
||||
if self._method == 'POST':
|
||||
# Allow X-HTTP-METHOD-OVERRIDE header
|
||||
self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
|
||||
self._method)
|
||||
# Allow X-HTTP-METHOD-OVERRIDE header
|
||||
self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
|
||||
self._method)
|
||||
|
||||
def _load_stream(self):
|
||||
"""
|
||||
|
@ -347,7 +346,7 @@ class Request(object):
|
|||
media_type = self.content_type
|
||||
|
||||
if stream is None or media_type is None:
|
||||
empty_data = QueryDict('', self._request._encoding)
|
||||
empty_data = QueryDict('', encoding=self._request._encoding)
|
||||
empty_files = MultiValueDict()
|
||||
return (empty_data, empty_files)
|
||||
|
||||
|
@ -356,7 +355,16 @@ class Request(object):
|
|||
if not parser:
|
||||
raise exceptions.UnsupportedMediaType(media_type)
|
||||
|
||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||
try:
|
||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||
except:
|
||||
# If we get an exception during parsing, fill in empty data and
|
||||
# re-raise. Ensures we don't simply repeat the error when
|
||||
# attempting to render the browsable renderer response, or when
|
||||
# logging the request or similar.
|
||||
self._data = QueryDict('', encoding=self._request._encoding)
|
||||
self._files = MultiValueDict()
|
||||
raise
|
||||
|
||||
# Parser classes may return the raw data, or a
|
||||
# DataAndFiles object. Unpack the result as required.
|
||||
|
|
|
@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse):
|
|||
assert charset, 'renderer returned unicode, and did not specify ' \
|
||||
'a charset value.'
|
||||
return bytes(ret.encode(charset))
|
||||
|
||||
if not ret:
|
||||
del self['Content-Type']
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
|
|
|
@ -184,18 +184,24 @@ class SimpleRouter(BaseRouter):
|
|||
bound_methods[method] = action
|
||||
return bound_methods
|
||||
|
||||
def get_lookup_regex(self, viewset):
|
||||
def get_lookup_regex(self, viewset, lookup_prefix=''):
|
||||
"""
|
||||
Given a viewset, return the portion of URL regex that is used
|
||||
to match against a single instance.
|
||||
|
||||
Note that lookup_prefix is not used directly inside REST rest_framework
|
||||
itself, but is required in order to nicely support nested router
|
||||
implementations, such as drf-nested-routers.
|
||||
|
||||
https://github.com/alanjds/drf-nested-routers
|
||||
"""
|
||||
if self.trailing_slash:
|
||||
base_regex = '(?P<{lookup_field}>[^/]+)'
|
||||
base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)'
|
||||
else:
|
||||
# Don't consume `.json` style suffixes
|
||||
base_regex = '(?P<{lookup_field}>[^/.]+)'
|
||||
base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)'
|
||||
lookup_field = getattr(viewset, 'lookup_field', 'pk')
|
||||
return base_regex.format(lookup_field=lookup_field)
|
||||
return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix)
|
||||
|
||||
def get_urls(self):
|
||||
"""
|
||||
|
|
|
@ -26,6 +26,10 @@ def usage():
|
|||
|
||||
|
||||
def main():
|
||||
try:
|
||||
django.setup()
|
||||
except AttributeError:
|
||||
pass
|
||||
TestRunner = get_runner(settings)
|
||||
|
||||
test_runner = TestRunner()
|
||||
|
|
|
@ -100,6 +100,9 @@ INSTALLED_APPS = (
|
|||
'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
|
||||
|
|
|
@ -13,6 +13,7 @@ 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.db import models
|
|||
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,27 @@ from rest_framework.relations import *
|
|||
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:
|
||||
|
@ -326,12 +350,13 @@ class BaseSerializer(WritableField):
|
|||
method = getattr(self, 'transform_%s' % field_name, None)
|
||||
if callable(method):
|
||||
value = method(obj, value)
|
||||
ret[key] = 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.
|
||||
"""
|
||||
|
@ -361,6 +386,9 @@ class BaseSerializer(WritableField):
|
|||
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)
|
||||
|
||||
|
@ -410,17 +438,25 @@ class BaseSerializer(WritableField):
|
|||
raise ValidationError(self.error_messages['required'])
|
||||
return
|
||||
|
||||
# Set the serializer object if it exists
|
||||
obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
|
||||
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
|
||||
|
||||
if 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
|
||||
else:
|
||||
# Set the serializer object if it exists
|
||||
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()
|
||||
|
||||
kwargs = {
|
||||
'instance': obj,
|
||||
'data': value,
|
||||
|
@ -465,7 +501,7 @@ class BaseSerializer(WritableField):
|
|||
else:
|
||||
many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
|
||||
if many:
|
||||
warnings.warn('Implict list/queryset serialization is deprecated. '
|
||||
warnings.warn('Implicit list/queryset serialization is deprecated. '
|
||||
'Use the `many=True` flag when instantiating the serializer.',
|
||||
DeprecationWarning, stacklevel=3)
|
||||
|
||||
|
@ -527,7 +563,7 @@ class BaseSerializer(WritableField):
|
|||
else:
|
||||
many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
|
||||
if many:
|
||||
warnings.warn('Implict list/queryset serialization is deprecated. '
|
||||
warnings.warn('Implicit list/queryset serialization is deprecated. '
|
||||
'Use the `many=True` flag when instantiating the serializer.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
|
@ -585,6 +621,7 @@ class ModelSerializerOptions(SerializerOptions):
|
|||
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):
|
||||
|
@ -611,6 +648,7 @@ class ModelSerializer(Serializer):
|
|||
models.TextField: CharField,
|
||||
models.CommaSeparatedIntegerField: CharField,
|
||||
models.BooleanField: BooleanField,
|
||||
models.NullBooleanField: BooleanField,
|
||||
models.FileField: FileField,
|
||||
models.ImageField: ImageField,
|
||||
}
|
||||
|
@ -647,7 +685,7 @@ class ModelSerializer(Serializer):
|
|||
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
|
||||
|
@ -704,7 +742,9 @@ class ModelSerializer(Serializer):
|
|||
is_m2m = isinstance(relation.field,
|
||||
models.fields.related.ManyToManyField)
|
||||
|
||||
if is_m2m and not relation.field.rel.through._meta.auto_created:
|
||||
if (is_m2m and
|
||||
hasattr(relation.field.rel, 'through') and
|
||||
not relation.field.rel.through._meta.auto_created):
|
||||
has_through_model = True
|
||||
|
||||
if nested:
|
||||
|
@ -717,20 +757,38 @@ class ModelSerializer(Serializer):
|
|||
field.read_only = True
|
||||
|
||||
ret[accessor_name] = field
|
||||
|
||||
# Ensure that 'read_only_fields' is an iterable
|
||||
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||
|
||||
# Add the `read_only` flag to any fields that have bee specified
|
||||
# Add the `read_only` flag to any fields that have been 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
|
||||
|
||||
# Ensure that 'write_only_fields' is an iterable
|
||||
assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
|
||||
|
||||
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
|
||||
|
||||
|
@ -829,7 +887,7 @@ class ModelSerializer(Serializer):
|
|||
except KeyError:
|
||||
return ModelField(model_field=model_field, **kwargs)
|
||||
|
||||
def get_validation_exclusions(self):
|
||||
def get_validation_exclusions(self, instance=None):
|
||||
"""
|
||||
Return a list of field names to exclude from model validation.
|
||||
"""
|
||||
|
@ -841,6 +899,7 @@ class ModelSerializer(Serializer):
|
|||
field_name = field.source or field_name
|
||||
if field_name in exclusions \
|
||||
and not field.read_only \
|
||||
and (field.required or hasattr(instance, field_name)) \
|
||||
and not isinstance(field, Serializer):
|
||||
exclusions.remove(field_name)
|
||||
return exclusions
|
||||
|
@ -855,7 +914,7 @@ class ModelSerializer(Serializer):
|
|||
the full_clean validation checking.
|
||||
"""
|
||||
try:
|
||||
instance.full_clean(exclude=self.get_validation_exclusions())
|
||||
instance.full_clean(exclude=self.get_validation_exclusions(instance))
|
||||
except ValidationError as err:
|
||||
self._errors = err.message_dict
|
||||
return None
|
||||
|
@ -872,7 +931,7 @@ class ModelSerializer(Serializer):
|
|||
|
||||
# Reverse fk or one-to-one relations
|
||||
for (obj, model) in meta.get_all_related_objects_with_model():
|
||||
field_name = obj.field.related_query_name()
|
||||
field_name = obj.get_accessor_name()
|
||||
if field_name in attrs:
|
||||
related_data[field_name] = attrs.pop(field_name)
|
||||
|
||||
|
@ -883,7 +942,7 @@ class ModelSerializer(Serializer):
|
|||
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)
|
||||
|
||||
|
@ -896,7 +955,10 @@ class ModelSerializer(Serializer):
|
|||
# Update an existing instance...
|
||||
if instance is not None:
|
||||
for key, val in attrs.items():
|
||||
setattr(instance, key, val)
|
||||
try:
|
||||
setattr(instance, key, val)
|
||||
except ValueError:
|
||||
self._errors[key] = self.error_messages['required']
|
||||
|
||||
# ...or create a new instance
|
||||
else:
|
||||
|
@ -940,11 +1002,16 @@ class ModelSerializer(Serializer):
|
|||
del(obj._m2m_data)
|
||||
|
||||
if getattr(obj, '_related_data', None):
|
||||
related_fields = dict([
|
||||
(field.get_accessor_name(), field)
|
||||
for field, model
|
||||
in obj._meta.get_all_related_objects_with_model()
|
||||
])
|
||||
for accessor_name, related in obj._related_data.items():
|
||||
if isinstance(related, RelationsList):
|
||||
# Nested reverse fk relationship
|
||||
for related_item in related:
|
||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
||||
fk_field = related_fields[accessor_name].field.name
|
||||
setattr(related_item, fk_field, obj)
|
||||
self.save_object(related_item)
|
||||
|
||||
|
@ -971,6 +1038,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
|||
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):
|
||||
|
@ -989,13 +1057,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
if self.opts.view_name is None:
|
||||
self.opts.view_name = self._get_default_view_name(self.opts.model)
|
||||
|
||||
if 'url' not in fields:
|
||||
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
|
||||
|
||||
|
@ -1031,7 +1099,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
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
|
||||
|
||||
|
|
|
@ -69,6 +69,10 @@ DEFAULTS = {
|
|||
'PAGINATE_BY_PARAM': None,
|
||||
'MAX_PAGINATE_BY': None,
|
||||
|
||||
# Filtering
|
||||
'SEARCH_PARAM': 'search',
|
||||
'ORDERING_PARAM': 'ordering',
|
||||
|
||||
# Authentication
|
||||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
@ -95,6 +99,7 @@ DEFAULTS = {
|
|||
'URL_FORMAT_OVERRIDE': 'format',
|
||||
|
||||
'FORMAT_SUFFIX_KWARG': 'format',
|
||||
'URL_FIELD_NAME': 'url',
|
||||
|
||||
# Input and output formats
|
||||
'DATE_INPUT_FORMATS': (
|
||||
|
|
|
@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
def is_informational(code):
|
||||
return code >= 100 and code <= 199
|
||||
|
||||
def is_success(code):
|
||||
return code >= 200 and code <= 299
|
||||
|
||||
def is_redirect(code):
|
||||
return code >= 300 and code <= 399
|
||||
|
||||
def is_client_error(code):
|
||||
return code >= 400 and code <= 499
|
||||
|
||||
def is_server_error(code):
|
||||
return code >= 500 and code <= 599
|
||||
|
||||
|
||||
HTTP_100_CONTINUE = 100
|
||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||
HTTP_200_OK = 200
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<span href="/">
|
||||
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||
{% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||
</span>
|
||||
<ul class="nav pull-right">
|
||||
{% block userlinks %}
|
||||
|
@ -118,7 +118,7 @@
|
|||
</div>
|
||||
<div class="response-info">
|
||||
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||
{% endfor %}
|
||||
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,11 @@ from __future__ import unicode_literals, absolute_import
|
|||
from django import template
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from django.http import QueryDict
|
||||
from django.utils.encoding import iri_to_uri
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from rest_framework.compat import urlparse, force_text, six, smart_urlquote
|
||||
import re, string
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -144,7 +145,9 @@ def add_query_param(request, key, val):
|
|||
"""
|
||||
Add a query parameter to the current request url, and return the new url.
|
||||
"""
|
||||
return replace_query_param(request.get_full_path(), key, val)
|
||||
iri = request.get_full_path()
|
||||
uri = iri_to_uri(iri)
|
||||
return replace_query_param(uri, key, val)
|
||||
|
||||
|
||||
@register.filter
|
||||
|
@ -186,6 +189,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net
|
|||
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
|
||||
|
||||
|
||||
def smart_urlquote_wrapper(matched_url):
|
||||
"""
|
||||
Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
|
||||
be raised here, see issue #1386
|
||||
"""
|
||||
try:
|
||||
return smart_urlquote(matched_url)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||
"""
|
||||
|
@ -208,7 +222,6 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
safe_input = isinstance(text, SafeData)
|
||||
words = word_split_re.split(force_text(text))
|
||||
for i, word in enumerate(words):
|
||||
match = None
|
||||
if '.' in word or '@' in word or ':' in word:
|
||||
# Deal with punctuation.
|
||||
lead, middle, trail = '', word, ''
|
||||
|
@ -230,9 +243,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
|
|||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||
if simple_url_re.match(middle):
|
||||
url = smart_urlquote(middle)
|
||||
url = smart_urlquote_wrapper(middle)
|
||||
elif simple_url_2_re.match(middle):
|
||||
url = smart_urlquote('http://%s' % middle)
|
||||
url = smart_urlquote_wrapper('http://%s' % middle)
|
||||
elif not ':' in middle and simple_email_re.match(middle):
|
||||
local, domain = middle.rsplit('@', 1)
|
||||
try:
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
|||
from django.test.client import Client as DjangoClient
|
||||
from django.test.client import ClientHandler
|
||||
from django.test import testcases
|
||||
from django.utils.http import urlencode
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import RequestFactory as DjangoRequestFactory
|
||||
from rest_framework.compat import force_bytes_or_smart_bytes, six
|
||||
|
@ -71,6 +72,17 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
|
||||
return ret, content_type
|
||||
|
||||
def get(self, path, data=None, **extra):
|
||||
r = {
|
||||
'QUERY_STRING': urlencode(data or {}, doseq=True),
|
||||
}
|
||||
# Fix to support old behavior where you have the arguments in the url
|
||||
# See #1461
|
||||
if not data and '?' in path:
|
||||
r['QUERY_STRING'] = path.split('?')[1]
|
||||
r.update(extra)
|
||||
return self.generic('GET', path, **r)
|
||||
|
||||
def post(self, path, data=None, format=None, content_type=None, **extra):
|
||||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('POST', path, data, content_type, **extra)
|
||||
|
|
0
rest_framework/tests/accounts/__init__.py
Normal file
0
rest_framework/tests/accounts/__init__.py
Normal file
8
rest_framework/tests/accounts/models.py
Normal file
8
rest_framework/tests/accounts/models.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.db import models
|
||||
|
||||
from rest_framework.tests.users.models import User
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
owner = models.ForeignKey(User, related_name='accounts_owned')
|
||||
admins = models.ManyToManyField(User, blank=True, null=True, related_name='accounts_administered')
|
11
rest_framework/tests/accounts/serializers.py
Normal file
11
rest_framework/tests/accounts/serializers.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.tests.accounts.models import Account
|
||||
from rest_framework.tests.users.serializers import UserSerializer
|
||||
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
admins = UserSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Account
|
|
@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel):
|
|||
|
||||
class ActionItem(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=200)
|
||||
started = models.NullBooleanField(default=False)
|
||||
done = models.BooleanField(default=False)
|
||||
info = CustomField(default='---', max_length=12)
|
||||
|
||||
|
@ -102,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel):
|
|||
|
||||
class Album(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100, unique=True)
|
||||
|
||||
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
|
||||
|
||||
class Photo(RESTFrameworkModel):
|
||||
description = models.TextField()
|
||||
|
@ -167,3 +168,10 @@ class NullableOneToOneSource(RESTFrameworkModel):
|
|||
class BasicModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BasicModel
|
||||
|
||||
|
||||
# Models to test filters
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
|
0
rest_framework/tests/records/__init__.py
Normal file
0
rest_framework/tests/records/__init__.py
Normal file
6
rest_framework/tests/records/models.py
Normal file
6
rest_framework/tests/records/models.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Record(models.Model):
|
||||
account = models.ForeignKey('accounts.Account', blank=True, null=True)
|
||||
owner = models.ForeignKey('users.User', blank=True, null=True)
|
8
rest_framework/tests/serializers.py
Normal file
8
rest_framework/tests/serializers.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
|
||||
|
||||
class NullableFKSourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NullableForeignKeySource
|
|
@ -3,6 +3,7 @@ from django.contrib.auth.models import User
|
|||
from django.http import HttpResponse
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.utils.http import urlencode
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import exceptions
|
||||
from rest_framework import permissions
|
||||
|
@ -19,7 +20,7 @@ from rest_framework.authentication import (
|
|||
)
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.compat import patterns, url, include
|
||||
from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
|
||||
from rest_framework.compat import oauth2_provider, oauth2_provider_scope
|
||||
from rest_framework.compat import oauth, oauth_provider
|
||||
from rest_framework.test import APIRequestFactory, APIClient
|
||||
from rest_framework.views import APIView
|
||||
|
@ -53,10 +54,14 @@ urlpatterns = patterns('',
|
|||
permission_classes=[permissions.TokenHasReadWriteScope]))
|
||||
)
|
||||
|
||||
class OAuth2AuthenticationDebug(OAuth2Authentication):
|
||||
allow_query_params_token = True
|
||||
|
||||
if oauth2_provider is not None:
|
||||
urlpatterns += patterns('',
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
|
||||
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
|
||||
url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])),
|
||||
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
|
||||
permission_classes=[permissions.TokenHasReadWriteScope])),
|
||||
)
|
||||
|
@ -249,7 +254,7 @@ class OAuthTests(TestCase):
|
|||
def setUp(self):
|
||||
# these imports are here because oauth is optional and hiding them in try..except block or compat
|
||||
# could obscure problems if something breaks
|
||||
from oauth_provider.models import Consumer, Resource
|
||||
from oauth_provider.models import Consumer, Scope
|
||||
from oauth_provider.models import Token as OAuthToken
|
||||
from oauth_provider import consts
|
||||
|
||||
|
@ -269,8 +274,8 @@ class OAuthTests(TestCase):
|
|||
self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
|
||||
name='example', user=self.user, status=self.consts.ACCEPTED)
|
||||
|
||||
self.resource = Resource.objects.create(name="resource name", url="api/")
|
||||
self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource,
|
||||
self.scope = Scope.objects.create(name="resource name", url="api/")
|
||||
self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope,
|
||||
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
|
||||
)
|
||||
|
||||
|
@ -362,7 +367,8 @@ class OAuthTests(TestCase):
|
|||
def test_post_form_with_urlencoded_parameters(self):
|
||||
"""Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
|
||||
params = self._create_authorization_url_parameters()
|
||||
response = self.csrf_client.post('/oauth/', params)
|
||||
auth = self._create_authorization_header()
|
||||
response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
|
||||
|
@ -397,10 +403,10 @@ class OAuthTests(TestCase):
|
|||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
|
||||
@unittest.skipUnless(oauth, 'oauth2 not installed')
|
||||
def test_get_form_with_readonly_resource_passing_auth(self):
|
||||
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
|
||||
"""Ensure POSTing with a readonly scope instead of a write scope fails"""
|
||||
read_only_access_token = self.token
|
||||
read_only_access_token.resource.is_readonly = True
|
||||
read_only_access_token.resource.save()
|
||||
read_only_access_token.scope.is_readonly = True
|
||||
read_only_access_token.scope.save()
|
||||
params = self._create_authorization_url_parameters()
|
||||
response = self.csrf_client.get('/oauth-with-scope/', params)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -410,8 +416,8 @@ class OAuthTests(TestCase):
|
|||
def test_post_form_with_readonly_resource_failing_auth(self):
|
||||
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
|
||||
read_only_access_token = self.token
|
||||
read_only_access_token.resource.is_readonly = True
|
||||
read_only_access_token.resource.save()
|
||||
read_only_access_token.scope.is_readonly = True
|
||||
read_only_access_token.scope.save()
|
||||
params = self._create_authorization_url_parameters()
|
||||
response = self.csrf_client.post('/oauth-with-scope/', params)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
|
@ -421,10 +427,11 @@ class OAuthTests(TestCase):
|
|||
def test_post_form_with_write_resource_passing_auth(self):
|
||||
"""Ensure POSTing with a write resource succeed"""
|
||||
read_write_access_token = self.token
|
||||
read_write_access_token.resource.is_readonly = False
|
||||
read_write_access_token.resource.save()
|
||||
read_write_access_token.scope.is_readonly = False
|
||||
read_write_access_token.scope.save()
|
||||
params = self._create_authorization_url_parameters()
|
||||
response = self.csrf_client.post('/oauth-with-scope/', params)
|
||||
auth = self._create_authorization_header()
|
||||
response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
|
||||
|
@ -486,7 +493,7 @@ class OAuth2Tests(TestCase):
|
|||
self.ACCESS_TOKEN = "access_token"
|
||||
self.REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
self.oauth2_client = oauth2_provider_models.Client.objects.create(
|
||||
self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
redirect_uri='',
|
||||
|
@ -495,12 +502,12 @@ class OAuth2Tests(TestCase):
|
|||
user=None,
|
||||
)
|
||||
|
||||
self.access_token = oauth2_provider_models.AccessToken.objects.create(
|
||||
self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
|
||||
token=self.ACCESS_TOKEN,
|
||||
client=self.oauth2_client,
|
||||
user=self.user,
|
||||
)
|
||||
self.refresh_token = oauth2_provider_models.RefreshToken.objects.create(
|
||||
self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
|
||||
user=self.user,
|
||||
access_token=self.access_token,
|
||||
client=self.oauth2_client
|
||||
|
@ -543,6 +550,27 @@ class OAuth2Tests(TestCase):
|
|||
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_passing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in form data succeed"""
|
||||
response = self.csrf_client.post('/oauth2-test/',
|
||||
data={'access_token': self.access_token.token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_passing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True"""
|
||||
query = urlencode({'access_token': self.access_token.token})
|
||||
response = self.csrf_client.get('/oauth2-test-debug/?%s' % query)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_failing_auth_url_transport(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False"""
|
||||
query = urlencode({'access_token': self.access_token.token})
|
||||
response = self.csrf_client.get('/oauth2-test/?%s' % query)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_passing_auth(self):
|
||||
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
|
||||
|
|
|
@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase):
|
|||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
||||
|
||||
def test_invalid_choice_model(self):
|
||||
s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'})
|
||||
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||
self.assertFalse(s.is_valid())
|
||||
self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
|
||||
self.assertEqual(s.data['choice'], '')
|
||||
|
||||
def test_empty_choice_model(self):
|
||||
"""
|
||||
Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field.
|
||||
Test that the 'empty' value is correctly passed and used depending on
|
||||
the 'null' property on the model field.
|
||||
"""
|
||||
s = ChoiceFieldModelSerializer(data={'choice' : ''})
|
||||
s = ChoiceFieldModelSerializer(data={'choice': ''})
|
||||
self.assertTrue(s.is_valid())
|
||||
self.assertEqual(s.data['choice'], '')
|
||||
|
||||
s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''})
|
||||
s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
|
||||
self.assertTrue(s.is_valid())
|
||||
self.assertEqual(s.data['choice'], None)
|
||||
|
||||
|
@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase):
|
|||
self.assertEqual(f.from_native(''), None)
|
||||
self.assertEqual(f.from_native(None), None)
|
||||
|
||||
def test_metadata_choices(self):
|
||||
"""
|
||||
Make sure proper choices are included in the field's metadata.
|
||||
"""
|
||||
choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
|
||||
f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
|
||||
self.assertEqual(f.metadata()['choices'], choices)
|
||||
|
||||
def test_metadata_choices_not_required(self):
|
||||
"""
|
||||
Make sure proper choices are included in the field's metadata.
|
||||
"""
|
||||
choices = [{'value': v, 'display_name': n}
|
||||
for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
|
||||
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||
self.assertEqual(f.metadata()['choices'], choices)
|
||||
|
||||
|
||||
class EmailFieldTests(TestCase):
|
||||
"""
|
||||
|
@ -842,7 +860,9 @@ class SlugFieldTests(TestCase):
|
|||
|
||||
class URLFieldTests(TestCase):
|
||||
"""
|
||||
Tests for URLField attribute values
|
||||
Tests for URLField attribute values.
|
||||
|
||||
(Includes test for #1210, checking that validators can be overridden.)
|
||||
"""
|
||||
|
||||
class URLFieldModel(RESTFrameworkModel):
|
||||
|
@ -884,6 +904,11 @@ class URLFieldTests(TestCase):
|
|||
self.assertEqual(getattr(serializer.fields['url_field'],
|
||||
'max_length'), 20)
|
||||
|
||||
def test_validators_can_be_overridden(self):
|
||||
url_field = serializers.URLField(validators=[])
|
||||
validators = url_field.validators
|
||||
self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators')
|
||||
|
||||
|
||||
class FieldMetadata(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
@ -7,18 +7,15 @@ from django.test import TestCase
|
|||
from django.utils import unittest
|
||||
from rest_framework import generics, serializers, status, filters
|
||||
from rest_framework.compat import django_filters, patterns, url
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from .models import FilterableItem
|
||||
from .utils import temporary_setting
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
||||
|
||||
if django_filters:
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
|
@ -128,7 +125,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
|
@ -136,7 +133,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the date filter works.
|
||||
search_date = datetime.date(2012, 9, 22)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
|
||||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] == search_date]
|
||||
|
@ -151,7 +148,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter works.
|
||||
search_decimal = Decimal('2.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] == search_decimal]
|
||||
|
@ -184,7 +181,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the decimal filter set with 'lt' in the filter class works.
|
||||
search_decimal = Decimal('4.25')
|
||||
request = factory.get('/?decimal=%s' % search_decimal)
|
||||
request = factory.get('/', {'decimal': '%s' % search_decimal})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['decimal'] < search_decimal]
|
||||
|
@ -192,7 +189,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the date filter set with 'gt' in the filter class works.
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
|
||||
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date]
|
||||
|
@ -200,7 +197,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
# Tests that the text filter set with 'icontains' in the filter class works.
|
||||
search_text = 'ff'
|
||||
request = factory.get('/?text=%s' % search_text)
|
||||
request = factory.get('/', {'text': '%s' % search_text})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if search_text in f['text'].lower()]
|
||||
|
@ -209,7 +206,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
# Tests that multiple filters works.
|
||||
search_decimal = Decimal('5.25')
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
|
||||
request = factory.get('/', {
|
||||
'decimal': '%s' % (search_decimal,),
|
||||
'date': '%s' % (search_date,)
|
||||
})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
expected_data = [f for f in self.data if f['date'] > search_date and
|
||||
|
@ -234,7 +234,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
view = FilterFieldsRootView.as_view()
|
||||
|
||||
search_integer = 10
|
||||
request = factory.get('/?integer=%s' % search_integer)
|
||||
request = factory.get('/', {'integer': '%s' % search_integer})
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
@ -265,14 +265,18 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
|||
# Tests that the decimal filter set that should fail.
|
||||
search_decimal = Decimal('4.25')
|
||||
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
|
||||
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(high_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Tests that the decimal filter set that should succeed.
|
||||
search_decimal = Decimal('4.25')
|
||||
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
|
||||
low_item_data = self._serialize_object(low_item)
|
||||
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(low_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, low_item_data)
|
||||
|
||||
|
@ -281,7 +285,11 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
|||
search_date = datetime.date(2012, 10, 2)
|
||||
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
|
||||
valid_item_data = self._serialize_object(valid_item)
|
||||
response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(valid_item)), {
|
||||
'decimal': '{decimal}'.format(decimal=search_decimal),
|
||||
'date': '{date}'.format(date=search_date)
|
||||
})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, valid_item_data)
|
||||
|
||||
|
@ -315,7 +323,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=b')
|
||||
request = factory.get('/', {'search': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -332,7 +340,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('=title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=zzz')
|
||||
request = factory.get('/', {'search': 'zzz'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -348,7 +356,7 @@ class SearchFilterTests(TestCase):
|
|||
search_fields = ('title', '^text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('?search=b')
|
||||
request = factory.get('/', {'search': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -357,6 +365,24 @@ class SearchFilterTests(TestCase):
|
|||
]
|
||||
)
|
||||
|
||||
def test_search_with_nonstandard_search_param(self):
|
||||
with temporary_setting('SEARCH_PARAM', 'query', module=filters):
|
||||
class SearchListView(generics.ListAPIView):
|
||||
model = SearchFilterModel
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
search_fields = ('title', 'text')
|
||||
|
||||
view = SearchListView.as_view()
|
||||
request = factory.get('/', {'query': 'b'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 1, 'title': 'z', 'text': 'abc'},
|
||||
{'id': 2, 'title': 'zz', 'text': 'bcd'}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class OrdringFilterModel(models.Model):
|
||||
title = models.CharField(max_length=20)
|
||||
|
@ -368,7 +394,6 @@ class OrderingFilterRelatedModel(models.Model):
|
|||
related_name="relateds")
|
||||
|
||||
|
||||
|
||||
class OrderingFilterTests(TestCase):
|
||||
def setUp(self):
|
||||
# Sequence of title/text is:
|
||||
|
@ -394,9 +419,10 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=text')
|
||||
request = factory.get('/', {'ordering': 'text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -412,9 +438,10 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=-text')
|
||||
request = factory.get('/', {'ordering': '-text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -430,9 +457,10 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=foobar')
|
||||
request = factory.get('/', {'ordering': 'foobar'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -448,6 +476,7 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
oredering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('')
|
||||
|
@ -466,6 +495,7 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('')
|
||||
|
@ -494,11 +524,12 @@ class OrderingFilterTests(TestCase):
|
|||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = 'title'
|
||||
ordering_fields = '__all__'
|
||||
queryset = OrdringFilterModel.objects.all().annotate(
|
||||
models.Count("relateds"))
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('?ordering=relateds__count')
|
||||
request = factory.get('/', {'ordering': 'relateds__count'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
|
@ -509,5 +540,122 @@ class OrderingFilterTests(TestCase):
|
|||
]
|
||||
)
|
||||
|
||||
def test_ordering_with_nonstandard_ordering_param(self):
|
||||
with temporary_setting('ORDERING_PARAM', 'order', filters):
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
model = OrdringFilterModel
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
ordering = ('title',)
|
||||
ordering_fields = ('text',)
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('/', {'order': 'text'})
|
||||
response = view(request)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 1, 'title': 'zyx', 'text': 'abc'},
|
||||
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
|
||||
{'id': 3, 'title': 'xwv', 'text': 'cde'},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SensitiveOrderingFilterModel(models.Model):
|
||||
username = models.CharField(max_length=20)
|
||||
password = models.CharField(max_length=100)
|
||||
|
||||
|
||||
# Three different styles of serializer.
|
||||
# All should allow ordering by username, but not by password.
|
||||
class SensitiveDataSerializer1(serializers.ModelSerializer):
|
||||
username = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
model = SensitiveOrderingFilterModel
|
||||
fields = ('id', 'username')
|
||||
|
||||
|
||||
class SensitiveDataSerializer2(serializers.ModelSerializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SensitiveOrderingFilterModel
|
||||
fields = ('id', 'username', 'password')
|
||||
|
||||
|
||||
class SensitiveDataSerializer3(serializers.ModelSerializer):
|
||||
user = serializers.CharField(source='username')
|
||||
|
||||
class Meta:
|
||||
model = SensitiveOrderingFilterModel
|
||||
fields = ('id', 'user')
|
||||
|
||||
|
||||
class SensitiveOrderingFilterTests(TestCase):
|
||||
def setUp(self):
|
||||
for idx in range(3):
|
||||
username = {0: 'userA', 1: 'userB', 2: 'userC'}[idx]
|
||||
password = {0: 'passA', 1: 'passC', 2: 'passB'}[idx]
|
||||
SensitiveOrderingFilterModel(username=username, password=password).save()
|
||||
|
||||
def test_order_by_serializer_fields(self):
|
||||
for serializer_cls in [
|
||||
SensitiveDataSerializer1,
|
||||
SensitiveDataSerializer2,
|
||||
SensitiveDataSerializer3
|
||||
]:
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
serializer_class = serializer_cls
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('/', {'ordering': '-username'})
|
||||
response = view(request)
|
||||
|
||||
if serializer_cls == SensitiveDataSerializer3:
|
||||
username_field = 'user'
|
||||
else:
|
||||
username_field = 'username'
|
||||
|
||||
# Note: Inverse username ordering correctly applied.
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 3, username_field: 'userC'},
|
||||
{'id': 2, username_field: 'userB'},
|
||||
{'id': 1, username_field: 'userA'},
|
||||
]
|
||||
)
|
||||
|
||||
def test_cannot_order_by_non_serializer_fields(self):
|
||||
for serializer_cls in [
|
||||
SensitiveDataSerializer1,
|
||||
SensitiveDataSerializer2,
|
||||
SensitiveDataSerializer3
|
||||
]:
|
||||
class OrderingListView(generics.ListAPIView):
|
||||
queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
|
||||
filter_backends = (filters.OrderingFilter,)
|
||||
serializer_class = serializer_cls
|
||||
|
||||
view = OrderingListView.as_view()
|
||||
request = factory.get('/', {'ordering': 'password'})
|
||||
response = view(request)
|
||||
|
||||
if serializer_cls == SensitiveDataSerializer3:
|
||||
username_field = 'user'
|
||||
else:
|
||||
username_field = 'username'
|
||||
|
||||
# Note: The passwords are not in order. Default ordering is used.
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
[
|
||||
{'id': 1, username_field: 'userA'}, # PassB
|
||||
{'id': 2, username_field: 'userB'}, # PassC
|
||||
{'id': 3, username_field: 'userC'}, # PassA
|
||||
]
|
||||
)
|
||||
|
|
|
@ -4,8 +4,10 @@ from django.contrib.contenttypes.generic import GenericRelation, GenericForeignK
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags have a descriptive slug, and are attached to an arbitrary object.
|
||||
|
@ -15,10 +17,11 @@ class Tag(models.Model):
|
|||
object_id = models.PositiveIntegerField()
|
||||
tagged_item = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Bookmark(models.Model):
|
||||
"""
|
||||
A URL bookmark that may have multiple tags attached.
|
||||
|
@ -26,10 +29,11 @@ class Bookmark(models.Model):
|
|||
url = models.URLField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return 'Bookmark: %s' % self.url
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Note(models.Model):
|
||||
"""
|
||||
A textual note that may have multiple tags attached.
|
||||
|
@ -37,7 +41,7 @@ class Note(models.Model):
|
|||
text = models.TextField()
|
||||
tags = GenericRelation(Tag)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return 'Note: %s' % self.text
|
||||
|
||||
|
||||
|
@ -69,6 +73,35 @@ class TestGenericRelations(TestCase):
|
|||
}
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
def test_generic_nested_relation(self):
|
||||
"""
|
||||
Test saving a GenericRelation field via a nested serializer.
|
||||
"""
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tag
|
||||
exclude = ('content_type', 'object_id')
|
||||
|
||||
class BookmarkSerializer(serializers.ModelSerializer):
|
||||
tags = TagSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
exclude = ('id',)
|
||||
|
||||
data = {
|
||||
'url': 'https://docs.djangoproject.com/',
|
||||
'tags': [
|
||||
{'tag': 'contenttypes'},
|
||||
{'tag': 'genericrelations'},
|
||||
]
|
||||
}
|
||||
serializer = BookmarkSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
serializer.save()
|
||||
self.assertEqual(serializer.object.tags.count(), 2)
|
||||
|
||||
def test_generic_fk(self):
|
||||
"""
|
||||
Test a relationship that spans a GenericForeignKey field.
|
||||
|
|
|
@ -50,7 +50,7 @@ class TemplateHTMLRendererTests(TestCase):
|
|||
"""
|
||||
self.get_template = django.template.loader.get_template
|
||||
|
||||
def get_template(template_name):
|
||||
def get_template(template_name, dirs=None):
|
||||
if template_name == 'example.html':
|
||||
return Template("example: {{ object }}")
|
||||
raise TemplateDoesNotExist(template_name)
|
||||
|
@ -108,11 +108,13 @@ class TemplateHTMLRendererExceptionTests(TestCase):
|
|||
def test_not_found_html_view_with_template(self):
|
||||
response = self.client.get('/not_found')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(response.content, six.b("404: Not found"))
|
||||
self.assertTrue(response.content in (
|
||||
six.b("404: Not found"), six.b("404 Not Found")))
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
def test_permission_denied_html_view_with_template(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content, six.b("403: Permission denied"))
|
||||
self.assertTrue(response.content in (
|
||||
six.b("403: Permission denied"), six.b("403 Forbidden")))
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
|
|
@ -3,6 +3,7 @@ import json
|
|||
from django.test import TestCase
|
||||
from rest_framework import generics, status, serializers
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import (
|
||||
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
|
||||
|
@ -331,3 +332,48 @@ class TestOverriddenURLField(TestCase):
|
|||
serializer.data,
|
||||
{'title': 'New blog post', 'url': 'foo bar'}
|
||||
)
|
||||
|
||||
|
||||
class TestURLFieldNameBySettings(TestCase):
|
||||
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
self.saved_url_field_name = api_settings.URL_FIELD_NAME
|
||||
api_settings.URL_FIELD_NAME = 'global_url_field'
|
||||
|
||||
class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = ('title', api_settings.URL_FIELD_NAME)
|
||||
|
||||
self.Serializer = Serializer
|
||||
self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
def tearDown(self):
|
||||
api_settings.URL_FIELD_NAME = self.saved_url_field_name
|
||||
|
||||
def test_overridden_url_field_name(self):
|
||||
request = factory.get('/posts/')
|
||||
serializer = self.Serializer(self.obj, context={'request': request})
|
||||
self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
|
||||
|
||||
|
||||
class TestURLFieldNameByOptions(TestCase):
|
||||
urls = 'rest_framework.tests.test_hyperlinkedserializers'
|
||||
|
||||
def setUp(self):
|
||||
class Serializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = BlogPost
|
||||
fields = ('title', 'serializer_url_field')
|
||||
url_field_name = 'serializer_url_field'
|
||||
|
||||
self.Serializer = Serializer
|
||||
self.obj = BlogPost.objects.create(title="New blog post")
|
||||
|
||||
def test_overridden_url_field_name(self):
|
||||
request = factory.get('/posts/')
|
||||
serializer = self.Serializer(self.obj, context={'request': request})
|
||||
self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
|
||||
|
|
30
rest_framework/tests/test_nullable_fields.py
Normal file
30
rest_framework/tests/test_nullable_fields.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||
from rest_framework.tests.views import NullableFKSourceDetail
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^objects/(?P<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
|
||||
)
|
||||
|
||||
|
||||
class NullableForeignKeyTests(APITestCase):
|
||||
"""
|
||||
DRF should be able to handle nullable foreign keys when a test
|
||||
Client POST/PUT request is made with its own serialized object.
|
||||
"""
|
||||
urls = 'rest_framework.tests.test_nullable_fields'
|
||||
|
||||
def test_updating_object_with_null_fk(self):
|
||||
obj = NullableForeignKeySource(name='example', target=None)
|
||||
obj.save()
|
||||
serialized_data = NullableFKSourceSerializer(obj).data
|
||||
|
||||
response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
|
||||
|
||||
self.assertEqual(response.data, serialized_data)
|
|
@ -9,14 +9,18 @@ from rest_framework import generics, status, pagination, filters, serializers
|
|||
from rest_framework.compat import django_filters
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.tests.models import BasicModel
|
||||
from .models import FilterableItem
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
# Helper function to split arguments out of an url
|
||||
def split_arguments_from_url(url):
|
||||
if '?' not in url:
|
||||
return url
|
||||
|
||||
class FilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
path, args = url.split('?')
|
||||
args = dict(r.split('=') for r in args.split('&'))
|
||||
return path, args
|
||||
|
||||
|
||||
class RootView(generics.ListCreateAPIView):
|
||||
|
@ -84,7 +88,7 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -93,7 +97,7 @@ class IntegrationTestPagination(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -146,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
|
||||
EXPECTED_NUM_QUERIES = 2
|
||||
|
||||
request = factory.get('/?decimal=15.20')
|
||||
request = factory.get('/', {'decimal': '15.20'})
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -155,7 +159,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -164,7 +168,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['previous']))
|
||||
with self.assertNumQueries(EXPECTED_NUM_QUERIES):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -191,7 +195,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
|
||||
view = BasicFilterFieldsRootView.as_view()
|
||||
|
||||
request = factory.get('/?decimal=15.20')
|
||||
request = factory.get('/', {'decimal': '15.20'})
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -200,7 +204,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertNotEqual(response.data['next'], None)
|
||||
self.assertEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['next'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['next']))
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -209,7 +213,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
|
|||
self.assertEqual(response.data['next'], None)
|
||||
self.assertNotEqual(response.data['previous'], None)
|
||||
|
||||
request = factory.get(response.data['previous'])
|
||||
request = factory.get(*split_arguments_from_url(response.data['previous']))
|
||||
with self.assertNumQueries(2):
|
||||
response = view(request).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
@ -317,7 +321,7 @@ class TestCustomPaginateByParam(TestCase):
|
|||
"""
|
||||
If paginate_by_param is set, the new kwarg should limit per view requests.
|
||||
"""
|
||||
request = factory.get('/?page_size=5')
|
||||
request = factory.get('/', {'page_size': 5})
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
@ -345,7 +349,7 @@ class TestMaxPaginateByParam(TestCase):
|
|||
"""
|
||||
If max_paginate_by is set, it should limit page size for the view.
|
||||
"""
|
||||
request = factory.get('/?page_size=10')
|
||||
request = factory.get('/', data={'page_size': 10})
|
||||
response = self.view(request).render()
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
self.assertEqual(response.data['results'], self.data[:5])
|
||||
|
|
|
@ -98,3 +98,23 @@ class RelatedFieldSourceTests(TestCase):
|
|||
obj = ClassWithQuerysetMethod()
|
||||
value = field.field_to_native(obj, 'field_name')
|
||||
self.assertEqual(value, ['BlogPost object'])
|
||||
|
||||
# Regression for #1129
|
||||
def test_exception_for_incorect_fk(self):
|
||||
"""
|
||||
Check that the exception message are correct if the source field
|
||||
doesn't exist.
|
||||
"""
|
||||
from rest_framework.tests.models import ManyToManySource
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
attrs = {
|
||||
'name': serializers.SlugRelatedField(
|
||||
slug_field='name', source='banzai'),
|
||||
'Meta': Meta,
|
||||
}
|
||||
|
||||
TestSerializer = type(str('TestSerializer'),
|
||||
(serializers.ModelSerializer,), attrs)
|
||||
with self.assertRaises(AttributeError):
|
||||
TestSerializer(data={'name': 'foo'})
|
||||
|
|
|
@ -3,9 +3,7 @@ from django.db import models
|
|||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class OneToOneTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
from .models import OneToOneTarget
|
||||
|
||||
|
||||
class OneToOneSource(models.Model):
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from decimal import Decimal
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -15,7 +16,9 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
|||
from rest_framework.parsers import YAMLParser, XMLParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from collections import MutableMapping
|
||||
import datetime
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
|
||||
|
@ -32,6 +35,10 @@ expected_results = [
|
|||
]
|
||||
|
||||
|
||||
class DummyTestModel(models.Model):
|
||||
name = models.CharField(max_length=42, default='')
|
||||
|
||||
|
||||
class BasicRendererTests(TestCase):
|
||||
def test_expected_results(self):
|
||||
for value, renderer_cls, expected in expected_results:
|
||||
|
@ -64,11 +71,23 @@ class MockView(APIView):
|
|||
|
||||
|
||||
class MockGETView(APIView):
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
return Response({'foo': ['bar', 'baz']})
|
||||
|
||||
|
||||
|
||||
class MockPOSTView(APIView):
|
||||
def post(self, request, **kwargs):
|
||||
return Response({'foo': request.DATA})
|
||||
|
||||
|
||||
class EmptyGETView(APIView):
|
||||
renderer_classes = (JSONRenderer,)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class HTMLView(APIView):
|
||||
renderer_classes = (BrowsableAPIRenderer, )
|
||||
|
||||
|
@ -88,8 +107,10 @@ urlpatterns = patterns('',
|
|||
url(r'^cache$', MockGETView.as_view()),
|
||||
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
|
||||
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
|
||||
url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
|
||||
url(r'^html$', HTMLView.as_view()),
|
||||
url(r'^html1$', HTMLView1.as_view()),
|
||||
url(r'^empty$', EmptyGETView.as_view()),
|
||||
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
|
||||
)
|
||||
|
||||
|
@ -219,6 +240,34 @@ class RendererEndToEndTests(TestCase):
|
|||
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
|
||||
self.assertEqual(resp.status_code, DUMMYSTATUS)
|
||||
|
||||
def test_parse_error_renderers_browsable_api(self):
|
||||
"""Invalid data should still render the browsable API correctly."""
|
||||
resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
|
||||
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_204_no_content_responses_have_no_content_type_set(self):
|
||||
"""
|
||||
Regression test for #1196
|
||||
|
||||
https://github.com/tomchristie/django-rest-framework/issues/1196
|
||||
"""
|
||||
resp = self.client.get('/empty')
|
||||
self.assertEqual(resp.get('Content-Type', None), None)
|
||||
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def test_contains_headers_of_api_response(self):
|
||||
"""
|
||||
Issue #1437
|
||||
|
||||
Test we display the headers of the API response and not those from the
|
||||
HTML response
|
||||
"""
|
||||
resp = self.client.get('/html1')
|
||||
self.assertContains(resp, '>GET, HEAD, OPTIONS<')
|
||||
self.assertContains(resp, '>application/json<')
|
||||
self.assertNotContains(resp, '>text/html; charset=utf-8<')
|
||||
|
||||
|
||||
_flat_repr = '{"foo": ["bar", "baz"]}'
|
||||
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
|
||||
|
@ -244,6 +293,58 @@ class JSONRendererTests(TestCase):
|
|||
ret = JSONRenderer().render(_('test'))
|
||||
self.assertEqual(ret, b'"test"')
|
||||
|
||||
def test_render_queryset_values(self):
|
||||
o = DummyTestModel.objects.create(name='dummy')
|
||||
qs = DummyTestModel.objects.values('id', 'name')
|
||||
ret = JSONRenderer().render(qs)
|
||||
data = json.loads(ret.decode('utf-8'))
|
||||
self.assertEquals(data, [{'id': o.id, 'name': o.name}])
|
||||
|
||||
def test_render_queryset_values_list(self):
|
||||
o = DummyTestModel.objects.create(name='dummy')
|
||||
qs = DummyTestModel.objects.values_list('id', 'name')
|
||||
ret = JSONRenderer().render(qs)
|
||||
data = json.loads(ret.decode('utf-8'))
|
||||
self.assertEquals(data, [[o.id, o.name]])
|
||||
|
||||
def test_render_dict_abc_obj(self):
|
||||
class Dict(MutableMapping):
|
||||
def __init__(self):
|
||||
self._dict = dict()
|
||||
def __getitem__(self, key):
|
||||
return self._dict.__getitem__(key)
|
||||
def __setitem__(self, key, value):
|
||||
return self._dict.__setitem__(key, value)
|
||||
def __delitem__(self, key):
|
||||
return self._dict.__delitem__(key)
|
||||
def __iter__(self):
|
||||
return self._dict.__iter__()
|
||||
def __len__(self):
|
||||
return self._dict.__len__()
|
||||
def keys(self):
|
||||
return self._dict.keys()
|
||||
|
||||
x = Dict()
|
||||
x['key'] = 'string value'
|
||||
x[2] = 3
|
||||
ret = JSONRenderer().render(x)
|
||||
data = json.loads(ret.decode('utf-8'))
|
||||
self.assertEquals(data, {'key': 'string value', '2': 3})
|
||||
|
||||
def test_render_obj_with_getitem(self):
|
||||
class DictLike(object):
|
||||
def __init__(self):
|
||||
self._dict = {}
|
||||
def set(self, value):
|
||||
self._dict = dict(value)
|
||||
def __getitem__(self, key):
|
||||
return self._dict[key]
|
||||
|
||||
x = DictLike()
|
||||
x.set({'a': 1, 'b': 'string'})
|
||||
with self.assertRaises(TypeError):
|
||||
JSONRenderer().render(x)
|
||||
|
||||
def test_without_content_type_args(self):
|
||||
"""
|
||||
Test basic JSON rendering.
|
||||
|
@ -512,6 +613,10 @@ class CacheRenderTest(TestCase):
|
|||
method = getattr(self.client, http_method)
|
||||
resp = method(url)
|
||||
del resp.client, resp.request
|
||||
try:
|
||||
del resp.wsgi_request
|
||||
except AttributeError:
|
||||
pass
|
||||
return resp
|
||||
|
||||
def test_obj_pickling(self):
|
||||
|
|
|
@ -68,6 +68,9 @@ class TestMethodOverloading(TestCase):
|
|||
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
|
||||
self.assertEqual(request.method, 'DELETE')
|
||||
|
||||
|
||||
class TestContentParsing(TestCase):
|
||||
def test_standard_behaviour_determines_no_content_GET(self):
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
from django.db import models
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.test import TestCase
|
||||
from django.utils import unittest
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers, fields, relations
|
||||
|
@ -12,6 +13,31 @@ from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, Acti
|
|||
from rest_framework.tests.models import BasicModelSerializer
|
||||
import datetime
|
||||
import pickle
|
||||
try:
|
||||
import PIL
|
||||
except:
|
||||
PIL = None
|
||||
|
||||
|
||||
if PIL is not None:
|
||||
class AMOAFModel(RESTFrameworkModel):
|
||||
char_field = models.CharField(max_length=1024, blank=True)
|
||||
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||
email_field = models.EmailField(max_length=1024, blank=True)
|
||||
file_field = models.FileField(upload_to='test', max_length=1024, blank=True)
|
||||
image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)
|
||||
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||
url_field = models.URLField(max_length=1024, blank=True)
|
||||
|
||||
class DVOAFModel(RESTFrameworkModel):
|
||||
positive_integer_field = models.PositiveIntegerField(blank=True)
|
||||
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
|
||||
email_field = models.EmailField(blank=True)
|
||||
file_field = models.FileField(upload_to='test', blank=True)
|
||||
image_field = models.ImageField(upload_to='test', blank=True)
|
||||
slug_field = models.SlugField(blank=True)
|
||||
url_field = models.URLField(blank=True)
|
||||
|
||||
|
||||
class SubComment(object):
|
||||
|
@ -71,6 +97,15 @@ class ActionItemSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = ActionItem
|
||||
|
||||
class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
|
||||
"""
|
||||
Intended to test that fields with `required=False` are excluded from validation.
|
||||
"""
|
||||
title = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ActionItem
|
||||
fields = ('title',)
|
||||
|
||||
class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
|
||||
|
||||
|
@ -105,6 +140,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
|
|||
model = Person
|
||||
|
||||
|
||||
class NestedSerializerWithRenamedField(serializers.Serializer):
|
||||
renamed_info = serializers.Field(source='info')
|
||||
|
||||
|
||||
class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer):
|
||||
nested = NestedSerializerWithRenamedField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
|
||||
|
||||
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
|
||||
"""
|
||||
Testing for #652.
|
||||
|
@ -121,7 +167,7 @@ class AlbumsSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ['title'] # lists are also valid options
|
||||
fields = ['title', 'ref'] # lists are also valid options
|
||||
|
||||
|
||||
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
|
||||
|
@ -277,7 +323,13 @@ class BasicTests(TestCase):
|
|||
serializer.save()
|
||||
self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.')
|
||||
|
||||
|
||||
def test_fields_marked_as_not_required_are_excluded_from_validation(self):
|
||||
"""
|
||||
Check that fields with `required=False` are included in list of exclusions.
|
||||
"""
|
||||
serializer = ActionItemSerializerOptionalFields(self.actionitem)
|
||||
exclusions = serializer.get_validation_exclusions()
|
||||
self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
|
||||
|
||||
|
||||
class DictStyleSerializer(serializers.Serializer):
|
||||
|
@ -456,6 +508,46 @@ class ValidationTests(TestCase):
|
|||
)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
|
||||
def test_writable_star_source_on_nested_serializer_with_parent_object(self):
|
||||
class TitleSerializer(serializers.Serializer):
|
||||
title = serializers.WritableField(source='title')
|
||||
|
||||
class AlbumSerializer(serializers.ModelSerializer):
|
||||
nested = TitleSerializer(source='*')
|
||||
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ('nested',)
|
||||
|
||||
class PhotoSerializer(serializers.ModelSerializer):
|
||||
album = AlbumSerializer(source='album')
|
||||
|
||||
class Meta:
|
||||
model = Photo
|
||||
fields = ('album', )
|
||||
|
||||
photo = Photo(album=Album())
|
||||
|
||||
data = {'album': {'nested': {'title': 'test'}}}
|
||||
|
||||
serializer = PhotoSerializer(photo, data=data)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.data, data)
|
||||
|
||||
def test_writable_star_source_with_inner_source_fields(self):
|
||||
"""
|
||||
Tests that a serializer with source="*" correctly expands the
|
||||
it's fields into the outer serializer even if they have their
|
||||
own 'source' parameters.
|
||||
"""
|
||||
|
||||
serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={
|
||||
'name': 'marko',
|
||||
'nested': {'renamed_info': 'hi'}},
|
||||
)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.errors, {})
|
||||
|
||||
|
||||
class CustomValidationTests(TestCase):
|
||||
class CommentSerializerWithFieldValidator(CommentSerializer):
|
||||
|
@ -551,12 +643,38 @@ class ModelValidationTests(TestCase):
|
|||
"""
|
||||
Just check if serializers.ModelSerializer handles unique checks via .full_clean()
|
||||
"""
|
||||
serializer = AlbumsSerializer(data={'title': 'a'})
|
||||
serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'})
|
||||
serializer.is_valid()
|
||||
serializer.save()
|
||||
second_serializer = AlbumsSerializer(data={'title': 'a'})
|
||||
self.assertFalse(second_serializer.is_valid())
|
||||
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
|
||||
self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],})
|
||||
third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}])
|
||||
self.assertFalse(third_serializer.is_valid())
|
||||
self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
|
||||
|
||||
def test_foreign_key_is_null_with_partial(self):
|
||||
"""
|
||||
Test ModelSerializer validation with partial=True
|
||||
|
||||
Specifically test that a null foreign key does not pass validation
|
||||
"""
|
||||
album = Album(title='test')
|
||||
album.save()
|
||||
|
||||
class PhotoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Photo
|
||||
|
||||
photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
|
||||
self.assertTrue(photo_serializer.is_valid())
|
||||
photo = photo_serializer.save()
|
||||
|
||||
# Updating only the album (foreign key)
|
||||
photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
|
||||
self.assertFalse(photo_serializer.is_valid())
|
||||
self.assertTrue('album' in photo_serializer.errors)
|
||||
self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required'])
|
||||
|
||||
def test_foreign_key_with_partial(self):
|
||||
"""
|
||||
|
@ -817,6 +935,58 @@ class DefaultValueTests(TestCase):
|
|||
self.assertEqual(instance.text, 'overridden')
|
||||
|
||||
|
||||
class WritableFieldDefaultValueTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.expected = {'default': 'value'}
|
||||
self.create_field = fields.WritableField
|
||||
|
||||
def test_get_default_value_with_noncallable(self):
|
||||
field = self.create_field(default=self.expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_with_callable(self):
|
||||
field = self.create_field(default=lambda : self.expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_when_not_required(self):
|
||||
field = self.create_field(default=self.expected, required=False)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, self.expected)
|
||||
|
||||
def test_get_default_value_returns_None(self):
|
||||
field = self.create_field()
|
||||
got = field.get_default_value()
|
||||
self.assertIsNone(got)
|
||||
|
||||
def test_get_default_value_returns_non_True_values(self):
|
||||
values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
|
||||
for expected in values:
|
||||
field = self.create_field(default=expected)
|
||||
got = field.get_default_value()
|
||||
self.assertEqual(got, expected)
|
||||
|
||||
|
||||
class RelatedFieldDefaultValueTests(WritableFieldDefaultValueTests):
|
||||
|
||||
def setUp(self):
|
||||
self.expected = {'foo': 'bar'}
|
||||
self.create_field = relations.RelatedField
|
||||
|
||||
def test_get_default_value_returns_empty_list(self):
|
||||
field = self.create_field(many=True)
|
||||
got = field.get_default_value()
|
||||
self.assertListEqual(got, [])
|
||||
|
||||
def test_get_default_value_returns_expected(self):
|
||||
expected = [1, 2, 3]
|
||||
field = self.create_field(many=True, default=expected)
|
||||
got = field.get_default_value()
|
||||
self.assertListEqual(got, expected)
|
||||
|
||||
|
||||
class CallableDefaultValueTests(TestCase):
|
||||
def setUp(self):
|
||||
class CallableDefaultValueSerializer(serializers.ModelSerializer):
|
||||
|
@ -1430,18 +1600,10 @@ class ManyFieldHelpTextTest(TestCase):
|
|||
self.assertEqual('Some help text.', rel_field.help_text)
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
class AMOAFModel(RESTFrameworkModel):
|
||||
char_field = models.CharField(max_length=1024, blank=True)
|
||||
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
|
||||
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
|
||||
email_field = models.EmailField(max_length=1024, blank=True)
|
||||
file_field = models.FileField(max_length=1024, blank=True)
|
||||
image_field = models.ImageField(max_length=1024, blank=True)
|
||||
slug_field = models.SlugField(max_length=1024, blank=True)
|
||||
url_field = models.URLField(max_length=1024, blank=True)
|
||||
|
||||
class AMOAFSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -1511,17 +1673,10 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
|
|||
self.field_test('url_field')
|
||||
|
||||
|
||||
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
|
||||
class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
class DVOAFModel(RESTFrameworkModel):
|
||||
positive_integer_field = models.PositiveIntegerField(blank=True)
|
||||
positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
|
||||
email_field = models.EmailField(blank=True)
|
||||
file_field = models.FileField(blank=True)
|
||||
image_field = models.ImageField(blank=True)
|
||||
slug_field = models.SlugField(blank=True)
|
||||
url_field = models.URLField(blank=True)
|
||||
|
||||
class DVOAFSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -1720,3 +1875,75 @@ class TestSerializerTransformMethods(TestCase):
|
|||
'b_renamed': None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DefaultTrueBooleanModel(models.Model):
|
||||
cat = models.BooleanField(default=True)
|
||||
dog = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class SerializerDefaultTrueBoolean(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SerializerDefaultTrueBoolean, self).setUp()
|
||||
|
||||
class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DefaultTrueBooleanModel
|
||||
fields = ('cat', 'dog')
|
||||
|
||||
self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
|
||||
|
||||
def test_enabled_as_false(self):
|
||||
serializer = self.default_true_boolean_serializer(data={'cat': False,
|
||||
'dog': False})
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.data['cat'], False)
|
||||
self.assertEqual(serializer.data['dog'], False)
|
||||
|
||||
def test_enabled_as_true(self):
|
||||
serializer = self.default_true_boolean_serializer(data={'cat': True,
|
||||
'dog': True})
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.data['cat'], True)
|
||||
self.assertEqual(serializer.data['dog'], True)
|
||||
|
||||
def test_enabled_partial(self):
|
||||
serializer = self.default_true_boolean_serializer(data={'cat': False},
|
||||
partial=True)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.data['cat'], False)
|
||||
self.assertEqual(serializer.data['dog'], False)
|
||||
|
||||
|
||||
class BoolenFieldTypeTest(TestCase):
|
||||
'''
|
||||
Ensure the various Boolean based model fields are rendered as the proper
|
||||
field type
|
||||
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
'''
|
||||
Setup an ActionItemSerializer for BooleanTesting
|
||||
'''
|
||||
data = {
|
||||
'title': 'b' * 201,
|
||||
}
|
||||
self.serializer = ActionItemSerializer(data=data)
|
||||
|
||||
def test_booleanfield_type(self):
|
||||
'''
|
||||
Test that BooleanField is infered from models.BooleanField
|
||||
'''
|
||||
bfield = self.serializer.get_fields()['done']
|
||||
self.assertEqual(type(bfield), fields.BooleanField)
|
||||
|
||||
def test_nullbooleanfield_type(self):
|
||||
'''
|
||||
Test that BooleanField is infered from models.NullBooleanField
|
||||
|
||||
https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8
|
||||
'''
|
||||
bfield = self.serializer.get_fields()['started']
|
||||
self.assertEqual(type(bfield), fields.BooleanField)
|
||||
|
|
19
rest_framework/tests/test_serializer_import.py
Normal file
19
rest_framework/tests/test_serializer_import.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.accounts.serializers import AccountSerializer
|
||||
|
||||
|
||||
class ImportingModelSerializerTests(TestCase):
|
||||
"""
|
||||
In some situations like, GH #1225, it is possible, especially in
|
||||
testing, to import a serializer who's related models have not yet
|
||||
been resolved by Django. `AccountSerializer` is an example of such
|
||||
a serializer (imported at the top of this file).
|
||||
"""
|
||||
def test_import_model_serializer(self):
|
||||
"""
|
||||
The serializer at the top of this file should have been
|
||||
imported successfully, and we should be able to instantiate it.
|
||||
"""
|
||||
self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer)
|
|
@ -6,6 +6,7 @@ Doesn't cover model serializers.
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from . import models
|
||||
|
||||
|
||||
class WritableNestedSerializerBasicTests(TestCase):
|
||||
|
@ -311,3 +312,36 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase):
|
|||
serializer = self.AlbumSerializer(instance=original, data=data)
|
||||
self.assertEqual(serializer.is_valid(), True)
|
||||
self.assertEqual(serializer.object, expected)
|
||||
|
||||
|
||||
class NestedModelSerializerUpdateTests(TestCase):
|
||||
def test_second_nested_level(self):
|
||||
john = models.Person.objects.create(name="john")
|
||||
|
||||
post = john.blogpost_set.create(title="Test blog post")
|
||||
post.blogpostcomment_set.create(text="I hate this blog post")
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.BlogPostComment
|
||||
|
||||
class BlogPostSerializer(serializers.ModelSerializer):
|
||||
comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
|
||||
class Meta:
|
||||
model = models.BlogPost
|
||||
fields = ('id', 'title', 'comments')
|
||||
|
||||
class PersonSerializer(serializers.ModelSerializer):
|
||||
posts = BlogPostSerializer(many=True, source='blogpost_set')
|
||||
class Meta:
|
||||
model = models.Person
|
||||
fields = ('id', 'name', 'age', 'posts')
|
||||
|
||||
serialize = PersonSerializer(instance=john)
|
||||
deserialize = PersonSerializer(data=serialize.data, instance=john)
|
||||
self.assertTrue(deserialize.is_valid())
|
||||
|
||||
result = deserialize.object
|
||||
result.save()
|
||||
self.assertEqual(result.id, john.id)
|
||||
|
|
28
rest_framework/tests/test_serializers.py
Normal file
28
rest_framework/tests/test_serializers.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.serializers import _resolve_model
|
||||
from rest_framework.tests.models import BasicModel
|
||||
|
||||
|
||||
class ResolveModelTests(TestCase):
|
||||
"""
|
||||
`_resolve_model` should return a Django model class given the
|
||||
provided argument is a Django model class itself, or a properly
|
||||
formatted string representation of one.
|
||||
"""
|
||||
def test_resolve_django_model(self):
|
||||
resolved_model = _resolve_model(BasicModel)
|
||||
self.assertEqual(resolved_model, BasicModel)
|
||||
|
||||
def test_resolve_string_representation(self):
|
||||
resolved_model = _resolve_model('tests.BasicModel')
|
||||
self.assertEqual(resolved_model, BasicModel)
|
||||
|
||||
def test_resolve_non_django_model(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_resolve_model(TestCase)
|
||||
|
||||
def test_resolve_improper_string_representation(self):
|
||||
with self.assertRaises(ValueError):
|
||||
_resolve_model('BasicModel')
|
33
rest_framework/tests/test_status.py
Normal file
33
rest_framework/tests/test_status.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework.status import (
|
||||
is_informational, is_success, is_redirect, is_client_error, is_server_error
|
||||
)
|
||||
|
||||
|
||||
class TestStatus(TestCase):
|
||||
def test_status_categories(self):
|
||||
self.assertFalse(is_informational(99))
|
||||
self.assertTrue(is_informational(100))
|
||||
self.assertTrue(is_informational(199))
|
||||
self.assertFalse(is_informational(200))
|
||||
|
||||
self.assertFalse(is_success(199))
|
||||
self.assertTrue(is_success(200))
|
||||
self.assertTrue(is_success(299))
|
||||
self.assertFalse(is_success(300))
|
||||
|
||||
self.assertFalse(is_redirect(299))
|
||||
self.assertTrue(is_redirect(300))
|
||||
self.assertTrue(is_redirect(399))
|
||||
self.assertFalse(is_redirect(400))
|
||||
|
||||
self.assertFalse(is_client_error(399))
|
||||
self.assertTrue(is_client_error(400))
|
||||
self.assertTrue(is_client_error(499))
|
||||
self.assertFalse(is_client_error(500))
|
||||
|
||||
self.assertFalse(is_server_error(499))
|
||||
self.assertTrue(is_server_error(500))
|
||||
self.assertTrue(is_server_error(599))
|
||||
self.assertFalse(is_server_error(600))
|
51
rest_framework/tests/test_templatetags.py
Normal file
51
rest_framework/tests/test_templatetags.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# encoding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class TemplateTagTests(TestCase):
|
||||
|
||||
def test_add_query_param_with_non_latin_charactor(self):
|
||||
# Ensure we don't double-escape non-latin characters
|
||||
# that are present in the querystring.
|
||||
# See #1314.
|
||||
request = factory.get("/", {'q': '查询'})
|
||||
json_url = add_query_param(request, "format", "json")
|
||||
self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
|
||||
self.assertIn("format=json", json_url)
|
||||
|
||||
|
||||
class Issue1386Tests(TestCase):
|
||||
"""
|
||||
Covers #1386
|
||||
"""
|
||||
|
||||
def test_issue_1386(self):
|
||||
"""
|
||||
Test function urlize_quoted_links with different args
|
||||
"""
|
||||
correct_urls = [
|
||||
"asdf.com",
|
||||
"asdf.net",
|
||||
"www.as_df.org",
|
||||
"as.d8f.ghj8.gov",
|
||||
]
|
||||
for i in correct_urls:
|
||||
res = urlize_quoted_links(i)
|
||||
self.assertNotEqual(res, i)
|
||||
self.assertIn(i, res)
|
||||
|
||||
incorrect_urls = [
|
||||
"mailto://asdf@fdf.com",
|
||||
"asdf.netnet",
|
||||
]
|
||||
for i in incorrect_urls:
|
||||
res = urlize_quoted_links(i)
|
||||
self.assertEqual(i, res)
|
||||
|
||||
# example from issue #1386, this shouldn't raise an exception
|
||||
_ = urlize_quoted_links("asdf:[/p]zxcv.com")
|
|
@ -1,6 +1,8 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from rest_framework.compat import patterns, url
|
||||
|
@ -143,3 +145,20 @@ class TestAPIRequestFactory(TestCase):
|
|||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
self.assertEqual(response.data['user'], 'example')
|
||||
|
||||
def test_upload_file(self):
|
||||
# This is a 1x1 black png
|
||||
simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82')
|
||||
simple_png.name = 'test.png'
|
||||
factory = APIRequestFactory()
|
||||
factory.post('/', data={'image': simple_png})
|
||||
|
||||
def test_request_factory_url_arguments(self):
|
||||
"""
|
||||
This is a non regression test against #1461
|
||||
"""
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/view/?demo=test')
|
||||
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
||||
request = factory.get('/view/', {'demo': 'test'})
|
||||
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import generics, serializers, status
|
||||
|
@ -47,12 +48,18 @@ class ShouldValidateModel(models.Model):
|
|||
class ShouldValidateModelSerializer(serializers.ModelSerializer):
|
||||
renamed = serializers.CharField(source='should_validate_field', required=False)
|
||||
|
||||
def validate_renamed(self, attrs, source):
|
||||
value = attrs[source]
|
||||
if len(value) < 3:
|
||||
raise serializers.ValidationError('Minimum 3 characters.')
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
model = ShouldValidateModel
|
||||
fields = ('renamed',)
|
||||
|
||||
|
||||
class TestPreSaveValidationExclusions(TestCase):
|
||||
class TestPreSaveValidationExclusionsSerializer(TestCase):
|
||||
def test_renamed_fields_are_model_validated(self):
|
||||
"""
|
||||
Ensure fields with 'source' applied do get still get model validation.
|
||||
|
@ -61,6 +68,19 @@ class TestPreSaveValidationExclusions(TestCase):
|
|||
# does not have `blank=True`, so this serializer should not validate.
|
||||
serializer = ShouldValidateModelSerializer(data={'renamed': ''})
|
||||
self.assertEqual(serializer.is_valid(), False)
|
||||
self.assertIn('renamed', serializer.errors)
|
||||
self.assertNotIn('should_validate_field', serializer.errors)
|
||||
|
||||
|
||||
class TestCustomValidationMethods(TestCase):
|
||||
def test_custom_validation_method_is_executed(self):
|
||||
serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn('renamed', serializer.errors)
|
||||
|
||||
def test_custom_validation_method_passing(self):
|
||||
serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
|
||||
class ValidationSerializer(serializers.Serializer):
|
||||
|
@ -83,3 +103,46 @@ class TestAvoidValidation(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual(serializer.errors,
|
||||
{'non_field_errors': ['Invalid data']})
|
||||
|
||||
|
||||
# regression tests for issue: 1493
|
||||
|
||||
class ValidationMaxValueValidatorModel(models.Model):
|
||||
number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)])
|
||||
|
||||
|
||||
class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ValidationMaxValueValidatorModel
|
||||
|
||||
|
||||
class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = ValidationMaxValueValidatorModel
|
||||
serializer_class = ValidationMaxValueValidatorModelSerializer
|
||||
|
||||
|
||||
class TestMaxValueValidatorValidation(TestCase):
|
||||
|
||||
def test_max_value_validation_serializer_success(self):
|
||||
serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99})
|
||||
self.assertTrue(serializer.is_valid())
|
||||
|
||||
def test_max_value_validation_serializer_fails(self):
|
||||
serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101})
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors)
|
||||
|
||||
def test_max_value_validation_success(self):
|
||||
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
|
||||
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json')
|
||||
view = UpdateMaxValueValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_max_value_validation_fail(self):
|
||||
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
|
||||
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
|
||||
view = UpdateMaxValueValidationModel().as_view()
|
||||
response = view(request, pk=obj.pk).render()
|
||||
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
|
42
rest_framework/tests/test_write_only_fields.py
Normal file
42
rest_framework/tests/test_write_only_fields.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ExampleModel(models.Model):
|
||||
email = models.EmailField(max_length=100)
|
||||
password = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class WriteOnlyFieldTests(TestCase):
|
||||
def test_write_only_fields(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
data = {
|
||||
'email': 'foo@example.com',
|
||||
'password': '123'
|
||||
}
|
||||
serializer = ExampleSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.object, data)
|
||||
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
||||
|
||||
def test_write_only_fields_meta(self):
|
||||
class ExampleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ExampleModel
|
||||
fields = ('email', 'password')
|
||||
write_only_fields = ('password',)
|
||||
|
||||
data = {
|
||||
'email': 'foo@example.com',
|
||||
'password': '123'
|
||||
}
|
||||
serializer = ExampleSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertTrue(isinstance(serializer.object, ExampleModel))
|
||||
self.assertEquals(serializer.object.email, data['email'])
|
||||
self.assertEquals(serializer.object.password, data['password'])
|
||||
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
0
rest_framework/tests/users/__init__.py
Normal file
0
rest_framework/tests/users/__init__.py
Normal file
6
rest_framework/tests/users/models.py
Normal file
6
rest_framework/tests/users/models.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
account = models.ForeignKey('accounts.Account', blank=True, null=True, related_name='users')
|
||||
active_record = models.ForeignKey('records.Record', blank=True, null=True)
|
8
rest_framework/tests/users/serializers.py
Normal file
8
rest_framework/tests/users/serializers.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from rest_framework.tests.users.models import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
25
rest_framework/tests/utils.py
Normal file
25
rest_framework/tests/utils.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from contextlib import contextmanager
|
||||
from rest_framework.compat import six
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_setting(setting, value, module=None):
|
||||
"""
|
||||
Temporarily change value of setting for test.
|
||||
|
||||
Optionally reload given module, useful when module uses value of setting on
|
||||
import.
|
||||
"""
|
||||
original_value = getattr(api_settings, setting)
|
||||
setattr(api_settings, setting, value)
|
||||
|
||||
if module is not None:
|
||||
six.moves.reload_module(module)
|
||||
|
||||
yield
|
||||
|
||||
setattr(api_settings, setting, original_value)
|
||||
|
||||
if module is not None:
|
||||
six.moves.reload_module(module)
|
8
rest_framework/tests/views.py
Normal file
8
rest_framework/tests/views.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.tests.models import NullableForeignKeySource
|
||||
from rest_framework.tests.serializers import NullableFKSourceSerializer
|
||||
|
||||
|
||||
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
model = NullableForeignKeySource
|
||||
model_serializer_class = NullableFKSourceSerializer
|
|
@ -136,6 +136,8 @@ class SimpleRateThrottle(BaseThrottle):
|
|||
remaining_duration = self.duration
|
||||
|
||||
available_requests = self.num_requests - len(self.history) + 1
|
||||
if available_requests <= 0:
|
||||
return None
|
||||
|
||||
return remaining_duration / float(available_requests)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Helper classes for parsers.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import Promise
|
||||
from rest_framework.compat import timezone, force_text
|
||||
|
@ -42,8 +43,15 @@ class JSONEncoder(json.JSONEncoder):
|
|||
return str(o.total_seconds())
|
||||
elif isinstance(o, decimal.Decimal):
|
||||
return str(o)
|
||||
elif isinstance(o, QuerySet):
|
||||
return list(o)
|
||||
elif hasattr(o, 'tolist'):
|
||||
return o.tolist()
|
||||
elif hasattr(o, '__getitem__'):
|
||||
try:
|
||||
return dict(o)
|
||||
except:
|
||||
pass
|
||||
elif hasattr(o, '__iter__'):
|
||||
return [i for i in o]
|
||||
return super(JSONEncoder, self).default(o)
|
||||
|
|
|
@ -112,12 +112,13 @@ class APIView(View):
|
|||
|
||||
@property
|
||||
def default_response_headers(self):
|
||||
# TODO: deprecate?
|
||||
# TODO: Only vary by accept if multiple renderers
|
||||
return {
|
||||
headers = {
|
||||
'Allow': ', '.join(self.allowed_methods),
|
||||
'Vary': 'Accept'
|
||||
}
|
||||
if len(self.renderer_classes) > 1:
|
||||
headers['Vary'] = 'Accept'
|
||||
return headers
|
||||
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
@ -130,7 +131,7 @@ class APIView(View):
|
|||
"""
|
||||
If request is not permitted, determine what kind of exception to raise.
|
||||
"""
|
||||
if not self.request.successful_authenticator:
|
||||
if not request.successful_authenticator:
|
||||
raise exceptions.NotAuthenticated()
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
|
@ -294,7 +295,7 @@ class APIView(View):
|
|||
|
||||
# Dispatch methods
|
||||
|
||||
def initialize_request(self, request, *args, **kargs):
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns the initial request object.
|
||||
"""
|
||||
|
|
4
setup.py
4
setup.py
|
@ -12,7 +12,7 @@ def get_version(package):
|
|||
Return package version as listed in `__version__` in `init.py`.
|
||||
"""
|
||||
init_py = open(os.path.join(package, '__init__.py')).read()
|
||||
return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
||||
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
||||
|
||||
|
||||
def get_packages(package):
|
||||
|
@ -55,7 +55,7 @@ if sys.argv[-1] == 'publish':
|
|||
setup(
|
||||
name='djangorestframework',
|
||||
version=version,
|
||||
url='http://django-rest-framework.org',
|
||||
url='http://www.django-rest-framework.org',
|
||||
license='BSD',
|
||||
description='Web APIs for Django, made easy.',
|
||||
author='Tom Christie',
|
||||
|
|
75
tox.ini
75
tox.ini
|
@ -1,110 +1,147 @@
|
|||
[tox]
|
||||
downloadcache = {toxworkdir}/cache/
|
||||
envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
|
||||
envlist = py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
|
||||
|
||||
[testenv]
|
||||
commands = {envpython} rest_framework/runtests/runtests.py
|
||||
|
||||
[testenv:py3.3-django1.7]
|
||||
basepython = python3.3
|
||||
deps = https://www.djangoproject.com/download/1.7b1/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.7]
|
||||
basepython = python3.2
|
||||
deps = https://www.djangoproject.com/download/1.7b1/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.7]
|
||||
basepython = python2.7
|
||||
deps = https://www.djangoproject.com/download/1.7b1/tarball/
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.4
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.3-django1.6]
|
||||
basepython = python3.3
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.6]
|
||||
basepython = python3.2
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.6]
|
||||
basepython = python2.7
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.4
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.6]
|
||||
basepython = python2.6
|
||||
deps = Django==1.6
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.4
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.3-django1.5]
|
||||
basepython = python3.3
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py3.2-django1.5]
|
||||
basepython = python3.2
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = django==1.5.5
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django==1.4.10
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django==1.4.10
|
||||
django-filter==0.6a1
|
||||
django-filter==0.7
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.7-django1.3]
|
||||
basepython = python2.7
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
||||
[testenv:py2.6-django1.3]
|
||||
basepython = python2.6
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth-plus==2.0
|
||||
django-oauth-plus==2.2.1
|
||||
oauth2==1.5.211
|
||||
django-oauth2-provider==0.2.3
|
||||
django-guardian==1.1.1
|
||||
Pillow==2.3.0
|
||||
|
|
Loading…
Reference in New Issue
Block a user