diff --git a/.travis.yml b/.travis.yml index d5a0f16e2..7b9f9366c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,18 +7,22 @@ python: - "3.3" env: - - DJANGO="django==1.6.1" + - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" + - DJANGO="django==1.6.2" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" install: - pip install $DJANGO - - pip install defusedxml==0.3 - - pip install django-filter==0.6 - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" + - pip install defusedxml==0.3 Pillow==2.3.0 + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" + - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" + - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -26,6 +30,8 @@ script: matrix: exclude: + - python: "2.6" + env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - python: "3.2" env: DJANGO="django==1.4.10" - python: "3.3" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0544a479..a7aa6fc40 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 684868006..73e4b13fc 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. @@ -113,7 +113,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou # License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without @@ -143,21 +143,21 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [sandbox]: http://restframework.herokuapp.com/ -[index]: http://django-rest-framework.org/ -[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication -[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication -[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers -[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer -[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views -[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html -[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html -[routers]: http://django-rest-framework.org/api-guide/routers.html -[serializers]: http://django-rest-framework.org/api-guide/serializers.html -[authentication]: http://django-rest-framework.org/api-guide/authentication.html +[index]: http://www.django-rest-framework.org/ +[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication +[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication +[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers +[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer +[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views +[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html +[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html +[routers]: http://www.django-rest-framework.org/api-guide/routers.html +[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html +[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html -[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html +[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion -[image]: http://django-rest-framework.org/img/quickstart.png +[image]: http://www.django-rest-framework.org/img/quickstart.png [tox]: http://testrun.org/tox/latest/ @@ -165,7 +165,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [wlonk]: https://twitter.com/wlonk/status/261689665952833536 [laserllama]: https://twitter.com/laserllama/status/328688333750407168 -[docs]: http://django-rest-framework.org/ +[docs]: http://www.django-rest-framework.org/ [urlobject]: https://github.com/zacharyvoase/urlobject [markdown]: http://pypi.python.org/pypi/Markdown/ [pyyaml]: http://pypi.python.org/pypi/PyYAML diff --git a/docs/404.html b/docs/404.html index 4938da6ea..864247e78 100644 --- a/docs/404.html +++ b/docs/404.html @@ -3,17 +3,17 @@ Django REST framework - 404 - Page not found - - + + - - - - + + + + @@ -176,9 +176,9 @@ - - - + + +

-# Django REST framework +--- -**Awesome web-browsable Web APIs.** +

+

Django REST Framework

+ +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 @@ -219,32 +239,33 @@ Send a description of the issue via email to [rest-framework-security@googlegrou ## License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. -Redistribution and use in source and binary forms, with or without +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -Redistributions of source code must retain the above copyright notice, this +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master -[urlobject]: https://github.com/zacharyvoase/urlobject +[mozilla]: http://www.mozilla.org/en-US/about/ +[eventbrite]: https://www.eventbrite.co.uk/about/ [markdown]: http://pypi.python.org/pypi/Markdown/ [yaml]: http://pypi.python.org/pypi/PyYAML [defusedxml]: https://pypi.python.org/pypi/defusedxml diff --git a/docs/template.html b/docs/template.html index c065237a5..a397d067b 100644 --- a/docs/template.html +++ b/docs/template.html @@ -170,31 +170,12 @@ + - - diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 0f980e1cb..a997c7829 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/contributing.md b/docs/topics/contributing.md index 30d292f80..18a05050c 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -14,7 +14,7 @@ If you use REST framework, we'd love you to be vocal about your experiences with Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. -When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. ## Code of conduct @@ -38,7 +38,7 @@ Some tips on good issue reporting: ## Triaging issues -Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to * Read through the ticket - does it make sense, is it missing any context that would help explain it better? * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? @@ -60,14 +60,14 @@ To run the tests, clone the repository, and then: # Setup the virtual environment virtualenv env - env/bin/activate + source env/bin/activate pip install -r requirements.txt pip install -r optionals.txt # Run the tests rest_framework/runtests/runtests.py -You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: +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 @@ -130,8 +130,8 @@ There are a couple of conventions you should follow when working on the document Headers should use the hash style. For example: ### Some important topic - -The underline style should not be used. **Don't do this:** + +The underline style should not be used. **Don't do this:** Some important topic ==================== @@ -141,9 +141,9 @@ The underline style should not be used. **Don't do this:** Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. Here is a link to [some other thing][other-thing]. - + More text... - + [other-thing]: http://example.com/other/thing This style helps keep the documentation source consistent and readable. @@ -159,9 +159,9 @@ Linking in this style means you'll be able to click the hyperlink in your markdo If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: --- - + **Note:** A useful documentation note. - + --- # Third party packages diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d4c00bc4e..5f0dc7522 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -182,6 +182,7 @@ The following people have helped make REST framework great. * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] * Philip Forget - [philipforget] +* Artem Mezhenin - [amezhenin] Many thanks to everyone who's contributed to the project. @@ -400,3 +401,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ian-foote]: https://github.com/ian-foote [chuckharmston]: https://github.com/chuckharmston [philipforget]: https://github.com/philipforget +[amezhenin]: https://github.com/amezhenin diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index da17aa749..37addc8d7 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -38,8 +38,6 @@ You can determine your currently installed version using `pip freeze`: --- -## 2.3.x series - ### 2.4.0 * `@detail_route` and `@list_route` decorators replace `@action` and `@link`. @@ -50,12 +48,40 @@ You can determine your currently installed version using `pip freeze`: * Added `cache` attribute to throttles to allow overriding of default cache. * Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off. -### Master +## 2.3.x series + +### 2.3.13 +## 2.3.x series + + +**Date**: 6th March 2014 + +* Django 1.7 Support. +* Fix `default` argument when used with serializer relation fields. +* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'. +* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`. +* Use `urandom` for token generation. +* Only use `Vary: Accept` when more than one renderer exists. + +### 2.3.12 + +**Date**: 15th January 2014 + +* **Security fix**: `OrderingField` now only allows ordering on readable serializer fields, or on fields explicitly specified using `ordering_fields`. This prevents users being able to order by fields that are not visible in the API, and exploiting the ordering of sensitive data such as password hashes. +* Bugfix: `write_only = True` fields now display in the browsable API. + +### 2.3.11 + +**Date**: 14th January 2014 + +* Added `write_only` serializer field argument. +* Added `write_only_fields` option to `ModelSerializer` classes. * JSON renderer now deals with objects that implement a dict-like interface. * Fix compatiblity with newer versions of `django-oauth-plus`. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. * Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. +* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params. ### 2.3.10 @@ -74,7 +100,6 @@ You can determine your currently installed version using `pip freeze`: * Fix Django 1.6 exception API compatibility issue caused by `ValidationError`. * Include errors in HTML forms in browsable API. ->>>>>>> master * Added JSON renderer support for numpy scalars. * Added `transform_` hooks on serializers for easily modifying field output. * Added `get_context` hook in `BrowsableAPIRenderer`. @@ -100,15 +125,15 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. -* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*] +* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*] --- -[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. +[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. class DisablePaginationMixin(object): 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 2298df59a..979c4a3e3 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/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index bdc6b5791..432371f34 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user. -We can add a login view for use with the browsable API, by editing our URLconf once more. +We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file. Add the following import at the top of the file: diff --git a/mkdocs.py b/mkdocs.py index 09c9dcc67..f973096f3 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 }}', """

The team behind REST framework is launching a new API service.

+

If you want to be first in line when we start issuing invitations, please sign up here.

""") + else: + output = output.replace('{{ ad_block }}', '') + if prev_url: output = output.replace('{{ prev_url }}', prev_url) output = output.replace('{{ prev_url_disabled }}', '') diff --git a/optionals.txt b/optionals.txt index 96f4b2f44..262e76443 100644 --- a/optionals.txt +++ b/optionals.txt @@ -5,3 +5,4 @@ django-filter>=0.5.4 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +Pillow==2.3.0 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index f5483b9d6..2d76b55d5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,10 +8,10 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.10' +__version__ = '2.3.13' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' -__copyright__ = 'Copyright 2011-2013 Tom Christie' +__copyright__ = 'Copyright 2011-2014 Tom Christie' # Version synonym VERSION = __version__ diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 1f8d37fa5..cbc83574a 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -7,6 +7,7 @@ import base64 from django.contrib.auth import authenticate from django.core.exceptions import ImproperlyConfigured from django.middleware.csrf import CsrfViewMiddleware +from django.conf import settings from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import oauth, oauth_provider, oauth_provider_store from rest_framework.compat import oauth2_provider, provider_now, check_nonce @@ -291,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication): OAuth 2 authentication backend using `django-oauth2-provider` """ www_authenticate_realm = 'api' + allow_query_params_token = settings.DEBUG def __init__(self, *args, **kwargs): super(OAuth2Authentication, self).__init__(*args, **kwargs) @@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != b'bearer': + if auth and auth[0].lower() == b'bearer': + access_token = auth[1] + elif 'access_token' in request.POST: + access_token = request.POST['access_token'] + elif 'access_token' in request.GET and self.allow_query_params_token: + access_token = request.GET['access_token'] + else: return None if len(auth) == 1: @@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication): msg = 'Invalid bearer header. Token string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(request, auth[1]) + return self.authenticate_credentials(request, access_token) def authenticate_credentials(self, request, access_token): """ @@ -326,11 +334,11 @@ class OAuth2Authentication(BaseAuthentication): """ try: - token = oauth2_provider.models.AccessToken.objects.select_related('user') + token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user') # provider_now switches to timezone aware datetime when # the oauth2_provider version supports to it. token = token.get(token=access_token, expires__gt=provider_now()) - except oauth2_provider.models.AccessToken.DoesNotExist: + except oauth2_provider.oauth2.models.AccessToken.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') user = token.user diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 024f62bfe..8eac2cc49 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,5 +1,5 @@ -import uuid -import hmac +import binascii +import os from hashlib import sha1 from django.conf import settings from django.db import models @@ -34,8 +34,7 @@ class Token(models.Model): return super(Token, self).save(*args, **kwargs) def generate_key(self): - unique = uuid.uuid4() - return hmac.new(unique.bytes, digestmod=sha1).hexdigest() + return binascii.hexlify(os.urandom(20)) def __unicode__(self): return self.key diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 45045c0f0..a013a1559 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -121,7 +121,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.4 the test client just uses smart_str from django.utils.encoding import smart_str as force_bytes_or_smart_bytes @@ -216,13 +216,10 @@ except (ImportError, ImproperlyConfigured): # OAuth 2 support is optional try: - import provider.oauth2 as oauth2_provider - from provider.oauth2 import models as oauth2_provider_models - from provider.oauth2 import forms as oauth2_provider_forms + import provider as oauth2_provider from provider import scope as oauth2_provider_scope from provider import constants as oauth2_constants - from provider import __version__ as provider_version - if provider_version in ('0.2.3', '0.2.4'): + if oauth2_provider.__version__ in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes import datetime @@ -232,8 +229,6 @@ try: from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None - oauth2_provider_models = None - oauth2_provider_forms = None oauth2_provider_scope = None oauth2_constants = None provider_now = None @@ -251,3 +246,23 @@ if six.PY3: else: def is_non_str_iterable(obj): return hasattr(obj, '__iter__') + + +try: + from django.utils.encoding import python_2_unicode_compatible +except ImportError: + def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 425a72149..5f774a9f3 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -6,47 +6,42 @@ In addition Django's built in 403 and 404 exceptions are handled. """ from __future__ import unicode_literals from rest_framework import status +import math class APIException(Exception): """ Base class for REST framework exceptions. - Subclasses should provide `.status_code` and `.detail` properties. + Subclasses should provide `.status_code` and `.default_detail` properties. """ - pass + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = '' + def __init__(self, detail=None): + self.detail = detail or self.default_detail + + def __str__(self): + return self.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 +70,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 16485b414..533de28c2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -166,7 +166,7 @@ class Field(object): Called to set up a field prior to field_to_native or field_from_native. parent - The parent serializer. - model_field - The model field this field corresponds to, if one exists. + field_name - The name of the field being initialized. """ self.parent = parent self.root = parent.root or parent @@ -248,6 +248,7 @@ class WritableField(Field): """ Base for read/write fields. """ + write_only = False default_validators = [] default_error_messages = { 'required': _('This field is required.'), @@ -257,13 +258,17 @@ 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): 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: @@ -291,6 +296,11 @@ class WritableField(Field): result.validators = self.validators[:] return result + def get_default_value(self): + if is_simple_callable(self.default): + return self.default() + return self.default + def validate(self, value): if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) @@ -313,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`, @@ -334,10 +349,7 @@ class WritableField(Field): except KeyError: if self.default is not None and not self.partial: # Note: partial updates shouldn't set defaults - if is_simple_callable(self.default): - native = self.default() - else: - native = self.default + native = self.get_default_value() else: if self.required: raise ValidationError(self.error_messages['required']) @@ -465,7 +477,8 @@ class URLField(CharField): type_label = 'url' def __init__(self, **kwargs): - kwargs['validators'] = [validators.URLValidator()] + if not 'validators' in kwargs: + kwargs['validators'] = [validators.URLValidator()] super(URLField, self).__init__(**kwargs) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 5c6a187c4..96d15eb9d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -3,8 +3,10 @@ Provides generic filtering backends that can be used to filter the results returned by list views. """ from __future__ import unicode_literals +from django.core.exceptions import ImproperlyConfigured from django.db import models from rest_framework.compat import django_filters, six, guardian, get_model_name +from rest_framework.settings import api_settings from functools import reduce import operator @@ -68,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend): class SearchFilter(BaseFilterBackend): - search_param = 'search' # The URL query parameter used for the search. + # The URL query parameter used for the search. + search_param = api_settings.SEARCH_PARAM def get_search_terms(self, request): """ @@ -106,7 +109,9 @@ class SearchFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend): - ordering_param = 'ordering' # The URL query parameter used for the ordering. + # The URL query parameter used for the ordering. + ordering_param = api_settings.ORDERING_PARAM + ordering_fields = None def get_ordering(self, request): """ @@ -122,17 +127,34 @@ class OrderingFilter(BaseFilterBackend): return (ordering,) return ordering - def remove_invalid_fields(self, queryset, ordering): - field_names = [field.name for field in queryset.model._meta.fields] - field_names += queryset.query.aggregates.keys() - return [term for term in ordering if term.lstrip('-') in field_names] + def remove_invalid_fields(self, queryset, ordering, view): + valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) + + if valid_fields is None: + # Default to allowing filtering on serializer fields + serializer_class = getattr(view, 'serializer_class') + if serializer_class is None: + msg = ("Cannot use %s on a view which does not have either a " + "'serializer_class' or 'ordering_fields' attribute.") + raise ImproperlyConfigured(msg % self.__class__.__name__) + valid_fields = [ + field.source or field_name + for field_name, field in serializer_class().fields.items() + if not getattr(field, 'write_only', False) + ] + elif valid_fields == '__all__': + # View explictly allows filtering on any model field + valid_fields = [field.name for field in queryset.model._meta.fields] + valid_fields += queryset.query.aggregates.keys() + + return [term for term in ordering if term.lstrip('-') in valid_fields] def filter_queryset(self, request, queryset, view): ordering = self.get_ordering(request) if ordering: # Skip any incorrect parameters - ordering = self.remove_invalid_fields(queryset, ordering) + ordering = self.remove_invalid_fields(queryset, ordering, view) if not ordering: # Use 'ordering' attribute by default diff --git a/rest_framework/generics.py b/rest_framework/generics.py index bd33c01ab..c3256844b 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 b62a4cc17..2cc87eef1 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 {} @@ -115,30 +116,27 @@ class UpdateModelMixin(object): partial = kwargs.pop('partial', False) self.object = self.get_object_or_none() - if self.object is None: - created = True - save_kwargs = {'force_insert': True} - success_status_code = status.HTTP_201_CREATED - else: - created = False - save_kwargs = {'force_update': True} - success_status_code = status.HTTP_200_OK - serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES, partial=partial) - if serializer.is_valid(): - try: - self.pre_save(serializer.object) - except ValidationError as err: - # full_clean on model instance may be called in pre_save, so we - # have to handle eventual errors. - return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) - self.object = serializer.save(**save_kwargs) - self.post_save(self.object, created=created) - return Response(serializer.data, status=success_status_code) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + try: + self.pre_save(serializer.object) + except ValidationError as err: + # full_clean on model instance may be called in pre_save, + # so we have to handle eventual errors. + return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) + + if self.object is None: + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + self.object = serializer.save(force_update=True) + self.post_save(self.object, created=False) + return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 4785c0093..3b234dd58 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -33,6 +33,7 @@ class RelatedField(WritableField): many_widget = widgets.SelectMultiple form_field_class = forms.ChoiceField many_form_field_class = forms.MultipleChoiceField + null_values = (None, '', 'None') cache_choices = False empty_label = None @@ -50,6 +51,8 @@ class RelatedField(WritableField): super(RelatedField, self).__init__(*args, **kwargs) if not self.required: + # Accessed in ModelChoiceIterator django/forms/models.py:1034 + # If set adds empty choice. self.empty_label = BLANK_CHOICE_DASH[0][1] self.queryset = queryset @@ -57,16 +60,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... @@ -115,6 +113,14 @@ class RelatedField(WritableField): choices = property(_get_choices, _set_choices) + ### Default value handling + + def get_default_value(self): + default = super(RelatedField, self).get_default_value() + if self.many and default is None: + return [] + return default + ### Regular serializer stuff... def field_to_native(self, obj, field_name): @@ -163,11 +169,11 @@ class RelatedField(WritableField): except KeyError: if self.partial: return - value = [] if self.many else None + value = self.get_default_value() - if value in (None, '') and self.required: - raise ValidationError(self.error_messages['required']) - elif value in (None, ''): + if value in self.null_values: + if self.required: + raise ValidationError(self.error_messages['required']) into[(self.source or field_name)] = None elif self.many: into[(self.source or field_name)] = [self.from_native(item) for item in value] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2fdd33376..7a7da5610 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 @@ -145,7 +146,7 @@ class XMLRenderer(BaseRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): """ - Renders *obj* into serialized XML. + Renders `data` into serialized XML. """ if data is None: return '' @@ -195,7 +196,7 @@ class YAMLRenderer(BaseRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): """ - Renders *obj* into serialized YAML. + Renders `data` into serialized YAML. """ assert yaml, 'YAMLRenderer requires pyyaml to be installed' @@ -426,7 +427,7 @@ class BrowsableAPIRenderer(BaseRenderer): files = request.FILES except ParseError: data = None - files = None + files = None else: data = None files = None @@ -543,6 +544,14 @@ class BrowsableAPIRenderer(BaseRenderer): raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form + response_headers = dict(response.items()) + renderer_content_type = '' + if renderer: + renderer_content_type = '%s' % renderer.media_type + if renderer.charset: + renderer_content_type += ' ;%s' % renderer.charset + response_headers['Content-Type'] = renderer_content_type + context = { 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, @@ -554,6 +563,7 @@ class BrowsableAPIRenderer(BaseRenderer): 'breadcrumblist': self.get_breadcrumbs(request), 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], + 'response_headers': response_headers, 'put_form': self.get_rendered_html_form(view, 'PUT', request), 'post_form': self.get_rendered_html_form(view, 'POST', request), @@ -597,7 +607,7 @@ class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' format = 'multipart' charset = 'utf-8' - BOUNDARY = 'BoUnDaRyStRiNg' + BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg' def render(self, data, accepted_media_type=None, renderer_context=None): return encode_multipart(self.BOUNDARY, data) diff --git a/rest_framework/request.py b/rest_framework/request.py index fcea25083..40467c03d 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): """ @@ -347,7 +346,7 @@ class Request(object): media_type = self.content_type if stream is None or media_type is None: - empty_data = QueryDict('', self._request._encoding) + empty_data = QueryDict('', encoding=self._request._encoding) empty_files = MultiValueDict() return (empty_data, empty_files) @@ -363,7 +362,7 @@ class Request(object): # re-raise. Ensures we don't simply repeat the error when # attempting to render the browsable renderer response, or when # logging the request or similar. - self._data = QueryDict('', self._request._encoding) + self._data = QueryDict('', encoding=self._request._encoding) self._files = MultiValueDict() raise diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index da36d23fc..2daaae4ee 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -26,6 +26,10 @@ def usage(): def main(): + try: + django.setup() + except AttributeError: + pass TestRunner = get_runner(settings) test_runner = TestRunner() diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 12aa73e7b..36283d8ec 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -97,6 +97,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 cbf73fc30..5b14e4032 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,12 +13,15 @@ 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.db import models from django.forms import widgets from django.utils.datastructures import SortedDict -from rest_framework.compat import six +from rest_framework.compat import get_concrete_model, six +from rest_framework.settings import api_settings + # Note: We do the following so that users of the framework can use this style: # @@ -31,6 +34,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: @@ -325,12 +349,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. """ @@ -360,6 +385,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) @@ -404,16 +432,6 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return - # Set the serializer object if it exists - obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - - # If we have a model manager or similar object then we need - # to iterate through each instance. - if (self.many and - not hasattr(obj, '__iter__') and - is_simple_callable(getattr(obj, 'all', None))): - obj = obj.all() - if self.source == '*': if value: reverted_data = self.restore_fields(value, {}) @@ -423,6 +441,16 @@ class BaseSerializer(WritableField): if value in (None, ''): into[(self.source or field_name)] = None else: + # Set the serializer object if it exists + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() + kwargs = { 'instance': obj, 'data': value, @@ -467,7 +495,7 @@ class BaseSerializer(WritableField): else: many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=3) @@ -524,7 +552,16 @@ class BaseSerializer(WritableField): if self._data is None: obj = self.object - if self.many: + if self.many is not None: + many = self.many + else: + many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) + if many: + warnings.warn('Implicit list/queryset serialization is deprecated. ' + 'Use the `many=True` flag when instantiating the serializer.', + DeprecationWarning, stacklevel=2) + + if many: self._data = [self.to_native(item) for item in obj] else: self._data = self.to_native(obj) @@ -578,6 +615,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): @@ -641,7 +679,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 @@ -713,20 +751,38 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field + + # Ensure that 'read_only_fields' is an iterable + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' - # Add the `read_only` flag to any fields that have bee specified + # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: - assert field_name not in self.base_fields.keys(), \ - "field '%s' on serializer '%s' specified in " \ - "`read_only_fields`, but also added " \ - "as an explicit field. Remove it from `read_only_fields`." % \ - (field_name, self.__class__.__name__) - assert field_name in ret, \ - "Non-existant field '%s' specified in `read_only_fields` " \ - "on serializer '%s'." % \ - (field_name, self.__class__.__name__) + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`read_only_fields`, but also added " + "as an explicit field. Remove it from `read_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `read_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) ret[field_name].read_only = True + + # Ensure that 'write_only_fields' is an iterable + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' + + for field_name in self.opts.write_only_fields: + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`write_only_fields`, but also added " + "as an explicit field. Remove it from `write_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `write_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) + ret[field_name].write_only = True return ret @@ -829,7 +885,7 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def get_validation_exclusions(self): + def get_validation_exclusions(self, instance=None): """ Return a list of field names to exclude from model validation. """ @@ -841,6 +897,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ + and (field.required or hasattr(instance, field_name)) \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -855,7 +912,7 @@ class ModelSerializer(Serializer): the full_clean validation checking. """ try: - instance.full_clean(exclude=self.get_validation_exclusions()) + instance.full_clean(exclude=self.get_validation_exclusions(instance)) except ValidationError as err: self._errors = err.message_dict return None @@ -883,7 +940,7 @@ class ModelSerializer(Serializer): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many: + for field in meta.many_to_many + meta.virtual_fields: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) @@ -979,6 +1036,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): @@ -997,13 +1055,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 @@ -1039,7 +1097,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 383de72ea..189131f15 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -70,6 +70,10 @@ DEFAULTS = { 'PAGINATE_BY_PARAM': None, 'MAX_PAGINATE_BY': None, + # Filtering + 'SEARCH_PARAM': 'search', + 'ORDERING_PARAM': 'ordering', + # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, @@ -96,6 +100,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 42ede9682..210741ed6 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -34,7 +34,7 @@