aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--README.md38
-rw-r--r--docs/404.html104
-rwxr-xr-xdocs/api-guide/authentication.md6
-rw-r--r--docs/api-guide/fields.md12
-rw-r--r--docs/api-guide/filtering.md26
-rwxr-xr-xdocs/api-guide/generic-views.md11
-rw-r--r--docs/api-guide/relations.md2
-rw-r--r--docs/api-guide/serializers.md48
-rw-r--r--docs/api-guide/settings.md6
-rw-r--r--docs/img/logo.pngbin0 -> 41532 bytes
-rw-r--r--docs/index.md69
-rw-r--r--docs/template.html27
-rw-r--r--docs/topics/2.2-announcement.md2
-rw-r--r--docs/topics/release-notes.md15
-rw-r--r--docs/tutorial/1-serialization.md5
-rwxr-xr-xmkdocs.py8
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/compat.py2
-rw-r--r--rest_framework/exceptions.py33
-rw-r--r--rest_framework/fields.py12
-rw-r--r--rest_framework/filters.py29
-rw-r--r--rest_framework/generics.py2
-rw-r--r--rest_framework/mixins.py3
-rw-r--r--rest_framework/relations.py15
-rw-r--r--rest_framework/renderers.py3
-rw-r--r--rest_framework/request.py9
-rw-r--r--rest_framework/runtests/settings.py3
-rw-r--r--rest_framework/serializers.py72
-rw-r--r--rest_framework/settings.py1
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/tests/accounts/__init__.py0
-rw-r--r--rest_framework/tests/accounts/models.py8
-rw-r--r--rest_framework/tests/accounts/serializers.py11
-rw-r--r--rest_framework/tests/records/__init__.py0
-rw-r--r--rest_framework/tests/records/models.py6
-rw-r--r--rest_framework/tests/test_filters.py104
-rw-r--r--rest_framework/tests/test_hyperlinkedserializers.py46
-rw-r--r--rest_framework/tests/test_relations.py20
-rw-r--r--rest_framework/tests/test_renderers.py19
-rw-r--r--rest_framework/tests/test_request.py3
-rw-r--r--rest_framework/tests/test_serializer_import.py19
-rw-r--r--rest_framework/tests/test_serializer_nested.py1
-rw-r--r--rest_framework/tests/test_serializers.py28
-rw-r--r--rest_framework/tests/test_testing.py9
-rw-r--r--rest_framework/tests/test_write_only_fields.py42
-rw-r--r--rest_framework/tests/users/__init__.py0
-rw-r--r--rest_framework/tests/users/models.py6
-rw-r--r--rest_framework/tests/users/serializers.py8
-rw-r--r--rest_framework/utils/encoders.py3
-rw-r--r--rest_framework/views.py9
-rwxr-xr-xsetup.py2
52 files changed, 709 insertions, 204 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e0544a47..a7aa6fc4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -67,7 +67,7 @@ To run the tests, clone the repository, and then:
# Run the tests
rest_framework/runtests/runtests.py
-You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
+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
diff --git a/README.md b/README.md
index 8c682231..73e4b13f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Django REST framework
-**Awesome web-browseable Web APIs.**
-
[![build-status-image]][travis]
-**Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs].
+**Awesome web-browseable Web APIs.**
+
+**Note**: Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
# Overview
@@ -99,7 +99,7 @@ That's it, we're done!
# Documentation & Support
-Full documentation for the project is available at [http://django-rest-framework.org][docs].
+Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC.
@@ -143,21 +143,21 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/
-[index]: http://django-rest-framework.org/
-[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
-[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
-[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
-[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
-[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
-[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
-[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
-[routers]: http://django-rest-framework.org/api-guide/routers.html
-[serializers]: http://django-rest-framework.org/api-guide/serializers.html
-[authentication]: http://django-rest-framework.org/api-guide/authentication.html
-
-[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
+[index]: http://www.django-rest-framework.org/
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication
+[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers
+[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer
+[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views
+[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html
+[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html
+[routers]: http://www.django-rest-framework.org/api-guide/routers.html
+[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html
+[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html
+
+[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
-[image]: http://django-rest-framework.org/img/quickstart.png
+[image]: http://www.django-rest-framework.org/img/quickstart.png
[tox]: http://testrun.org/tox/latest/
@@ -165,7 +165,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[wlonk]: https://twitter.com/wlonk/status/261689665952833536
[laserllama]: https://twitter.com/laserllama/status/328688333750407168
-[docs]: http://django-rest-framework.org/
+[docs]: http://www.django-rest-framework.org/
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
[pyyaml]: http://pypi.python.org/pypi/PyYAML
diff --git a/docs/404.html b/docs/404.html
index 4938da6e..864247e7 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -3,17 +3,17 @@
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Django REST framework - 404 - Page not found</title>
- <link href="http://django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
- <link rel="canonical" href="http://django-rest-framework.org/404"/>
+ <link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="canonical" href="http://www.django-rest-framework.org/404"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Django, API, REST, 404 - Page not found">
<meta name="author" content="Tom Christie">
<!-- Le styles -->
- <link href="http://django-rest-framework.org/css/prettify.css" rel="stylesheet">
- <link href="http://django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
- <link href="http://django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
- <link href="http://django-rest-framework.org/css/default.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
@@ -50,63 +50,63 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
- <a class="brand" href="http://django-rest-framework.org">Django REST framework</a>
+ <a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
<div class="nav-collapse collapse">
<ul class="nav">
- <li><a href="http://django-rest-framework.org">Home</a></li>
+ <li><a href="http://www.django-rest-framework.org">Home</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
<ul class="dropdown-menu">
- <li><a href="http://django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
- <li><a href="http://django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
<ul class="dropdown-menu">
- <li><a href="http://django-rest-framework.org/api-guide/requests">Requests</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/responses">Responses</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/views">Views</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/routers">Routers</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/parsers">Parsers</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/renderers">Renderers</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/serializers">Serializers</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/authentication">Authentication</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/permissions">Permissions</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/throttling">Throttling</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/filtering">Filtering</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/pagination">Pagination</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/testing">Testing</a></li>
- <li><a href="http://django-rest-framework.org/api-guide/settings">Settings</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/requests">Requests</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/responses">Responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/views">Views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/routers">Routers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/parsers">Parsers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/renderers">Renderers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/serializers">Serializers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/authentication">Authentication</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/permissions">Permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/throttling">Throttling</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/filtering">Filtering</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/pagination">Pagination</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/testing">Testing</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/settings">Settings</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
<ul class="dropdown-menu">
- <li><a href="http://django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
- <li><a href="http://django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
- <li><a href="http://django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
- <li><a href="http://django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
- <li><a href="http://django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
- <li><a href="http://django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
- <li><a href="http://django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
- <li><a href="http://django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
- <li><a href="http://django-rest-framework.org/topics/release-notes">Release Notes</a></li>
- <li><a href="http://django-rest-framework.org/topics/credits">Credits</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/release-notes">Release Notes</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/credits">Credits</a></li>
</ul>
</li>
</ul>
@@ -160,7 +160,7 @@
<div id="main-content" class="span12">
<h1 id="404-page-not-found" style="text-align: center">404</h1>
<p style="text-align: center"><strong>Page not found</strong></p>
- <p style="text-align: center">Try the <a href="http://django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
+ <p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
</div><!--/span-->
</div><!--/row-->
</div><!--/.fluid-container-->
@@ -176,9 +176,9 @@
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
- <script src="http://django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
- <script src="http://django-rest-framework.org/js/prettify-1.0.js"></script>
- <script src="http://django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
+ <script src="http://www.django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
+ <script src="http://www.django-rest-framework.org/js/prettify-1.0.js"></script>
+ <script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
<script>
//$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) };
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 53efc49a..dc8e2099 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -389,6 +389,10 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp
The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
+## JSON Web Token Authentication
+
+JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
+
[cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
@@ -413,3 +417,5 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
[doac]: https://github.com/Rediker-Software/doac
[rediker]: https://github.com/Rediker-Software
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
+[blimp]: https://github.com/GetBlimp
+[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index e05c0306..c136509b 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -28,7 +28,13 @@ Defaults to the name of the field.
### `read_only`
-Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
+Set this to `True` to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.
+
+Defaults to `False`
+
+### `write_only`
+
+Set this to `True` to ensure that the field may be used when updating or creating an instance, but is not included when serializing the representation.
Defaults to `False`
@@ -167,13 +173,13 @@ or `django.db.models.fields.TextField`.
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
-**Signature:** `CharField(max_length=200, min_length=None)`
+**Signature:** `URLField(max_length=200, min_length=None)`
## SlugField
Corresponds to `django.db.models.fields.SlugField`.
-**Signature:** `CharField(max_length=50, min_length=None)`
+**Signature:** `SlugField(max_length=50, min_length=None)`
## ChoiceField
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 0e02a2a7..07420d84 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -282,13 +282,37 @@ Multiple orderings may also be specified:
http://example.com/api/users?ordering=account,username
+### Specifying which fields may be ordered against
+
+It's recommended that you explicitly specify which fields the API should allowing in the ordering filter. You can do this by setting an `ordering_fields` attribute on the view, like so:
+
+ class UserListView(generics.ListAPIView):
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = ('username', 'email')
+
+This helps prevent unexpected data leakage, such as allowing users to order against a password hash field or other sensitive data.
+
+If you *don't* specify an `ordering_fields` attribute on the view, the filter class will default to allowing the user to filter on any readable fields on the serializer specified by the `serializer_class` attribute.
+
+If you are confident that the queryset being used by the view doesn't contain any sensitive data, you can also explicitly specify that a view should allow ordering on *any* model field or queryset aggregate, by using the special value `'__all__'`.
+
+ class BookingsListView(generics.ListAPIView):
+ queryset = Booking.objects.all()
+ serializer_class = BookingSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = '__all__'
+
+### Specifying a default ordering
+
If an `ordering` attribute is set on the view, this will be used as the default ordering.
Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
- serializer = UserSerializer
+ serializer_class = UserSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('username',)
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 83c3e45f..e23b2c74 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -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.
-[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
+# Third party packages
+
+The following third party packages provide additional generic view implementations.
+
+## Django REST Framework bulk
+The [django-rest-framework-bulk package][django-rest-framework-bulk] implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
+
+
+[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
[GenericAPIView]: #genericapiview
[ListModelMixin]: #listmodelmixin
[CreateModelMixin]: #createmodelmixin
[RetrieveModelMixin]: #retrievemodelmixin
[UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin
+[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 4bee75af..cc4f5585 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -454,7 +454,7 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati
[cite]: http://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
-[routers]: http://django-rest-framework.org/api-guide/routers#defaultrouter
+[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 6fc25f57..e8369c20 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -103,11 +103,11 @@ Deserialization is similar. First we parse a stream into Python native datatype
When deserializing data, we can either create a new instance, or update an existing instance.
serializer = CommentSerializer(data=data) # Create new instance
- serializer = CommentSerializer(comment, data=data) # Update `instance`
+ serializer = CommentSerializer(comment, data=data) # Update `comment`
By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
- serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
+ serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `comment` with partial data
## Validation
@@ -208,7 +208,7 @@ Similarly if a nested representation should be a list of items, you should pass
Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object.
- serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
+ serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
@@ -373,6 +373,25 @@ You may wish to specify multiple fields as read-only. Instead of adding each fi
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
+## Specifying which fields should be write-only
+
+You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so:
+
+ class CreateUserSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = User
+ fields = ('email', 'username', 'password')
+ write_only_fields = ('password',) # Note: Password field is write-only
+
+ def restore_object(self, attrs, instance=None):
+ """
+ Instantiate a new User instance.
+ """
+ assert instance is None, 'Cannot update users with CreateUserSerializer'
+ user = User(email=attrs['email'], username=attrs['username'])
+ user.set_password(attrs['password'])
+ return user
+
## Specifying fields explicitly
You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
@@ -445,6 +464,29 @@ For more specific requirements such as specifying a different lookup for each fi
model = Account
fields = ('url', 'account_name', 'users', 'created')
+## Overiding the URL field behavior
+
+The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
+
+You can also override this on a per-serializer basis by using the `url_field_name` option on the serializer, like so:
+
+ class AccountSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Account
+ fields = ('account_url', 'account_name', 'users', 'created')
+ url_field_name = 'account_url'
+
+**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
+
+You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
+
+ class AccountSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Account
+ fields = ('account_url', 'account_name', 'users', 'created')
+ view_name = 'account_detail'
+ lookup_field='account_name'
+
---
# Advanced serializer usage
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 13f96f9a..5aee52aa 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -353,6 +353,12 @@ This should be a function with the following signature:
Default: `'rest_framework.views.exception_handler'`
+#### URL_FIELD_NAME
+
+A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`.
+
+Default: `'url'`
+
#### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix.
diff --git a/docs/img/logo.png b/docs/img/logo.png
new file mode 100644
index 00000000..73de34f4
--- /dev/null
+++ b/docs/img/logo.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index 7688d428..2a4ad885 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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>
-<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
+<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
</p>
-# Django REST framework
+---
+
+<p>
+<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>
-**Awesome web-browsable Web APIs.**
+<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
+</p>
+
+<!--
+# Django REST framework
+-->
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
@@ -20,13 +35,16 @@ Some reasons you might want to use REST framework:
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
+* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite].
-There is a live example API for testing purposes, [available here][sandbox].
-
-**Below**: *Screenshot from the browsable API*
+---
![Screenshot][image]
+**Above**: *Screenshot from the browsable API*
+
+----
+
## Requirements
REST framework requires the following:
@@ -62,7 +80,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
- 'rest_framework',
+ 'rest_framework',
)
If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
@@ -111,7 +129,7 @@ Here's our project's root `urls.py` module:
class GroupViewSet(viewsets.ModelViewSet):
model = Group
-
+
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
@@ -140,6 +158,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [5 - Relationships & hyperlinked APIs][tut-5]
* [6 - Viewsets & routers][tut-6]
+There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox].
+
## API Guide
The API guide is your complete reference manual to all the functionality provided by REST framework.
@@ -196,7 +216,7 @@ Run the tests:
./rest_framework/runtests/runtests.py
-To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
+To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
tox
@@ -222,29 +242,30 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
Copyright (c) 2011-2014, Tom Christie
All rights reserved.
-Redistribution and use in source and binary forms, with or without
+Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this
+Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this
-list of conditions and the following disclaimer in the documentation and/or
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
-[urlobject]: https://github.com/zacharyvoase/urlobject
+[mozilla]: http://www.mozilla.org/en-US/about/
+[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
diff --git a/docs/template.html b/docs/template.html
index c065237a..a397d067 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -170,31 +170,12 @@
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
{{ toc }}
<div>
- <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 sign up here:</p>
-
-<!-- Begin MailChimp Signup Form -->
-<link href="//cdn-images.mailchimp.com/embedcode/slim-081711.css" rel="stylesheet" type="text/css">
-<style type="text/css">
- #mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
- /* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
- We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
-</style>
-<div id="mc_embed_signup" style="background: rgb(245, 245, 245)">
-<form action="http://dabapps.us1.list-manage1.com/subscribe/post?u=cf73a9994eb5b8d8d461b5dfb&amp;id=cb6af8e8bd" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
-<!-- <label for="mce-EMAIL">Keep me posted!</label>
- --> <input style="width: 90%" type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required>
- <div class="clear"><input class="btn btn-success" type="submit" value="Yes, keep me posted!" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
-</form>
-</div>
-</style></div>
- </ul>
+{{ ad_block }}
+
+</div>
+</ul>
-<!--End mc_embed_signup-->
</div>
</div>
diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md
index 0f980e1c..a997c782 100644
--- a/docs/topics/2.2-announcement.md
+++ b/docs/topics/2.2-announcement.md
@@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
-[credits]: http://django-rest-framework.org/topics/credits
+[credits]: http://www.django-rest-framework.org/topics/credits
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[marcgibbons]: https://github.com/marcgibbons/
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index b09bd0be..14503148 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,8 +40,19 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
-### Master
+### 2.3.12
+**Date**: 15th January 2014
+
+* **Security fix**: `OrderingField` now only allows ordering on readable serializer fields, or on fields explicitly specified using `ordering_fields`. This prevents users being able to order by fields that are not visible in the API, and exploiting the ordering of sensitive data such as password hashes.
+* Bugfix: `write_only = True` fields now display in the browsable API.
+
+### 2.3.11
+
+**Date**: 14th January 2014
+
+* Added `write_only` serializer field argument.
+* Added `write_only_fields` option to `ModelSerializer` classes.
* JSON renderer now deals with objects that implement a dict-like interface.
* Fix compatiblity with newer versions of `django-oauth-plus`.
* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations.
@@ -98,7 +109,7 @@ You can determine your currently installed version using `pip freeze`:
class DisablePaginationMixin(object):
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 super(DisablePaginationMixin, self).get_paginate_by(queryset)
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 2298df59..979c4a3e 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -17,9 +17,8 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
:::bash
- mkdir ~/env
- virtualenv ~/env/tutorial
- source ~/env/tutorial/bin/activate
+ virtualenv env
+ source env/bin/activate
Now that we're inside a virtualenv environment, we can install our package requirements.
diff --git a/mkdocs.py b/mkdocs.py
index 09c9dcc6..f973096f 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -18,7 +18,7 @@ if local:
suffix = '.html'
index = 'index.html'
else:
- base_url = 'http://django-rest-framework.org'
+ base_url = 'http://www.django-rest-framework.org'
suffix = ''
index = ''
@@ -161,6 +161,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = output.replace('{{ page_id }}', filename[:-3])
output = output.replace('{{ canonical_url }}', canonical_url)
+ if filename =='index.md':
+ output = output.replace('{{ ad_block }}', """<hr><p><strong>The team behind REST framework is launching a new API service.</strong></p>
+<p>If you want to be first in line when we start issuing invitations, please <a href="http://brightapi.com">sign up here</a>.</p>""")
+ else:
+ output = output.replace('{{ ad_block }}', '')
+
if prev_url:
output = output.replace('{{ prev_url }}', prev_url)
output = output.replace('{{ prev_url_disabled }}', '')
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index f5483b9d..6759680b 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
"""
__title__ = 'Django REST framework'
-__version__ = '2.3.10'
+__version__ = '2.3.12'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2013 Tom Christie'
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index b69749fe..d283e2f5 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -457,7 +457,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
try:
# In 1.5 the test client uses force_bytes
- from django.utils.encoding import force_bytes_or_smart_bytes
+ from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
except ImportError:
# In 1.3 and 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 425a7214..4276625a 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -6,6 +6,7 @@ In addition Django's built in 403 and 404 exceptions are handled.
"""
from __future__ import unicode_literals
from rest_framework import status
+import math
class APIException(Exception):
@@ -13,40 +14,32 @@ class APIException(Exception):
Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.detail` properties.
"""
- pass
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
+ default_detail = ''
+
+ def __init__(self, detail=None):
+ self.detail = detail or self.default_detail
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.'
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
-
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.'
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
-
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Authentication credentials were not provided.'
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
-
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = 'You do not have permission to perform this action.'
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
-
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
@@ -75,14 +68,14 @@ class UnsupportedMediaType(APIException):
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = "Request was throttled."
+ default_detail = 'Request was throttled.'
extra_detail = "Expected available in %d second%s."
def __init__(self, wait=None, detail=None):
- import math
- self.wait = wait and math.ceil(wait) or None
- if wait is not None:
- format = detail or self.default_detail + self.extra_detail
- self.detail = format % (self.wait, self.wait != 1 and 's' or '')
- else:
+ if wait is None:
self.detail = detail or self.default_detail
+ self.wait = None
+ else:
+ format = (detail or self.default_detail) + self.extra_detail
+ self.detail = format % (wait, wait != 1 and 's' or '')
+ self.wait = math.ceil(wait)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f1de447c..2f475d6e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -246,6 +246,7 @@ class WritableField(Field):
"""
Base for read/write fields.
"""
+ write_only = False
default_validators = []
default_error_messages = {
'required': _('This field is required.'),
@@ -255,7 +256,7 @@ class WritableField(Field):
default = None
def __init__(self, source=None, label=None, help_text=None,
- read_only=False, required=None,
+ read_only=False, write_only=False, required=None,
validators=[], error_messages=None, widget=None,
default=None, blank=None):
@@ -269,6 +270,10 @@ class WritableField(Field):
super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
self.read_only = read_only
+ self.write_only = write_only
+
+ assert not (read_only and write_only), "Cannot set read_only=True and write_only=True"
+
if required is None:
self.required = not(read_only)
else:
@@ -318,6 +323,11 @@ class WritableField(Field):
if errors:
raise ValidationError(errors)
+ def field_to_native(self, obj, field_name):
+ if self.write_only:
+ return None
+ return super(WritableField, self).field_to_native(obj, field_name)
+
def field_from_native(self, data, files, field_name, into):
"""
Given a dictionary and a field name, updates the dictionary `into`,
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 5c6a187c..de91caed 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results
returned by list views.
"""
from __future__ import unicode_literals
+from django.core.exceptions import ImproperlyConfigured
from django.db import models
from rest_framework.compat import django_filters, six, guardian, get_model_name
from functools import reduce
@@ -107,6 +108,7 @@ class SearchFilter(BaseFilterBackend):
class OrderingFilter(BaseFilterBackend):
ordering_param = 'ordering' # The URL query parameter used for the ordering.
+ ordering_fields = None
def get_ordering(self, request):
"""
@@ -122,17 +124,34 @@ class OrderingFilter(BaseFilterBackend):
return (ordering,)
return ordering
- def remove_invalid_fields(self, queryset, ordering):
- field_names = [field.name for field in queryset.model._meta.fields]
- field_names += queryset.query.aggregates.keys()
- return [term for term in ordering if term.lstrip('-') in field_names]
+ def remove_invalid_fields(self, queryset, ordering, view):
+ valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
+
+ if valid_fields is None:
+ # Default to allowing filtering on serializer fields
+ serializer_class = getattr(view, 'serializer_class')
+ if serializer_class is None:
+ msg = ("Cannot use %s on a view which does not have either a "
+ "'serializer_class' or 'ordering_fields' attribute.")
+ raise ImproperlyConfigured(msg % self.__class__.__name__)
+ valid_fields = [
+ field.source or field_name
+ for field_name, field in serializer_class().fields.items()
+ if not getattr(field, 'write_only', False)
+ ]
+ elif valid_fields == '__all__':
+ # View explictly allows filtering on any model field
+ valid_fields = [field.name for field in queryset.model._meta.fields]
+ valid_fields += queryset.query.aggregates.keys()
+
+ return [term for term in ordering if term.lstrip('-') in valid_fields]
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request)
if ordering:
# Skip any incorrect parameters
- ordering = self.remove_invalid_fields(queryset, ordering)
+ ordering = self.remove_invalid_fields(queryset, ordering, view)
if not ordering:
# Use 'ordering' attribute by default
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index fd411ad3..7bac510f 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -352,7 +352,7 @@ class GenericAPIView(views.APIView):
def post_delete(self, obj):
"""
- Placeholder method for calling after saving an object.
+ Placeholder method for calling after deleting an object.
"""
pass
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 43950c4b..5fbcf700 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -11,6 +11,7 @@ from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import clone_request
+from rest_framework.settings import api_settings
import warnings
@@ -60,7 +61,7 @@ class CreateModelMixin(object):
def get_success_headers(self, data):
try:
- return {'Location': data['url']}
+ return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 35c00bf1..02185c2f 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -65,16 +65,11 @@ class RelatedField(WritableField):
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
- try:
- manager = getattr(self.parent.opts.model, self.source or field_name)
- if hasattr(manager, 'related'): # Forward
- self.queryset = manager.related.model._default_manager.all()
- else: # Reverse
- self.queryset = manager.field.rel.to._default_manager.all()
- except Exception:
- msg = ('Serializer related fields must include a `queryset`' +
- ' argument or set `read_only=True')
- raise Exception(msg)
+ manager = getattr(self.parent.opts.model, self.source or field_name)
+ if hasattr(manager, 'related'): # Forward
+ self.queryset = manager.related.model._default_manager.all()
+ else: # Reverse
+ self.queryset = manager.field.rel.to._default_manager.all()
### We need this stuff to make form choices work...
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 2fdd3337..e8afc26d 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -10,6 +10,7 @@ from __future__ import unicode_literals
import copy
import json
+import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header
@@ -597,7 +598,7 @@ class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
charset = 'utf-8'
- BOUNDARY = 'BoUnDaRyStRiNg'
+ BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data)
diff --git a/rest_framework/request.py b/rest_framework/request.py
index fcea2508..ca70b49e 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -223,7 +223,7 @@ class Request(object):
def user(self, value):
"""
Sets the user on the current request. This is necessary to maintain
- compatilbility with django.contrib.auth where the user proprety is
+ compatibility with django.contrib.auth where the user property is
set in the login and logout functions.
"""
self._user = value
@@ -279,10 +279,9 @@ class Request(object):
if not _hasattr(self, '_method'):
self._method = self._request.method
- if self._method == 'POST':
- # Allow X-HTTP-METHOD-OVERRIDE header
- self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
- self._method)
+ # Allow X-HTTP-METHOD-OVERRIDE header
+ self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
+ self._method)
def _load_stream(self):
"""
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
index be721658..3fc0eb2f 100644
--- a/rest_framework/runtests/settings.py
+++ b/rest_framework/runtests/settings.py
@@ -100,6 +100,9 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'rest_framework.tests',
+ 'rest_framework.tests.accounts',
+ 'rest_framework.tests.records',
+ 'rest_framework.tests.users',
)
# OAuth is optional and won't work if there is no oauth_provider & oauth2
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index c0c810ab..536b040b 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -13,6 +13,7 @@ response content is handled by parsers and renderers.
from __future__ import unicode_literals
import copy
import datetime
+import inspect
import types
from decimal import Decimal
from django.core.paginator import Page
@@ -20,6 +21,8 @@ from django.db import models
from django.forms import widgets
from django.utils.datastructures import SortedDict
from rest_framework.compat import get_concrete_model, six
+from rest_framework.settings import api_settings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -32,6 +35,27 @@ from rest_framework.relations import *
from rest_framework.fields import *
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situtations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
+
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if type(obj) == str and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ return models.get_model(app_name, model_name)
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ else:
+ raise ValueError("{0} is not a Django model".format(obj))
+
+
def pretty_name(name):
"""Converts 'first_name' to 'First name'"""
if not name:
@@ -326,12 +350,13 @@ class BaseSerializer(WritableField):
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
- ret[key] = value
+ if not getattr(field, 'write_only', False):
+ ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value)
return ret
- def from_native(self, data, files):
+ def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""
@@ -361,6 +386,9 @@ class BaseSerializer(WritableField):
Override default so that the serializer can be used as a nested field
across relationships.
"""
+ if self.write_only:
+ return None
+
if self.source == '*':
return self.to_native(obj)
@@ -593,6 +621,7 @@ class ModelSerializerOptions(SerializerOptions):
super(ModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, 'model', None)
self.read_only_fields = getattr(meta, 'read_only_fields', ())
+ self.write_only_fields = getattr(meta, 'write_only_fields', ())
class ModelSerializer(Serializer):
@@ -656,7 +685,7 @@ class ModelSerializer(Serializer):
if model_field.rel:
to_many = isinstance(model_field,
models.fields.related.ManyToManyField)
- related_model = model_field.rel.to
+ related_model = _resolve_model(model_field.rel.to)
if to_many and not model_field.rel.through._meta.auto_created:
has_through_model = True
@@ -732,17 +761,29 @@ class ModelSerializer(Serializer):
# Add the `read_only` flag to any fields that have bee specified
# in the `read_only_fields` option
for field_name in self.opts.read_only_fields:
- assert field_name not in self.base_fields.keys(), \
- "field '%s' on serializer '%s' specified in " \
- "`read_only_fields`, but also added " \
- "as an explicit field. Remove it from `read_only_fields`." % \
- (field_name, self.__class__.__name__)
- assert field_name in ret, \
- "Non-existant field '%s' specified in `read_only_fields` " \
- "on serializer '%s'." % \
- (field_name, self.__class__.__name__)
+ assert field_name not in self.base_fields.keys(), (
+ "field '%s' on serializer '%s' specified in "
+ "`read_only_fields`, but also added "
+ "as an explicit field. Remove it from `read_only_fields`." %
+ (field_name, self.__class__.__name__))
+ assert field_name in ret, (
+ "Non-existant field '%s' specified in `read_only_fields` "
+ "on serializer '%s'." %
+ (field_name, self.__class__.__name__))
ret[field_name].read_only = True
+ for field_name in self.opts.write_only_fields:
+ assert field_name not in self.base_fields.keys(), (
+ "field '%s' on serializer '%s' specified in "
+ "`write_only_fields`, but also added "
+ "as an explicit field. Remove it from `write_only_fields`." %
+ (field_name, self.__class__.__name__))
+ assert field_name in ret, (
+ "Non-existant field '%s' specified in `write_only_fields` "
+ "on serializer '%s'." %
+ (field_name, self.__class__.__name__))
+ ret[field_name].write_only = True
+
return ret
def get_pk_field(self, model_field):
@@ -990,6 +1031,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.view_name = getattr(meta, 'view_name', None)
self.lookup_field = getattr(meta, 'lookup_field', None)
+ self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME)
class HyperlinkedModelSerializer(ModelSerializer):
@@ -1008,13 +1050,13 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model)
- if 'url' not in fields:
+ if self.opts.url_field_name not in fields:
url_field = self._hyperlink_identify_field_class(
view_name=self.opts.view_name,
lookup_field=self.opts.lookup_field
)
ret = self._dict_class()
- ret['url'] = url_field
+ ret[self.opts.url_field_name] = url_field
ret.update(fields)
fields = ret
@@ -1050,7 +1092,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
We need to override the default, to use the url as the identity.
"""
try:
- return data.get('url', None)
+ return data.get(self.opts.url_field_name, None)
except AttributeError:
return None
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 8abaf140..ce171d6d 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -95,6 +95,7 @@ DEFAULTS = {
'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format',
+ 'URL_FIELD_NAME': 'url',
# Input and output formats
'DATE_INPUT_FORMATS': (
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 495163b6..d19d5a2b 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -33,7 +33,7 @@
<div class="navbar-inner">
<div class="container-fluid">
<span href="/">
- {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
+ {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
diff --git a/rest_framework/tests/accounts/__init__.py b/rest_framework/tests/accounts/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rest_framework/tests/accounts/__init__.py
diff --git a/rest_framework/tests/accounts/models.py b/rest_framework/tests/accounts/models.py
new file mode 100644
index 00000000..525e601b
--- /dev/null
+++ b/rest_framework/tests/accounts/models.py
@@ -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')
diff --git a/rest_framework/tests/accounts/serializers.py b/rest_framework/tests/accounts/serializers.py
new file mode 100644
index 00000000..a27b9ca6
--- /dev/null
+++ b/rest_framework/tests/accounts/serializers.py
@@ -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
diff --git a/rest_framework/tests/records/__init__.py b/rest_framework/tests/records/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rest_framework/tests/records/__init__.py
diff --git a/rest_framework/tests/records/models.py b/rest_framework/tests/records/models.py
new file mode 100644
index 00000000..76954807
--- /dev/null
+++ b/rest_framework/tests/records/models.py
@@ -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)
diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py
index 614f45cc..18188186 100644
--- a/rest_framework/tests/test_filters.py
+++ b/rest_framework/tests/test_filters.py
@@ -368,7 +368,6 @@ class OrderingFilterRelatedModel(models.Model):
related_name="relateds")
-
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -394,6 +393,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
+ ordering_fields = ('text',)
view = OrderingListView.as_view()
request = factory.get('?ordering=text')
@@ -412,6 +412,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
+ ordering_fields = ('text',)
view = OrderingListView.as_view()
request = factory.get('?ordering=-text')
@@ -430,6 +431,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
+ ordering_fields = ('text',)
view = OrderingListView.as_view()
request = factory.get('?ordering=foobar')
@@ -448,6 +450,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
+ oredering_fields = ('text',)
view = OrderingListView.as_view()
request = factory.get('')
@@ -466,6 +469,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
+ ordering_fields = ('text',)
view = OrderingListView.as_view()
request = factory.get('')
@@ -494,6 +498,7 @@ class OrderingFilterTests(TestCase):
model = OrdringFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
+ ordering_fields = '__all__'
queryset = OrdringFilterModel.objects.all().annotate(
models.Count("relateds"))
@@ -510,4 +515,101 @@ class OrderingFilterTests(TestCase):
)
+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
+ ]
+ ) \ No newline at end of file
diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py
index 61e613d7..83d46043 100644
--- a/rest_framework/tests/test_hyperlinkedserializers.py
+++ b/rest_framework/tests/test_hyperlinkedserializers.py
@@ -3,6 +3,7 @@ import json
from django.test import TestCase
from rest_framework import generics, status, serializers
from rest_framework.compat import patterns, url
+from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import (
Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
@@ -331,3 +332,48 @@ class TestOverriddenURLField(TestCase):
serializer.data,
{'title': 'New blog post', 'url': 'foo bar'}
)
+
+
+class TestURLFieldNameBySettings(TestCase):
+ urls = 'rest_framework.tests.test_hyperlinkedserializers'
+
+ def setUp(self):
+ self.saved_url_field_name = api_settings.URL_FIELD_NAME
+ api_settings.URL_FIELD_NAME = 'global_url_field'
+
+ class Serializer(serializers.HyperlinkedModelSerializer):
+
+ class Meta:
+ model = BlogPost
+ fields = ('title', api_settings.URL_FIELD_NAME)
+
+ self.Serializer = Serializer
+ self.obj = BlogPost.objects.create(title="New blog post")
+
+ def tearDown(self):
+ api_settings.URL_FIELD_NAME = self.saved_url_field_name
+
+ def test_overridden_url_field_name(self):
+ request = factory.get('/posts/')
+ serializer = self.Serializer(self.obj, context={'request': request})
+ self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
+
+
+class TestURLFieldNameByOptions(TestCase):
+ urls = 'rest_framework.tests.test_hyperlinkedserializers'
+
+ def setUp(self):
+ class Serializer(serializers.HyperlinkedModelSerializer):
+
+ class Meta:
+ model = BlogPost
+ fields = ('title', 'serializer_url_field')
+ url_field_name = 'serializer_url_field'
+
+ self.Serializer = Serializer
+ self.obj = BlogPost.objects.create(title="New blog post")
+
+ def test_overridden_url_field_name(self):
+ request = factory.get('/posts/')
+ serializer = self.Serializer(self.obj, context={'request': request})
+ self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py
index d19219c9..f52e0e1e 100644
--- a/rest_framework/tests/test_relations.py
+++ b/rest_framework/tests/test_relations.py
@@ -98,3 +98,23 @@ class RelatedFieldSourceTests(TestCase):
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
+
+ # Regression for #1129
+ def test_exception_for_incorect_fk(self):
+ """
+ Check that the exception message are correct if the source field
+ doesn't exist.
+ """
+ from rest_framework.tests.models import ManyToManySource
+ class Meta:
+ model = ManyToManySource
+ attrs = {
+ 'name': serializers.SlugRelatedField(
+ slug_field='name', source='banzai'),
+ 'Meta': Meta,
+ }
+
+ TestSerializer = type(str('TestSerializer'),
+ (serializers.ModelSerializer,), attrs)
+ with self.assertRaises(AttributeError):
+ TestSerializer(data={'name': 'foo'})
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py
index 2ae8ae18..fb33df2c 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/rest_framework/tests/test_renderers.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from decimal import Decimal
from django.core.cache import cache
+from django.db import models
from django.test import TestCase
from django.utils import unittest
from django.utils.translation import ugettext_lazy as _
@@ -34,6 +35,10 @@ expected_results = [
]
+class DummyTestModel(models.Model):
+ name = models.CharField(max_length=42, default='')
+
+
class BasicRendererTests(TestCase):
def test_expected_results(self):
for value, renderer_cls, expected in expected_results:
@@ -276,6 +281,20 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
+ def test_render_queryset_values(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [{'id': o.id, 'name': o.name}])
+
+ def test_render_queryset_values_list(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values_list('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [[o.id, o.name]])
+
def test_render_dict_abc_obj(self):
class Dict(MutableMapping):
def __init__(self):
diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py
index a60e7615..c0b50f33 100644
--- a/rest_framework/tests/test_request.py
+++ b/rest_framework/tests/test_request.py
@@ -68,6 +68,9 @@ class TestMethodOverloading(TestCase):
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
+ request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
+ self.assertEqual(request.method, 'DELETE')
+
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
diff --git a/rest_framework/tests/test_serializer_import.py b/rest_framework/tests/test_serializer_import.py
new file mode 100644
index 00000000..9f30a7ff
--- /dev/null
+++ b/rest_framework/tests/test_serializer_import.py
@@ -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)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
index 7114a060..6d69ffbd 100644
--- a/rest_framework/tests/test_serializer_nested.py
+++ b/rest_framework/tests/test_serializer_nested.py
@@ -345,4 +345,3 @@ class NestedModelSerializerUpdateTests(TestCase):
result = deserialize.object
result.save()
self.assertEqual(result.id, john.id)
-
diff --git a/rest_framework/tests/test_serializers.py b/rest_framework/tests/test_serializers.py
new file mode 100644
index 00000000..082a400c
--- /dev/null
+++ b/rest_framework/tests/test_serializers.py
@@ -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')
diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py
index 48b8956b..71bd8b55 100644
--- a/rest_framework/tests/test_testing.py
+++ b/rest_framework/tests/test_testing.py
@@ -1,6 +1,8 @@
# -- coding: utf-8 --
from __future__ import unicode_literals
+from io import BytesIO
+
from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.compat import patterns, url
@@ -143,3 +145,10 @@ class TestAPIRequestFactory(TestCase):
force_authenticate(request, user=user)
response = view(request)
self.assertEqual(response.data['user'], 'example')
+
+ def test_upload_file(self):
+ # This is a 1x1 black png
+ simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82')
+ simple_png.name = 'test.png'
+ factory = APIRequestFactory()
+ factory.post('/', data={'image': simple_png})
diff --git a/rest_framework/tests/test_write_only_fields.py b/rest_framework/tests/test_write_only_fields.py
new file mode 100644
index 00000000..aabb18d6
--- /dev/null
+++ b/rest_framework/tests/test_write_only_fields.py
@@ -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'})
diff --git a/rest_framework/tests/users/__init__.py b/rest_framework/tests/users/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rest_framework/tests/users/__init__.py
diff --git a/rest_framework/tests/users/models.py b/rest_framework/tests/users/models.py
new file mode 100644
index 00000000..128bac90
--- /dev/null
+++ b/rest_framework/tests/users/models.py
@@ -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)
diff --git a/rest_framework/tests/users/serializers.py b/rest_framework/tests/users/serializers.py
new file mode 100644
index 00000000..da496554
--- /dev/null
+++ b/rest_framework/tests/users/serializers.py
@@ -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
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 3ac920c6..e5fa4194 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -2,6 +2,7 @@
Helper classes for parsers.
"""
from __future__ import unicode_literals
+from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from rest_framework.compat import timezone, force_text
@@ -42,6 +43,8 @@ class JSONEncoder(json.JSONEncoder):
return str(o.total_seconds())
elif isinstance(o, decimal.Decimal):
return str(o)
+ elif isinstance(o, QuerySet):
+ return list(o)
elif hasattr(o, 'tolist'):
return o.tolist()
elif hasattr(o, '__getitem__'):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index e863af6d..02a6e25a 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -112,12 +112,13 @@ class APIView(View):
@property
def default_response_headers(self):
- # TODO: deprecate?
- # TODO: Only vary by accept if multiple renderers
- return {
+ headers = {
'Allow': ', '.join(self.allowed_methods),
- 'Vary': 'Accept'
}
+ if len(self.renderer_classes) > 1:
+ headers['Vary'] = 'Accept'
+ return headers
+
def http_method_not_allowed(self, request, *args, **kwargs):
"""
diff --git a/setup.py b/setup.py
index 969b3fc1..78cdb628 100755
--- a/setup.py
+++ b/setup.py
@@ -55,7 +55,7 @@ if sys.argv[-1] == 'publish':
setup(
name='djangorestframework',
version=version,
- url='http://django-rest-framework.org',
+ url='http://www.django-rest-framework.org',
license='BSD',
description='Web APIs for Django, made easy.',
author='Tom Christie',