Merge remote-tracking branch 'origin/master' into 2.4.0

Conflicts:
	.travis.yml
	docs/api-guide/fields.md
	docs/api-guide/routers.md
	docs/topics/release-notes.md
	rest_framework/authentication.py
	rest_framework/serializers.py
	rest_framework/templatetags/rest_framework.py
	rest_framework/tests/test_authentication.py
	rest_framework/tests/test_filters.py
	rest_framework/tests/test_hyperlinkedserializers.py
	rest_framework/tests/test_serializer.py
	rest_framework/tests/test_testing.py
	rest_framework/utils/encoders.py
	tox.ini
This commit is contained in:
Xavier Ordoquy 2014-04-13 00:05:57 +02:00
commit d08536ad9d
85 changed files with 1608 additions and 437 deletions

View File

@ -7,18 +7,22 @@ python:
- "3.3" - "3.3"
env: env:
- DJANGO="django==1.6.1" - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
- DJANGO="django==1.6.2"
- DJANGO="django==1.5.5" - DJANGO="django==1.5.5"
- DJANGO="django==1.4.10" - DJANGO="django==1.4.10"
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install defusedxml==0.3 - pip install defusedxml==0.3 Pillow==2.3.0
- pip install django-filter==0.6 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; 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=. - export PYTHONPATH=.
script: script:
@ -26,6 +30,8 @@ script:
matrix: matrix:
exclude: exclude:
- python: "2.6"
env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
- python: "3.2" - python: "3.2"
env: DJANGO="django==1.4.10" env: DJANGO="django==1.4.10"
- python: "3.3" - python: "3.3"

View File

@ -67,7 +67,7 @@ To run the tests, clone the repository, and then:
# Run the tests # Run the tests
rest_framework/runtests/runtests.py 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: 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 tox

View File

@ -1,10 +1,10 @@
# Django REST framework # Django REST framework
**Awesome web-browseable Web APIs.**
[![build-status-image]][travis] [![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 # Overview
@ -99,7 +99,7 @@ That's it, we're done!
# Documentation & Support # 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. 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 # License
Copyright (c) 2011-2013, Tom Christie Copyright (c) 2011-2014, Tom Christie
All rights reserved. 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
@ -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 [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
[index]: http://django-rest-framework.org/ [index]: http://www.django-rest-framework.org/
[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication [oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication
[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication [oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication
[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers [serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers
[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer [modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer
[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views [functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views
[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html [generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html
[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html [viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html
[routers]: http://django-rest-framework.org/api-guide/routers.html [routers]: http://www.django-rest-framework.org/api-guide/routers.html
[serializers]: http://django-rest-framework.org/api-guide/serializers.html [serializers]: http://www.django-rest-framework.org/api-guide/serializers.html
[authentication]: http://django-rest-framework.org/api-guide/authentication.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 [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/ [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 [wlonk]: https://twitter.com/wlonk/status/261689665952833536
[laserllama]: https://twitter.com/laserllama/status/328688333750407168 [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 [urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/ [markdown]: http://pypi.python.org/pypi/Markdown/
[pyyaml]: http://pypi.python.org/pypi/PyYAML [pyyaml]: http://pypi.python.org/pypi/PyYAML

View File

@ -3,17 +3,17 @@
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8"> <meta charset="utf-8">
<title>Django REST framework - 404 - Page not found</title> <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 href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="canonical" href="http://django-rest-framework.org/404"/> <link rel="canonical" href="http://www.django-rest-framework.org/404"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, 404 - Page not found"> <meta name="description" content="Django, API, REST, 404 - Page not found">
<meta name="author" content="Tom Christie"> <meta name="author" content="Tom Christie">
<!-- Le styles --> <!-- Le styles -->
<link href="http://django-rest-framework.org/css/prettify.css" rel="stylesheet"> <link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
<link href="http://django-rest-framework.org/css/bootstrap.css" rel="stylesheet"> <link href="http://www.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://www.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/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
@ -50,63 +50,63 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</a> </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"> <div class="nav-collapse collapse">
<ul class="nav"> <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"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="http://django-rest-framework.org/tutorial/quickstart">Quickstart</a></li> <li><a href="http://www.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://www.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://www.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://www.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://www.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://www.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/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="http://django-rest-framework.org/api-guide/requests">Requests</a></li> <li><a href="http://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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/settings">Settings</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
<ul class="dropdown-menu"> <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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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://www.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/credits">Credits</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
@ -160,7 +160,7 @@
<div id="main-content" class="span12"> <div id="main-content" class="span12">
<h1 id="404-page-not-found" style="text-align: center">404</h1> <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"><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><!--/span-->
</div><!--/row--> </div><!--/row-->
</div><!--/.fluid-container--> </div><!--/.fluid-container-->
@ -176,9 +176,9 @@
<!-- Le javascript <!-- Le javascript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- 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://www.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://www.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/bootstrap-2.1.1-min.js"></script>
<script> <script>
//$('.side-nav').scrollspy() //$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) }; var shiftWindow = function() { scrollBy(0, -50) };

View File

@ -389,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. 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/ [cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [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 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
@ -413,3 +425,13 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
[doac]: https://github.com/Rediker-Software/doac [doac]: https://github.com/Rediker-Software/doac
[rediker]: https://github.com/Rediker-Software [rediker]: https://github.com/Rediker-Software
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md# [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

View File

@ -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. 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: For example, the following request:
@ -86,7 +86,7 @@ Note that the exception handler will only be called for responses generated by r
The **base class** for all exceptions raised inside REST framework. 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: 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:
@ -94,7 +94,7 @@ For example, if your API relies on a third party service that may sometimes be u
class ServiceUnavailable(APIException): class ServiceUnavailable(APIException):
status_code = 503 status_code = 503
detail = 'Service temporarily unavailable, try again later.' default_detail = 'Service temporarily unavailable, try again later.'
## ParseError ## ParseError

View File

@ -28,7 +28,13 @@ Defaults to the name of the field.
### `read_only` ### `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` Defaults to `False`
@ -98,6 +104,7 @@ A serializer definition that looked like this:
expired = serializers.Field(source='has_expired') expired = serializers.Field(source='has_expired')
class Meta: class Meta:
model = Account
fields = ('url', 'owner', 'name', 'expired') fields = ('url', 'owner', 'name', 'expired')
Would produce output similar to: Would produce output similar to:
@ -119,7 +126,7 @@ A field that supports both read and write operations. By itself `WritableField`
## ModelField ## 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'))` 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'))`
@ -168,13 +175,13 @@ or `django.db.models.fields.TextField`.
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation. Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
**Signature:** `CharField(max_length=200, min_length=None, allow_none=False)` **Signature:** `URLField(max_length=200, min_length=None)`
## SlugField ## SlugField
Corresponds to `django.db.models.fields.SlugField`. Corresponds to `django.db.models.fields.SlugField`.
**Signature:** `CharField(max_length=50, min_length=None, allow_none=False)` **Signature:** `SlugField(max_length=50, min_length=None)`
## ChoiceField ## ChoiceField
@ -302,7 +309,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. 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 ## Examples
@ -341,8 +348,17 @@ As an example, let's create a field that can be used represent the class name of
""" """
return obj.__class__ 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 [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 [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 [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 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
[iso8601]: http://www.w3.org/TR/NOTE-datetime [iso8601]: http://www.w3.org/TR/NOTE-datetime
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org

View File

@ -24,7 +24,7 @@ For example:
from myapp.serializers import PurchaseSerializer from myapp.serializers import PurchaseSerializer
from rest_framework import generics from rest_framework import generics
class PurchaseList(generics.ListAPIView) class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
@ -46,7 +46,7 @@ For example if your URL config contained an entry like this:
You could then write a view that returned a purchase queryset filtered by the username portion of the URL: You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
class PurchaseList(generics.ListAPIView) class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
@ -63,7 +63,7 @@ A final example of filtering the initial queryset would be to determine the init
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL: We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
class PurchaseList(generics.ListAPIView) class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer serializer_class = PurchaseSerializer
def get_queryset(self): def get_queryset(self):
@ -264,13 +264,17 @@ For example:
search_fields = ('=username', '=email') 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]. For more details, see the [Django documentation][search-django-admin].
--- ---
## OrderingFilter ## 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 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 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. 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. 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): class UserListView(generics.ListAPIView):
queryset = User.objects.all() queryset = User.objects.all()
serializer = UserSerializer serializer_class = UserSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('username',) ordering = ('username',)

View File

@ -119,7 +119,7 @@ For example:
self.check_object_permissions(self.request, obj) self.check_object_permissions(self.request, obj)
return 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)` #### `get_filter_backends(self)`
@ -362,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. 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 [GenericAPIView]: #genericapiview
[ListModelMixin]: #listmodelmixin [ListModelMixin]: #listmodelmixin
[CreateModelMixin]: #createmodelmixin [CreateModelMixin]: #createmodelmixin
[RetrieveModelMixin]: #retrievemodelmixin [RetrieveModelMixin]: #retrievemodelmixin
[UpdateModelMixin]: #updatemodelmixin [UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin [DestroyModelMixin]: #destroymodelmixin
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk

View File

@ -147,4 +147,14 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use
pagination_serializer_class = CustomPaginationSerializer pagination_serializer_class = CustomPaginationSerializer
paginate_by = 10 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/ [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

View File

@ -134,7 +134,7 @@ By default this field is read-write, although you can change this behavior using
**Arguments**: **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`. * `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. * `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`. * `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**: **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'`. * `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. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
@ -454,6 +454,7 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati
[cite]: http://lwn.net/Articles/193245/ [cite]: http://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward [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 [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md [2.2-announcement]: ../topics/2.2-announcement.md
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers

View File

@ -37,7 +37,19 @@ The example above would generate the following URL patterns:
* URL pattern: `^accounts/$` Name: `'account-list'` * URL pattern: `^accounts/$` Name: `'account-list'`
* URL pattern: `^accounts/{pk}/$` Name: `'account-detail'` * URL pattern: `^accounts/{pk}/$` Name: `'account-detail'`
### Registering additional routes ---
**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 `@detail_route` or `@list_route` will also be routed. Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
For example, given a method like this on the `UserViewSet` class: For example, given a method like this on the `UserViewSet` class:

View File

@ -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. When deserializing data, we can either create a new instance, or update an existing instance.
serializer = CommentSerializer(data=data) # Create new 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. 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 ## 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. 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") raise serializers.ValidationError("finish must occur after start")
return attrs 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. 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() serializer.is_valid()
# False # False
serializer.errors 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. 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 ## 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. 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.
@ -445,6 +464,29 @@ For more specific requirements such as specifying a different lookup for each fi
model = Account model = Account
fields = ('url', 'account_name', 'users', 'created') 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 # Advanced serializer usage

View File

@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i
Default: `None` 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 ## Authentication settings
@ -353,6 +365,12 @@ This should be a function with the following signature:
Default: `'rest_framework.views.exception_handler'` 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 #### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix. The name of a parameter in the URL conf that may be used to provide a format suffix.

View File

@ -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. 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/') response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'}) 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/') response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})

View File

@ -160,7 +160,7 @@ For example, given the following views...
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.ScopedRateThrottle' 'rest_framework.throttling.ScopedRateThrottle',
), ),
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day', 'contacts': '1000/day',

View File

@ -233,7 +233,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
viewsets.GenericViewSet): 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 To use it, override the class and set the `.queryset` and
`.serializer_class` attributes. `.serializer_class` attributes.

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,15 +1,30 @@
<p class="badges"> <p class="badges" height=20px>
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe> <iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a> <a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script> <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"> <img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
</p> </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. 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. * [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]. * 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]. * [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] ![Screenshot][image]
**Above**: *Screenshot from the browsable API*
----
## Requirements ## Requirements
REST framework requires the following: REST framework requires the following:
@ -140,6 +158,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [5 - Relationships & hyperlinked APIs][tut-5] * [5 - Relationships & hyperlinked APIs][tut-5]
* [6 - Viewsets & routers][tut-6] * [6 - Viewsets & routers][tut-6]
There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox].
## API Guide ## API Guide
The API guide is your complete reference manual to all the functionality provided by REST framework. The API guide is your complete reference manual to all the functionality provided by REST framework.
@ -219,7 +239,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
## License ## License
Copyright (c) 2011-2013, Tom Christie Copyright (c) 2011-2014, Tom Christie
All rights reserved. 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
@ -244,7 +264,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis]: 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 [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/ [markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML [yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml [defusedxml]: https://pypi.python.org/pypi/defusedxml

View File

@ -170,31 +170,12 @@
<ul class="nav nav-list side-nav well sidebar-nav-fixed"> <ul class="nav nav-list side-nav well sidebar-nav-fixed">
{{ toc }} {{ toc }}
<div> <div>
<hr>
<p><strong>The team behind REST framework is 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&amp;id=cb6af8e8bd" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<!-- <label for="mce-EMAIL">Keep me posted!</label>
--> <input style="width: 90%" type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
<div class="clear"><input class="btn btn-success" type="submit" value="Yes, keep me posted!" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
</form>
</div> </div>
</style></div> </ul>
</ul>
<!--End mc_embed_signup-->
</div> </div>
</div> </div>

View File

@ -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/ [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [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 [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 [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[marcgibbons]: https://github.com/marcgibbons/ [marcgibbons]: https://github.com/marcgibbons/

View File

@ -60,14 +60,14 @@ To run the tests, clone the repository, and then:
# Setup the virtual environment # Setup the virtual environment
virtualenv env virtualenv env
env/bin/activate source env/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
pip install -r optionals.txt pip install -r optionals.txt
# Run the tests # Run the tests
rest_framework/runtests/runtests.py 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: 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 tox

View File

@ -182,6 +182,7 @@ The following people have helped make REST framework great.
* Ian Foote - [ian-foote] * Ian Foote - [ian-foote]
* Chuck Harmston - [chuckharmston] * Chuck Harmston - [chuckharmston]
* Philip Forget - [philipforget] * Philip Forget - [philipforget]
* Artem Mezhenin - [amezhenin]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
@ -400,3 +401,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[ian-foote]: https://github.com/ian-foote [ian-foote]: https://github.com/ian-foote
[chuckharmston]: https://github.com/chuckharmston [chuckharmston]: https://github.com/chuckharmston
[philipforget]: https://github.com/philipforget [philipforget]: https://github.com/philipforget
[amezhenin]: https://github.com/amezhenin

View File

@ -38,8 +38,6 @@ You can determine your currently installed version using `pip freeze`:
--- ---
## 2.3.x series
### 2.4.0 ### 2.4.0
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`. * `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
@ -50,12 +48,40 @@ You can determine your currently installed version using `pip freeze`:
* Added `cache` attribute to throttles to allow overriding of default cache. * Added `cache` attribute to throttles to allow overriding of default cache.
* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off. * Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off.
### Master
## 2.3.x series
### 2.3.13
## 2.3.x series
**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. * JSON renderer now deals with objects that implement a dict-like interface.
* Fix compatiblity with newer versions of `django-oauth-plus`. * 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: 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: 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 ### 2.3.10
@ -74,7 +100,6 @@ You can determine your currently installed version using `pip freeze`:
* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`. * Fix Django 1.6 exception API compatibility issue caused by `ValidationError`.
* Include errors in HTML forms in browsable API. * Include errors in HTML forms in browsable API.
>>>>>>> master
* Added JSON renderer support for numpy scalars. * Added JSON renderer support for numpy scalars.
* Added `transform_<fieldname>` hooks on serializers for easily modifying field output. * Added `transform_<fieldname>` hooks on serializers for easily modifying field output.
* Added `get_context` hook in `BrowsableAPIRenderer`. * Added `get_context` hook in `BrowsableAPIRenderer`.
@ -100,15 +125,15 @@ 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.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Client sending empty string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. * 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. [*] * Bugfix: Clients setting `page_size=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. [*] Note that the change in `page_size=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): class DisablePaginationMixin(object):
def get_paginate_by(self, queryset=None): def get_paginate_by(self, queryset=None):
if self.request.QUERY_PARAMS['self.paginate_by_param'] == '0': if self.request.QUERY_PARAMS[self.paginate_by_param] == '0':
return None return None
return super(DisablePaginationMixin, self).get_paginate_by(queryset) return super(DisablePaginationMixin, self).get_paginate_by(queryset)

View File

@ -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. 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 :::bash
mkdir ~/env virtualenv env
virtualenv ~/env/tutorial source env/bin/activate
source ~/env/tutorial/bin/activate
Now that we're inside a virtualenv environment, we can install our package requirements. Now that we're inside a virtualenv environment, we can install our package requirements.

View File

@ -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. 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: Add the following import at the top of the file:

View File

@ -18,7 +18,7 @@ if local:
suffix = '.html' suffix = '.html'
index = 'index.html' index = 'index.html'
else: else:
base_url = 'http://django-rest-framework.org' base_url = 'http://www.django-rest-framework.org'
suffix = '' suffix = ''
index = '' index = ''
@ -161,6 +161,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = output.replace('{{ page_id }}', filename[:-3]) output = output.replace('{{ page_id }}', filename[:-3])
output = output.replace('{{ canonical_url }}', canonical_url) 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: if prev_url:
output = output.replace('{{ prev_url }}', prev_url) output = output.replace('{{ prev_url }}', prev_url)
output = output.replace('{{ prev_url_disabled }}', '') output = output.replace('{{ prev_url_disabled }}', '')

View File

@ -5,3 +5,4 @@ django-filter>=0.5.4
django-oauth-plus>=2.2.1 django-oauth-plus>=2.2.1
oauth2>=1.5.211 oauth2>=1.5.211
django-oauth2-provider>=0.2.4 django-oauth2-provider>=0.2.4
Pillow==2.3.0

View File

@ -8,10 +8,10 @@ ______ _____ _____ _____ __ _
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '2.3.10' __version__ = '2.3.13'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2013 Tom Christie' __copyright__ = 'Copyright 2011-2014 Tom Christie'
# Version synonym # Version synonym
VERSION = __version__ VERSION = __version__

View File

@ -7,6 +7,7 @@ import base64
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from django.conf import settings
from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
from rest_framework.compat import oauth2_provider, provider_now, check_nonce from rest_framework.compat import oauth2_provider, provider_now, check_nonce
@ -291,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication):
OAuth 2 authentication backend using `django-oauth2-provider` OAuth 2 authentication backend using `django-oauth2-provider`
""" """
www_authenticate_realm = 'api' www_authenticate_realm = 'api'
allow_query_params_token = settings.DEBUG
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OAuth2Authentication, self).__init__(*args, **kwargs) super(OAuth2Authentication, self).__init__(*args, **kwargs)
@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication):
auth = get_authorization_header(request).split() 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 return None
if len(auth) == 1: if len(auth) == 1:
@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication):
msg = 'Invalid bearer header. Token string should not contain spaces.' msg = 'Invalid bearer header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg) 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): def authenticate_credentials(self, request, access_token):
""" """
@ -326,11 +334,11 @@ class OAuth2Authentication(BaseAuthentication):
""" """
try: 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 # provider_now switches to timezone aware datetime when
# the oauth2_provider version supports to it. # the oauth2_provider version supports to it.
token = token.get(token=access_token, expires__gt=provider_now()) 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') raise exceptions.AuthenticationFailed('Invalid token')
user = token.user user = token.user

View File

@ -1,5 +1,5 @@
import uuid import binascii
import hmac import os
from hashlib import sha1 from hashlib import sha1
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
@ -34,8 +34,7 @@ class Token(models.Model):
return super(Token, self).save(*args, **kwargs) return super(Token, self).save(*args, **kwargs)
def generate_key(self): def generate_key(self):
unique = uuid.uuid4() return binascii.hexlify(os.urandom(20))
return hmac.new(unique.bytes, digestmod=sha1).hexdigest()
def __unicode__(self): def __unicode__(self):
return self.key return self.key

View File

@ -121,7 +121,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload from django.test.client import FakePayload
try: try:
# In 1.5 the test client uses force_bytes # 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: except ImportError:
# In 1.4 the test client just uses smart_str # In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
@ -216,13 +216,10 @@ except (ImportError, ImproperlyConfigured):
# OAuth 2 support is optional # OAuth 2 support is optional
try: try:
import provider.oauth2 as oauth2_provider import provider as oauth2_provider
from provider.oauth2 import models as oauth2_provider_models
from provider.oauth2 import forms as oauth2_provider_forms
from provider import scope as oauth2_provider_scope from provider import scope as oauth2_provider_scope
from provider import constants as oauth2_constants from provider import constants as oauth2_constants
from provider import __version__ as provider_version if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
if provider_version in ('0.2.3', '0.2.4'):
# 0.2.3 and 0.2.4 are supported version that do not support # 0.2.3 and 0.2.4 are supported version that do not support
# timezone aware datetimes # timezone aware datetimes
import datetime import datetime
@ -232,8 +229,6 @@ try:
from django.utils.timezone import now as provider_now from django.utils.timezone import now as provider_now
except ImportError: except ImportError:
oauth2_provider = None oauth2_provider = None
oauth2_provider_models = None
oauth2_provider_forms = None
oauth2_provider_scope = None oauth2_provider_scope = None
oauth2_constants = None oauth2_constants = None
provider_now = None provider_now = None
@ -251,3 +246,23 @@ if six.PY3:
else: else:
def is_non_str_iterable(obj): def is_non_str_iterable(obj):
return hasattr(obj, '__iter__') 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

View File

@ -6,47 +6,42 @@ In addition Django's built in 403 and 404 exceptions are handled.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status from rest_framework import status
import math
class APIException(Exception): class APIException(Exception):
""" """
Base class for REST framework exceptions. 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
def __str__(self):
return self.detail
class ParseError(APIException): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.' default_detail = 'Malformed request.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
class AuthenticationFailed(APIException): class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.' default_detail = 'Incorrect authentication credentials.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
class NotAuthenticated(APIException): class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Authentication credentials were not provided.' default_detail = 'Authentication credentials were not provided.'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
class PermissionDenied(APIException): class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.' 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): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
@ -75,14 +70,14 @@ class UnsupportedMediaType(APIException):
class Throttled(APIException): class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS 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." extra_detail = "Expected available in %d second%s."
def __init__(self, wait=None, detail=None): def __init__(self, wait=None, detail=None):
import math if wait is None:
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:
self.detail = detail or self.default_detail 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)

View File

@ -166,7 +166,7 @@ class Field(object):
Called to set up a field prior to field_to_native or field_from_native. Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer. parent - The parent serializer.
model_field - The model field this field corresponds to, if one exists. field_name - The name of the field being initialized.
""" """
self.parent = parent self.parent = parent
self.root = parent.root or parent self.root = parent.root or parent
@ -248,6 +248,7 @@ class WritableField(Field):
""" """
Base for read/write fields. Base for read/write fields.
""" """
write_only = False
default_validators = [] default_validators = []
default_error_messages = { default_error_messages = {
'required': _('This field is required.'), 'required': _('This field is required.'),
@ -257,13 +258,17 @@ class WritableField(Field):
default = None default = None
def __init__(self, source=None, label=None, help_text=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, validators=[], error_messages=None, widget=None,
default=None, blank=None): default=None, blank=None):
super(WritableField, self).__init__(source=source, label=label, help_text=help_text) super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
self.read_only = read_only 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: if required is None:
self.required = not(read_only) self.required = not(read_only)
else: else:
@ -291,6 +296,11 @@ class WritableField(Field):
result.validators = self.validators[:] result.validators = self.validators[:]
return result return result
def get_default_value(self):
if is_simple_callable(self.default):
return self.default()
return self.default
def validate(self, value): def validate(self, value):
if value in validators.EMPTY_VALUES and self.required: if value in validators.EMPTY_VALUES and self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
@ -313,6 +323,11 @@ class WritableField(Field):
if errors: if errors:
raise ValidationError(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): def field_from_native(self, data, files, field_name, into):
""" """
Given a dictionary and a field name, updates the dictionary `into`, Given a dictionary and a field name, updates the dictionary `into`,
@ -334,10 +349,7 @@ class WritableField(Field):
except KeyError: except KeyError:
if self.default is not None and not self.partial: if self.default is not None and not self.partial:
# Note: partial updates shouldn't set defaults # Note: partial updates shouldn't set defaults
if is_simple_callable(self.default): native = self.get_default_value()
native = self.default()
else:
native = self.default
else: else:
if self.required: if self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
@ -465,6 +477,7 @@ class URLField(CharField):
type_label = 'url' type_label = 'url'
def __init__(self, **kwargs): def __init__(self, **kwargs):
if not 'validators' in kwargs:
kwargs['validators'] = [validators.URLValidator()] kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs) super(URLField, self).__init__(**kwargs)

View File

@ -3,8 +3,10 @@ Provides generic filtering backends that can be used to filter the results
returned by list views. returned by list views.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from rest_framework.compat import django_filters, six, guardian, get_model_name from rest_framework.compat import django_filters, six, guardian, get_model_name
from rest_framework.settings import api_settings
from functools import reduce from functools import reduce
import operator import operator
@ -68,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend):
class SearchFilter(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): def get_search_terms(self, request):
""" """
@ -106,7 +109,9 @@ class SearchFilter(BaseFilterBackend):
class OrderingFilter(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): def get_ordering(self, request):
""" """
@ -122,17 +127,34 @@ class OrderingFilter(BaseFilterBackend):
return (ordering,) return (ordering,)
return ordering return ordering
def remove_invalid_fields(self, queryset, ordering): def remove_invalid_fields(self, queryset, ordering, view):
field_names = [field.name for field in queryset.model._meta.fields] valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
field_names += queryset.query.aggregates.keys()
return [term for term in ordering if term.lstrip('-') in field_names] 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): def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request) ordering = self.get_ordering(request)
if ordering: if ordering:
# Skip any incorrect parameters # Skip any incorrect parameters
ordering = self.remove_invalid_fields(queryset, ordering) ordering = self.remove_invalid_fields(queryset, ordering, view)
if not ordering: if not ordering:
# Use 'ordering' attribute by default # Use 'ordering' attribute by default

View File

@ -352,7 +352,7 @@ class GenericAPIView(views.APIView):
def post_delete(self, obj): def post_delete(self, obj):
""" """
Placeholder method for calling after saving an object. Placeholder method for calling after deleting an object.
""" """
pass pass

View File

@ -11,6 +11,7 @@ from django.http import Http404
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.settings import api_settings
import warnings import warnings
@ -60,7 +61,7 @@ class CreateModelMixin(object):
def get_success_headers(self, data): def get_success_headers(self, data):
try: try:
return {'Location': data['url']} return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError): except (TypeError, KeyError):
return {} return {}
@ -115,30 +116,27 @@ class UpdateModelMixin(object):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none() 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, serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial) files=request.FILES, partial=partial)
if serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try: try:
self.pre_save(serializer.object) self.pre_save(serializer.object)
except ValidationError as err: except ValidationError as err:
# full_clean on model instance may be called in pre_save, so we # full_clean on model instance may be called in pre_save,
# have to handle eventual errors. # so we have to handle eventual errors.
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) 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)
return Response(serializer.errors, 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): def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True kwargs['partial'] = True

View File

@ -33,6 +33,7 @@ class RelatedField(WritableField):
many_widget = widgets.SelectMultiple many_widget = widgets.SelectMultiple
form_field_class = forms.ChoiceField form_field_class = forms.ChoiceField
many_form_field_class = forms.MultipleChoiceField many_form_field_class = forms.MultipleChoiceField
null_values = (None, '', 'None')
cache_choices = False cache_choices = False
empty_label = None empty_label = None
@ -50,6 +51,8 @@ class RelatedField(WritableField):
super(RelatedField, self).__init__(*args, **kwargs) super(RelatedField, self).__init__(*args, **kwargs)
if not self.required: if not self.required:
# Accessed in ModelChoiceIterator django/forms/models.py:1034
# If set adds empty choice.
self.empty_label = BLANK_CHOICE_DASH[0][1] self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset self.queryset = queryset
@ -57,16 +60,11 @@ class RelatedField(WritableField):
def initialize(self, parent, field_name): def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name) super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only: if self.queryset is None and not self.read_only:
try:
manager = getattr(self.parent.opts.model, self.source or field_name) manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all() self.queryset = manager.related.model._default_manager.all()
else: # Reverse else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all() 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)
### We need this stuff to make form choices work... ### We need this stuff to make form choices work...
@ -115,6 +113,14 @@ class RelatedField(WritableField):
choices = property(_get_choices, _set_choices) 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... ### Regular serializer stuff...
def field_to_native(self, obj, field_name): def field_to_native(self, obj, field_name):
@ -163,11 +169,11 @@ class RelatedField(WritableField):
except KeyError: except KeyError:
if self.partial: if self.partial:
return return
value = [] if self.many else None value = self.get_default_value()
if value in (None, '') and self.required: if value in self.null_values:
if self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
elif value in (None, ''):
into[(self.source or field_name)] = None into[(self.source or field_name)] = None
elif self.many: elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value] into[(self.source or field_name)] = [self.from_native(item) for item in value]

View File

@ -10,6 +10,7 @@ from __future__ import unicode_literals
import copy import copy
import json import json
import django
from django import forms from django import forms
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
@ -145,7 +146,7 @@ class XMLRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None): 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: if data is None:
return '' return ''
@ -195,7 +196,7 @@ class YAMLRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None): 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' assert yaml, 'YAMLRenderer requires pyyaml to be installed'
@ -543,6 +544,14 @@ class BrowsableAPIRenderer(BaseRenderer):
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form 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 = { context = {
'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view, 'view': view,
@ -554,6 +563,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'breadcrumblist': self.get_breadcrumbs(request), 'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods, 'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes], 'available_formats': [renderer.format for renderer in view.renderer_classes],
'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request), 'put_form': self.get_rendered_html_form(view, 'PUT', request),
'post_form': self.get_rendered_html_form(view, 'POST', request), 'post_form': self.get_rendered_html_form(view, 'POST', request),
@ -597,7 +607,7 @@ class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart' format = 'multipart'
charset = 'utf-8' 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): def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data) return encode_multipart(self.BOUNDARY, data)

View File

@ -223,7 +223,7 @@ class Request(object):
def user(self, value): def user(self, value):
""" """
Sets the user on the current request. This is necessary to maintain 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. set in the login and logout functions.
""" """
self._user = value self._user = value
@ -279,7 +279,6 @@ class Request(object):
if not _hasattr(self, '_method'): if not _hasattr(self, '_method'):
self._method = self._request.method self._method = self._request.method
if self._method == 'POST':
# Allow X-HTTP-METHOD-OVERRIDE header # Allow X-HTTP-METHOD-OVERRIDE header
self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
self._method) self._method)
@ -347,7 +346,7 @@ class Request(object):
media_type = self.content_type media_type = self.content_type
if stream is None or media_type is None: 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() empty_files = MultiValueDict()
return (empty_data, empty_files) return (empty_data, empty_files)
@ -363,7 +362,7 @@ class Request(object):
# re-raise. Ensures we don't simply repeat the error when # re-raise. Ensures we don't simply repeat the error when
# attempting to render the browsable renderer response, or when # attempting to render the browsable renderer response, or when
# logging the request or similar. # logging the request or similar.
self._data = QueryDict('', self._request._encoding) self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict() self._files = MultiValueDict()
raise raise

View File

@ -26,6 +26,10 @@ def usage():
def main(): def main():
try:
django.setup()
except AttributeError:
pass
TestRunner = get_runner(settings) TestRunner = get_runner(settings)
test_runner = TestRunner() test_runner = TestRunner()

View File

@ -97,6 +97,9 @@ INSTALLED_APPS = (
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_framework.tests', '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 # OAuth is optional and won't work if there is no oauth_provider & oauth2

View File

@ -13,12 +13,15 @@ response content is handled by parsers and renderers.
from __future__ import unicode_literals from __future__ import unicode_literals
import copy import copy
import datetime import datetime
import inspect
import types import types
from decimal import Decimal from decimal import Decimal
from django.db import models from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from rest_framework.compat import six 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: # Note: We do the following so that users of the framework can use this style:
# #
@ -31,6 +34,27 @@ from rest_framework.relations import *
from rest_framework.fields 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): def pretty_name(name):
"""Converts 'first_name' to 'First name'""" """Converts 'first_name' to 'First name'"""
if not name: if not name:
@ -325,12 +349,13 @@ class BaseSerializer(WritableField):
method = getattr(self, 'transform_%s' % field_name, None) method = getattr(self, 'transform_%s' % field_name, None)
if callable(method): if callable(method):
value = method(obj, value) value = method(obj, value)
if not getattr(field, 'write_only', False):
ret[key] = value ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value) ret.fields[key] = self.augment_field(field, field_name, key, value)
return ret return ret
def from_native(self, data, files): def from_native(self, data, files=None):
""" """
Deserialize primitives -> objects. Deserialize primitives -> objects.
""" """
@ -360,6 +385,9 @@ class BaseSerializer(WritableField):
Override default so that the serializer can be used as a nested field Override default so that the serializer can be used as a nested field
across relationships. across relationships.
""" """
if self.write_only:
return None
if self.source == '*': if self.source == '*':
return self.to_native(obj) return self.to_native(obj)
@ -404,6 +432,15 @@ class BaseSerializer(WritableField):
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
return return
if self.source == '*':
if 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 # 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 = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
@ -414,15 +451,6 @@ class BaseSerializer(WritableField):
is_simple_callable(getattr(obj, 'all', None))): is_simple_callable(getattr(obj, 'all', None))):
obj = obj.all() obj = obj.all()
if self.source == '*':
if 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:
kwargs = { kwargs = {
'instance': obj, 'instance': obj,
'data': value, 'data': value,
@ -467,7 +495,7 @@ class BaseSerializer(WritableField):
else: else:
many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
if many: 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.', 'Use the `many=True` flag when instantiating the serializer.',
DeprecationWarning, stacklevel=3) DeprecationWarning, stacklevel=3)
@ -524,7 +552,16 @@ class BaseSerializer(WritableField):
if self._data is None: if self._data is None:
obj = self.object obj = self.object
if self.many: if self.many is not None:
many = self.many
else:
many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
if many:
warnings.warn('Implicit list/queryset serialization is deprecated. '
'Use the `many=True` flag when instantiating the serializer.',
DeprecationWarning, stacklevel=2)
if many:
self._data = [self.to_native(item) for item in obj] self._data = [self.to_native(item) for item in obj]
else: else:
self._data = self.to_native(obj) self._data = self.to_native(obj)
@ -578,6 +615,7 @@ class ModelSerializerOptions(SerializerOptions):
super(ModelSerializerOptions, self).__init__(meta) super(ModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, 'model', None) self.model = getattr(meta, 'model', None)
self.read_only_fields = getattr(meta, 'read_only_fields', ()) self.read_only_fields = getattr(meta, 'read_only_fields', ())
self.write_only_fields = getattr(meta, 'write_only_fields', ())
class ModelSerializer(Serializer): class ModelSerializer(Serializer):
@ -641,7 +679,7 @@ class ModelSerializer(Serializer):
if model_field.rel: if model_field.rel:
to_many = isinstance(model_field, to_many = isinstance(model_field,
models.fields.related.ManyToManyField) 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: if to_many and not model_field.rel.through._meta.auto_created:
has_through_model = True has_through_model = True
@ -714,20 +752,38 @@ class ModelSerializer(Serializer):
ret[accessor_name] = field ret[accessor_name] = field
# Add the `read_only` flag to any fields that have bee specified # 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 been specified
# in the `read_only_fields` option # in the `read_only_fields` option
for field_name in self.opts.read_only_fields: for field_name in self.opts.read_only_fields:
assert field_name not in self.base_fields.keys(), \ assert field_name not in self.base_fields.keys(), (
"field '%s' on serializer '%s' specified in " \ "field '%s' on serializer '%s' specified in "
"`read_only_fields`, but also added " \ "`read_only_fields`, but also added "
"as an explicit field. Remove it from `read_only_fields`." % \ "as an explicit field. Remove it from `read_only_fields`." %
(field_name, self.__class__.__name__) (field_name, self.__class__.__name__))
assert field_name in ret, \ assert field_name in ret, (
"Non-existant field '%s' specified in `read_only_fields` " \ "Non-existant field '%s' specified in `read_only_fields` "
"on serializer '%s'." % \ "on serializer '%s'." %
(field_name, self.__class__.__name__) (field_name, self.__class__.__name__))
ret[field_name].read_only = True 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 return ret
def get_pk_field(self, model_field): def get_pk_field(self, model_field):
@ -829,7 +885,7 @@ class ModelSerializer(Serializer):
except KeyError: except KeyError:
return ModelField(model_field=model_field, **kwargs) 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. Return a list of field names to exclude from model validation.
""" """
@ -841,6 +897,7 @@ class ModelSerializer(Serializer):
field_name = field.source or field_name field_name = field.source or field_name
if field_name in exclusions \ if field_name in exclusions \
and not field.read_only \ and not field.read_only \
and (field.required or hasattr(instance, field_name)) \
and not isinstance(field, Serializer): and not isinstance(field, Serializer):
exclusions.remove(field_name) exclusions.remove(field_name)
return exclusions return exclusions
@ -855,7 +912,7 @@ class ModelSerializer(Serializer):
the full_clean validation checking. the full_clean validation checking.
""" """
try: try:
instance.full_clean(exclude=self.get_validation_exclusions()) instance.full_clean(exclude=self.get_validation_exclusions(instance))
except ValidationError as err: except ValidationError as err:
self._errors = err.message_dict self._errors = err.message_dict
return None return None
@ -883,7 +940,7 @@ class ModelSerializer(Serializer):
m2m_data[field_name] = attrs.pop(field_name) m2m_data[field_name] = attrs.pop(field_name)
# Forward m2m relations # 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: if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name) m2m_data[field.name] = attrs.pop(field.name)
@ -979,6 +1036,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
super(HyperlinkedModelSerializerOptions, self).__init__(meta) super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.view_name = getattr(meta, 'view_name', None) self.view_name = getattr(meta, 'view_name', None)
self.lookup_field = getattr(meta, 'lookup_field', 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): class HyperlinkedModelSerializer(ModelSerializer):
@ -997,13 +1055,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.view_name is None: if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model) 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( url_field = self._hyperlink_identify_field_class(
view_name=self.opts.view_name, view_name=self.opts.view_name,
lookup_field=self.opts.lookup_field lookup_field=self.opts.lookup_field
) )
ret = self._dict_class() ret = self._dict_class()
ret['url'] = url_field ret[self.opts.url_field_name] = url_field
ret.update(fields) ret.update(fields)
fields = ret fields = ret
@ -1039,7 +1097,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
We need to override the default, to use the url as the identity. We need to override the default, to use the url as the identity.
""" """
try: try:
return data.get('url', None) return data.get(self.opts.url_field_name, None)
except AttributeError: except AttributeError:
return None return None

View File

@ -70,6 +70,10 @@ DEFAULTS = {
'PAGINATE_BY_PARAM': None, 'PAGINATE_BY_PARAM': None,
'MAX_PAGINATE_BY': None, 'MAX_PAGINATE_BY': None,
# Filtering
'SEARCH_PARAM': 'search',
'ORDERING_PARAM': 'ordering',
# Authentication # Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,
@ -96,6 +100,7 @@ DEFAULTS = {
'URL_FORMAT_OVERRIDE': 'format', 'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format', 'FORMAT_SUFFIX_KWARG': 'format',
'URL_FIELD_NAME': 'url',
# Input and output formats # Input and output formats
'DATE_INPUT_FORMATS': ( 'DATE_INPUT_FORMATS': (

View File

@ -34,7 +34,7 @@
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<span href="/"> <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> </span>
<ul class="nav pull-right"> <ul class="nav pull-right">
{% block userlinks %} {% block userlinks %}
@ -119,7 +119,7 @@
</div> </div>
<div class="response-info"> <div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} <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 %} {% endfor %}
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} </div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div> </div>

View File

@ -2,10 +2,12 @@ from __future__ import unicode_literals, absolute_import
from django import template from django import template
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict from django.http import QueryDict
from django.utils.html import escape, smart_urlquote from django.utils.encoding import iri_to_uri
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from rest_framework.compat import urlparse, force_text, six from rest_framework.compat import urlparse, force_text, six
import re, string from django.utils.html import smart_urlquote
import re
register = template.Library() register = template.Library()
@ -61,7 +63,9 @@ def add_query_param(request, key, val):
""" """
Add a query parameter to the current request url, and return the new url. 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 @register.filter
@ -103,6 +107,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+$') 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 @register.filter
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True): def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
""" """
@ -125,7 +140,6 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
safe_input = isinstance(text, SafeData) safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_text(text)) words = word_split_re.split(force_text(text))
for i, word in enumerate(words): for i, word in enumerate(words):
match = None
if '.' in word or '@' in word or ':' in word: if '.' in word or '@' in word or ':' in word:
# Deal with punctuation. # Deal with punctuation.
lead, middle, trail = '', word, '' lead, middle, trail = '', word, ''
@ -147,9 +161,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
url = None url = None
nofollow_attr = ' rel="nofollow"' if nofollow else '' nofollow_attr = ' rel="nofollow"' if nofollow else ''
if simple_url_re.match(middle): if simple_url_re.match(middle):
url = smart_urlquote(middle) url = smart_urlquote_wrapper(middle)
elif simple_url_2_re.match(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): elif not ':' in middle and simple_email_re.match(middle):
local, domain = middle.rsplit('@', 1) local, domain = middle.rsplit('@', 1)
try: try:

View File

@ -8,6 +8,7 @@ from django.conf import settings
from django.test.client import Client as DjangoClient from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler from django.test.client import ClientHandler
from django.test import testcases from django.test import testcases
from django.utils.http import urlencode
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import RequestFactory as DjangoRequestFactory
from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.compat import force_bytes_or_smart_bytes, six
@ -71,6 +72,17 @@ class APIRequestFactory(DjangoRequestFactory):
return ret, content_type 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): def post(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type) data, content_type = self._encode_data(data, format, content_type)
return self.generic('POST', path, data, content_type, **extra) return self.generic('POST', path, data, content_type, **extra)

View 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')

View 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

View File

@ -103,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel):
class Album(RESTFrameworkModel): class Album(RESTFrameworkModel):
title = models.CharField(max_length=100, unique=True) title = models.CharField(max_length=100, unique=True)
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
class Photo(RESTFrameworkModel): class Photo(RESTFrameworkModel):
description = models.TextField() description = models.TextField()
@ -168,3 +168,10 @@ class NullableOneToOneSource(RESTFrameworkModel):
class BasicModelSerializer(serializers.ModelSerializer): class BasicModelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = BasicModel 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()

View File

View 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)

View 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

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from django.utils.http import urlencode
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework import permissions from rest_framework import permissions
@ -19,7 +20,7 @@ from rest_framework.authentication import (
OAuth2Authentication OAuth2Authentication
) )
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
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.compat import oauth, oauth_provider
from rest_framework.test import APIRequestFactory, APIClient from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView from rest_framework.views import APIView
@ -53,10 +54,14 @@ urlpatterns = patterns('',
permission_classes=[permissions.TokenHasReadWriteScope])) permission_classes=[permissions.TokenHasReadWriteScope]))
) )
class OAuth2AuthenticationDebug(OAuth2Authentication):
allow_query_params_token = True
if oauth2_provider is not None: if oauth2_provider is not None:
urlpatterns += patterns('', urlpatterns += patterns('',
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), 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], url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
permission_classes=[permissions.TokenHasReadWriteScope])), permission_classes=[permissions.TokenHasReadWriteScope])),
) )
@ -488,7 +493,7 @@ class OAuth2Tests(TestCase):
self.ACCESS_TOKEN = "access_token" self.ACCESS_TOKEN = "access_token"
self.REFRESH_TOKEN = "refresh_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_id=self.CLIENT_ID,
client_secret=self.CLIENT_SECRET, client_secret=self.CLIENT_SECRET,
redirect_uri='', redirect_uri='',
@ -497,12 +502,12 @@ class OAuth2Tests(TestCase):
user=None, 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, token=self.ACCESS_TOKEN,
client=self.oauth2_client, client=self.oauth2_client,
user=self.user, 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, user=self.user,
access_token=self.access_token, access_token=self.access_token,
client=self.oauth2_client client=self.oauth2_client
@ -545,6 +550,27 @@ class OAuth2Tests(TestCase):
response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200) 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') @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_post_form_passing_auth(self): def test_post_form_passing_auth(self):
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""

View File

@ -860,7 +860,9 @@ class SlugFieldTests(TestCase):
class URLFieldTests(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): class URLFieldModel(RESTFrameworkModel):
@ -902,6 +904,11 @@ class URLFieldTests(TestCase):
self.assertEqual(getattr(serializer.fields['url_field'], self.assertEqual(getattr(serializer.fields['url_field'],
'max_length'), 20) '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): class FieldMetadata(TestCase):
def setUp(self): def setUp(self):

View File

@ -1,25 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.conf.urls import patterns, url
from django.db import models from django.db import models
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from django.conf.urls import patterns, url
from rest_framework import generics, serializers, status, filters from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from rest_framework.tests.models import BasicModel
from .models import FilterableItem
from .utils import temporary_setting
factory = APIRequestFactory() 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: if django_filters:
# Basic filter on a list view. # Basic filter on a list view.
class FilterFieldsRootView(generics.ListCreateAPIView): class FilterFieldsRootView(generics.ListCreateAPIView):
@ -129,7 +125,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the decimal filter works. # Tests that the decimal filter works.
search_decimal = Decimal('2.25') search_decimal = Decimal('2.25')
request = factory.get('/?decimal=%s' % search_decimal) request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['decimal'] == search_decimal] expected_data = [f for f in self.data if f['decimal'] == search_decimal]
@ -137,7 +133,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the date filter works. # Tests that the date filter works.
search_date = datetime.date(2012, 9, 22) 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() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['date'] == search_date] expected_data = [f for f in self.data if f['date'] == search_date]
@ -152,7 +148,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the decimal filter works. # Tests that the decimal filter works.
search_decimal = Decimal('2.25') search_decimal = Decimal('2.25')
request = factory.get('/?decimal=%s' % search_decimal) request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['decimal'] == search_decimal] expected_data = [f for f in self.data if f['decimal'] == search_decimal]
@ -185,7 +181,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the decimal filter set with 'lt' in the filter class works. # Tests that the decimal filter set with 'lt' in the filter class works.
search_decimal = Decimal('4.25') search_decimal = Decimal('4.25')
request = factory.get('/?decimal=%s' % search_decimal) request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['decimal'] < search_decimal] expected_data = [f for f in self.data if f['decimal'] < search_decimal]
@ -193,7 +189,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the date filter set with 'gt' in the filter class works. # Tests that the date filter set with 'gt' in the filter class works.
search_date = datetime.date(2012, 10, 2) 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() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['date'] > search_date] expected_data = [f for f in self.data if f['date'] > search_date]
@ -201,7 +197,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that the text filter set with 'icontains' in the filter class works. # Tests that the text filter set with 'icontains' in the filter class works.
search_text = 'ff' search_text = 'ff'
request = factory.get('/?text=%s' % search_text) request = factory.get('/', {'text': '%s' % search_text})
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if search_text in f['text'].lower()] expected_data = [f for f in self.data if search_text in f['text'].lower()]
@ -210,7 +206,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
# Tests that multiple filters works. # Tests that multiple filters works.
search_decimal = Decimal('5.25') search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2) 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() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['date'] > search_date and expected_data = [f for f in self.data if f['date'] > search_date and
@ -235,7 +234,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
view = FilterFieldsRootView.as_view() view = FilterFieldsRootView.as_view()
search_integer = 10 search_integer = 10
request = factory.get('/?integer=%s' % search_integer) request = factory.get('/', {'integer': '%s' % search_integer})
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -266,14 +265,18 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
# Tests that the decimal filter set that should fail. # Tests that the decimal filter set that should fail.
search_decimal = Decimal('4.25') search_decimal = Decimal('4.25')
high_item = self.objects.filter(decimal__gt=search_decimal)[0] 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) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Tests that the decimal filter set that should succeed. # Tests that the decimal filter set that should succeed.
search_decimal = Decimal('4.25') search_decimal = Decimal('4.25')
low_item = self.objects.filter(decimal__lt=search_decimal)[0] low_item = self.objects.filter(decimal__lt=search_decimal)[0]
low_item_data = self._serialize_object(low_item) 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.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, low_item_data) self.assertEqual(response.data, low_item_data)
@ -282,7 +285,11 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
search_date = datetime.date(2012, 10, 2) search_date = datetime.date(2012, 10, 2)
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0] valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
valid_item_data = self._serialize_object(valid_item) 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.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, valid_item_data) self.assertEqual(response.data, valid_item_data)
@ -316,7 +323,7 @@ class SearchFilterTests(TestCase):
search_fields = ('title', 'text') search_fields = ('title', 'text')
view = SearchListView.as_view() view = SearchListView.as_view()
request = factory.get('?search=b') request = factory.get('/', {'search': 'b'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -333,7 +340,7 @@ class SearchFilterTests(TestCase):
search_fields = ('=title', 'text') search_fields = ('=title', 'text')
view = SearchListView.as_view() view = SearchListView.as_view()
request = factory.get('?search=zzz') request = factory.get('/', {'search': 'zzz'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -349,7 +356,7 @@ class SearchFilterTests(TestCase):
search_fields = ('title', '^text') search_fields = ('title', '^text')
view = SearchListView.as_view() view = SearchListView.as_view()
request = factory.get('?search=b') request = factory.get('/', {'search': 'b'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -358,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): class OrdringFilterModel(models.Model):
title = models.CharField(max_length=20) title = models.CharField(max_length=20)
@ -369,7 +394,6 @@ class OrderingFilterRelatedModel(models.Model):
related_name="relateds") related_name="relateds")
class OrderingFilterTests(TestCase): class OrderingFilterTests(TestCase):
def setUp(self): def setUp(self):
# Sequence of title/text is: # Sequence of title/text is:
@ -395,9 +419,10 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('?ordering=text') request = factory.get('/', {'ordering': 'text'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -413,9 +438,10 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('?ordering=-text') request = factory.get('/', {'ordering': '-text'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -431,9 +457,10 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('?ordering=foobar') request = factory.get('/', {'ordering': 'foobar'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -449,6 +476,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
oredering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('') request = factory.get('')
@ -467,6 +495,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = 'title' ordering = 'title'
ordering_fields = ('text',)
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('') request = factory.get('')
@ -495,11 +524,12 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = 'title' ordering = 'title'
ordering_fields = '__all__'
queryset = OrdringFilterModel.objects.all().annotate( queryset = OrdringFilterModel.objects.all().annotate(
models.Count("relateds")) models.Count("relateds"))
view = OrderingListView.as_view() view = OrderingListView.as_view()
request = factory.get('?ordering=relateds__count') request = factory.get('/', {'ordering': 'relateds__count'})
response = view(request) response = view(request)
self.assertEqual( self.assertEqual(
response.data, response.data,
@ -510,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
]
)

View File

@ -4,8 +4,10 @@ from django.contrib.contenttypes.generic import GenericRelation, GenericForeignK
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.compat import python_2_unicode_compatible
@python_2_unicode_compatible
class Tag(models.Model): class Tag(models.Model):
""" """
Tags have a descriptive slug, and are attached to an arbitrary object. Tags have a descriptive slug, and are attached to an arbitrary object.
@ -15,10 +17,11 @@ class Tag(models.Model):
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id') tagged_item = GenericForeignKey('content_type', 'object_id')
def __unicode__(self): def __str__(self):
return self.tag return self.tag
@python_2_unicode_compatible
class Bookmark(models.Model): class Bookmark(models.Model):
""" """
A URL bookmark that may have multiple tags attached. A URL bookmark that may have multiple tags attached.
@ -26,10 +29,11 @@ class Bookmark(models.Model):
url = models.URLField() url = models.URLField()
tags = GenericRelation(Tag) tags = GenericRelation(Tag)
def __unicode__(self): def __str__(self):
return 'Bookmark: %s' % self.url return 'Bookmark: %s' % self.url
@python_2_unicode_compatible
class Note(models.Model): class Note(models.Model):
""" """
A textual note that may have multiple tags attached. A textual note that may have multiple tags attached.
@ -37,7 +41,7 @@ class Note(models.Model):
text = models.TextField() text = models.TextField()
tags = GenericRelation(Tag) tags = GenericRelation(Tag)
def __unicode__(self): def __str__(self):
return 'Note: %s' % self.text return 'Note: %s' % self.text
@ -69,6 +73,35 @@ class TestGenericRelations(TestCase):
} }
self.assertEqual(serializer.data, expected) 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): def test_generic_fk(self):
""" """
Test a relationship that spans a GenericForeignKey field. Test a relationship that spans a GenericForeignKey field.

View File

@ -50,7 +50,7 @@ class TemplateHTMLRendererTests(TestCase):
""" """
self.get_template = django.template.loader.get_template 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': if template_name == 'example.html':
return Template("example: {{ object }}") return Template("example: {{ object }}")
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name)
@ -108,11 +108,13 @@ class TemplateHTMLRendererExceptionTests(TestCase):
def test_not_found_html_view_with_template(self): def test_not_found_html_view_with_template(self):
response = self.client.get('/not_found') response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_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') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view_with_template(self): def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied') response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 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') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
from django.conf.urls import patterns, url
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, status, serializers from rest_framework import generics, status, serializers
from django.conf.urls import patterns, url
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import ( from rest_framework.tests.models import (
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
@ -331,3 +332,48 @@ class TestOverriddenURLField(TestCase):
serializer.data, serializer.data,
{'title': 'New blog post', 'url': 'foo bar'} {'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)

View File

@ -0,0 +1,30 @@
from django.core.urlresolvers import reverse
from django.conf.urls 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)

View File

@ -9,14 +9,18 @@ from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel from rest_framework.tests.models import BasicModel
from .models import FilterableItem
factory = APIRequestFactory() 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): path, args = url.split('?')
text = models.CharField(max_length=100) args = dict(r.split('=') for r in args.split('&'))
decimal = models.DecimalField(max_digits=4, decimal_places=2) return path, args
date = models.DateField()
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
@ -84,7 +88,7 @@ class IntegrationTestPagination(TestCase):
self.assertNotEqual(response.data['next'], None) self.assertNotEqual(response.data['next'], None)
self.assertEqual(response.data['previous'], 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): with self.assertNumQueries(2):
response = self.view(request).render() response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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['next'], None)
self.assertNotEqual(response.data['previous'], 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): with self.assertNumQueries(2):
response = self.view(request).render() response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -146,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
EXPECTED_NUM_QUERIES = 2 EXPECTED_NUM_QUERIES = 2
request = factory.get('/?decimal=15.20') request = factory.get('/', {'decimal': '15.20'})
with self.assertNumQueries(EXPECTED_NUM_QUERIES): with self.assertNumQueries(EXPECTED_NUM_QUERIES):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -155,7 +159,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertNotEqual(response.data['next'], None) self.assertNotEqual(response.data['next'], None)
self.assertEqual(response.data['previous'], 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): with self.assertNumQueries(EXPECTED_NUM_QUERIES):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -164,7 +168,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEqual(response.data['next'], None) self.assertEqual(response.data['next'], None)
self.assertNotEqual(response.data['previous'], 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): with self.assertNumQueries(EXPECTED_NUM_QUERIES):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -191,7 +195,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
view = BasicFilterFieldsRootView.as_view() view = BasicFilterFieldsRootView.as_view()
request = factory.get('/?decimal=15.20') request = factory.get('/', {'decimal': '15.20'})
with self.assertNumQueries(2): with self.assertNumQueries(2):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -200,7 +204,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertNotEqual(response.data['next'], None) self.assertNotEqual(response.data['next'], None)
self.assertEqual(response.data['previous'], 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): with self.assertNumQueries(2):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -209,7 +213,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEqual(response.data['next'], None) self.assertEqual(response.data['next'], None)
self.assertNotEqual(response.data['previous'], 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): with self.assertNumQueries(2):
response = view(request).render() response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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. 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() response = self.view(request).render()
self.assertEqual(response.data['count'], 13) self.assertEqual(response.data['count'], 13)
self.assertEqual(response.data['results'], self.data[:5]) 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. 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() response = self.view(request).render()
self.assertEqual(response.data['count'], 13) self.assertEqual(response.data['count'], 13)
self.assertEqual(response.data['results'], self.data[:5]) self.assertEqual(response.data['results'], self.data[:5])

View File

@ -2,8 +2,10 @@
General tests for relational fields. General tests for relational fields.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django import get_version
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import BlogPost from rest_framework.tests.models import BlogPost
@ -98,3 +100,45 @@ class RelatedFieldSourceTests(TestCase):
obj = ClassWithQuerysetMethod() obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name') value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object']) 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'})
@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
class RelatedFieldChoicesTests(TestCase):
"""
Tests for #1408 "Web browseable API doesn't have blank option on drop down list box"
https://github.com/tomchristie/django-rest-framework/issues/1408
"""
def test_blank_option_is_added_to_choice_if_required_equals_false(self):
"""
"""
post = BlogPost(title="Checking blank option is added")
post.save()
queryset = BlogPost.objects.all()
field = serializers.RelatedField(required=False, queryset=queryset)
choice_count = BlogPost.objects.count()
widget_count = len(field.widget.choices)
self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')

View File

@ -3,9 +3,7 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from .models import OneToOneTarget
class OneToOneTarget(models.Model):
name = models.CharField(max_length=100)
class OneToOneSource(models.Model): class OneToOneSource(models.Model):

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from decimal import Decimal from decimal import Decimal
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
from django.core.cache import cache from django.core.cache import cache
from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -35,6 +36,10 @@ expected_results = [
] ]
class DummyTestModel(models.Model):
name = models.CharField(max_length=42, default='')
class BasicRendererTests(TestCase): class BasicRendererTests(TestCase):
def test_expected_results(self): def test_expected_results(self):
for value, renderer_cls, expected in expected_results: for value, renderer_cls, expected in expected_results:
@ -252,6 +257,18 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.get('Content-Type', None), None) self.assertEqual(resp.get('Content-Type', None), None)
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) 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"]}' _flat_repr = '{"foo": ["bar", "baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@ -277,6 +294,20 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test')) ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"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): def test_render_dict_abc_obj(self):
class Dict(MutableMapping): class Dict(MutableMapping):
def __init__(self): def __init__(self):
@ -583,6 +614,10 @@ class CacheRenderTest(TestCase):
method = getattr(self.client, http_method) method = getattr(self.client, http_method)
resp = method(url) resp = method(url)
del resp.client, resp.request del resp.client, resp.request
try:
del resp.wsgi_request
except AttributeError:
pass
return resp return resp
def test_obj_pickling(self): def test_obj_pickling(self):

View File

@ -68,6 +68,9 @@ class TestMethodOverloading(TestCase):
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE')) request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, '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): class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self): def test_standard_behaviour_determines_no_content_GET(self):

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
from django.test import TestCase from django.test import TestCase
from django.utils import unittest
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers, fields, relations 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 from rest_framework.tests.models import BasicModelSerializer
import datetime import datetime
import pickle 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): class SubComment(object):
@ -71,6 +97,15 @@ class ActionItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ActionItem 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): class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
@ -132,7 +167,7 @@ class AlbumsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Album model = Album
fields = ['title'] # lists are also valid options fields = ['title', 'ref'] # lists are also valid options
class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
@ -288,7 +323,13 @@ class BasicTests(TestCase):
serializer.save() serializer.save()
self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') 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): class DictStyleSerializer(serializers.Serializer):
@ -467,6 +508,32 @@ class ValidationTests(TestCase):
) )
self.assertEqual(serializer.is_valid(), True) 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): def test_writable_star_source_with_inner_source_fields(self):
""" """
Tests that a serializer with source="*" correctly expands the Tests that a serializer with source="*" correctly expands the
@ -576,12 +643,15 @@ class ModelValidationTests(TestCase):
""" """
Just check if serializers.ModelSerializer handles unique checks via .full_clean() 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.is_valid()
serializer.save() serializer.save()
second_serializer = AlbumsSerializer(data={'title': 'a'}) second_serializer = AlbumsSerializer(data={'title': 'a'})
self.assertFalse(second_serializer.is_valid()) 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): def test_foreign_key_is_null_with_partial(self):
""" """
@ -865,6 +935,58 @@ class DefaultValueTests(TestCase):
self.assertEqual(instance.text, 'overridden') 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): class CallableDefaultValueTests(TestCase):
def setUp(self): def setUp(self):
class CallableDefaultValueSerializer(serializers.ModelSerializer): class CallableDefaultValueSerializer(serializers.ModelSerializer):
@ -1492,19 +1614,10 @@ class ManyFieldHelpTextTest(TestCase):
self.assertEqual('Some help text.', rel_field.help_text) self.assertEqual('Some help text.', rel_field.help_text)
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
class AttributeMappingOnAutogeneratedFieldsTests(TestCase): class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def setUp(self): 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)
nullable_char_field = models.CharField(max_length=1024, blank=True, null=True)
class AMOAFSerializer(serializers.ModelSerializer): class AMOAFSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -1581,17 +1694,10 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
self.field_test('nullable_char_field') self.field_test('nullable_char_field')
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
class DefaultValuesOnAutogeneratedFieldsTests(TestCase): class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
def setUp(self): 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 DVOAFSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View 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)

View File

@ -345,4 +345,3 @@ class NestedModelSerializerUpdateTests(TestCase):
result = deserialize.object result = deserialize.object
result.save() result.save()
self.assertEqual(result.id, john.id) self.assertEqual(result.id, john.id)

View 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')

View 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")

View File

@ -2,6 +2,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from io import BytesIO
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
@ -143,3 +145,20 @@ class TestAPIRequestFactory(TestCase):
force_authenticate(request, user=user) force_authenticate(request, user=user)
response = view(request) response = view(request)
self.assertEqual(response.data['user'], 'example') 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']})

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.validators import MaxValueValidator
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import generics, serializers, status from rest_framework import generics, serializers, status
@ -102,3 +103,46 @@ class TestAvoidValidation(TestCase):
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
self.assertDictEqual(serializer.errors, self.assertDictEqual(serializer.errors,
{'non_field_errors': ['Invalid data']}) {'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)

View 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'})

View File

View 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)

View 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

View 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)

View 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

View File

@ -155,6 +155,8 @@ class SimpleRateThrottle(BaseThrottle):
remaining_duration = self.duration remaining_duration = self.duration
available_requests = self.num_requests - len(self.history) + 1 available_requests = self.num_requests - len(self.history) + 1
if available_requests <= 0:
return None
return remaining_duration / float(available_requests) return remaining_duration / float(available_requests)

View File

@ -3,6 +3,7 @@ Helper classes for parsers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils import timezone from django.utils import timezone
from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import force_text from rest_framework.compat import force_text
@ -43,6 +44,8 @@ class JSONEncoder(json.JSONEncoder):
return str(o.total_seconds()) return str(o.total_seconds())
elif isinstance(o, decimal.Decimal): elif isinstance(o, decimal.Decimal):
return str(o) return str(o)
elif isinstance(o, QuerySet):
return list(o)
elif hasattr(o, 'tolist'): elif hasattr(o, 'tolist'):
return o.tolist() return o.tolist()
elif hasattr(o, '__getitem__'): elif hasattr(o, '__getitem__'):

View File

@ -74,7 +74,7 @@ class _MediaType(object):
return 0 return 0
elif self.sub_type == '*': elif self.sub_type == '*':
return 1 return 1
elif not self.params or self.params.keys() == ['q']: elif not self.params or list(self.params.keys()) == ['q']:
return 2 return 2
return 3 return 3

View File

@ -112,12 +112,13 @@ class APIView(View):
@property @property
def default_response_headers(self): def default_response_headers(self):
# TODO: deprecate? headers = {
# TODO: Only vary by accept if multiple renderers
return {
'Allow': ', '.join(self.allowed_methods), '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): 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 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.NotAuthenticated()
raise exceptions.PermissionDenied() raise exceptions.PermissionDenied()
@ -294,7 +295,7 @@ class APIView(View):
# Dispatch methods # Dispatch methods
def initialize_request(self, request, *args, **kargs): def initialize_request(self, request, *args, **kwargs):
""" """
Returns the initial request object. Returns the initial request object.
""" """

View File

@ -55,7 +55,7 @@ if sys.argv[-1] == 'publish':
setup( setup(
name='djangorestframework', name='djangorestframework',
version=version, version=version,
url='http://django-rest-framework.org', url='http://www.django-rest-framework.org',
license='BSD', license='BSD',
description='Web APIs for Django, made easy.', description='Web APIs for Django, made easy.',
author='Tom Christie', author='Tom Christie',

65
tox.ini
View File

@ -1,90 +1,125 @@
[tox] [tox]
downloadcache = {toxworkdir}/cache/ 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 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
[testenv] [testenv]
commands = {envpython} rest_framework/runtests/runtests.py 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] [testenv:py3.3-django1.6]
basepython = python3.3 basepython = python3.3
deps = Django==1.6.1 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.6] [testenv:py3.2-django1.6]
basepython = python3.2 basepython = python3.2
deps = Django==1.6.1 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.6] [testenv:py2.7-django1.6]
basepython = python2.7 basepython = python2.7
deps = Django==1.6.1 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.6] [testenv:py2.6-django1.6]
basepython = python2.6 basepython = python2.6
deps = Django==1.6.1 deps = Django==1.6
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.4 django-oauth2-provider==0.2.4
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py3.3-django1.5] [testenv:py3.3-django1.5]
basepython = python3.3 basepython = python3.3
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.5] [testenv:py3.2-django1.5]
basepython = python3.2 basepython = python3.2
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.5] [testenv:py2.7-django1.5]
basepython = python2.7 basepython = python2.7
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.5] [testenv:py2.6-django1.5]
basepython = python2.6 basepython = python2.6
deps = django==1.5.5 deps = django==1.5.5
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.7-django1.4] [testenv:py2.7-django1.4]
basepython = python2.7 basepython = python2.7
deps = django==1.4.10 deps = django==1.4.10
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0
[testenv:py2.6-django1.4] [testenv:py2.6-django1.4]
basepython = python2.6 basepython = python2.6
deps = django==1.4.10 deps = django==1.4.10
django-filter==0.6a1 django-filter==0.7
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.2.1 django-oauth-plus==2.2.1
oauth2==1.5.211 oauth2==1.5.211
django-oauth2-provider==0.2.3 django-oauth2-provider==0.2.3
django-guardian==1.1.1 django-guardian==1.1.1
Pillow==2.3.0