diff options
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 @@ -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 Binary files differnew file mode 100644 index 00000000..73de34f4 --- /dev/null +++ b/docs/img/logo.png 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&repo=django-rest-framework&type=watch&count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe> -<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a> +<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script> <img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image"> </p> -# Django REST framework +--- + +<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&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. @@ -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): """ @@ -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', |
