mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 12:30:11 +03:00
Merge remote-tracking branch 'origin/master' into feature/dotted_names_for_regular_fields
Conflicts: rest_framework/serializers.py
This commit is contained in:
commit
5de99a60f9
18
.travis.yml
18
.travis.yml
|
@ -8,10 +8,10 @@ python:
|
||||||
- "3.4"
|
- "3.4"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
- DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- DJANGO="django==1.6.3"
|
- DJANGO="django==1.6.5"
|
||||||
- DJANGO="django==1.5.6"
|
- DJANGO="django==1.5.8"
|
||||||
- DJANGO="django==1.4.11"
|
- DJANGO="django==1.4.13"
|
||||||
- DJANGO="django==1.3.7"
|
- DJANGO="django==1.3.7"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
@ -24,7 +24,7 @@ install:
|
||||||
- "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.5.4; fi"
|
||||||
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; 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 [[ ${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.7b2/tarball/' ]]; 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.7.b4/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
|
||||||
- export PYTHONPATH=.
|
- export PYTHONPATH=.
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
@ -33,16 +33,16 @@ script:
|
||||||
matrix:
|
matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "2.6"
|
- python: "2.6"
|
||||||
env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/"
|
env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.2"
|
- python: "3.2"
|
||||||
env: DJANGO="django==1.3.7"
|
env: DJANGO="django==1.3.7"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.3"
|
- python: "3.3"
|
||||||
env: DJANGO="django==1.3.7"
|
env: DJANGO="django==1.3.7"
|
||||||
- python: "3.4"
|
- python: "3.4"
|
||||||
env: DJANGO="django==1.4.11"
|
env: DJANGO="django==1.4.13"
|
||||||
- python: "3.4"
|
- python: "3.4"
|
||||||
env: DJANGO="django==1.3.7"
|
env: DJANGO="django==1.3.7"
|
||||||
|
|
11
README.md
11
README.md
|
@ -1,3 +1,13 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Django REST framework 3 - Kickstarter announcement!
|
||||||
|
|
||||||
|
We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3.
|
||||||
|
|
||||||
|
If you want to help drive sustainable open-source development forward, then **please check out [the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Django REST framework
|
# Django REST framework
|
||||||
|
|
||||||
[![build-status-image]][travis]
|
[![build-status-image]][travis]
|
||||||
|
@ -136,6 +146,7 @@ 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
|
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.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
|
||||||
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
|
||||||
[twitter]: https://twitter.com/_tomchristie
|
[twitter]: https://twitter.com/_tomchristie
|
||||||
|
|
|
@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
|
||||||
|
|
||||||
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
|
||||||
|
|
||||||
To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
|
|
|
@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`.
|
||||||
|
|
||||||
## ChoiceField
|
## ChoiceField
|
||||||
|
|
||||||
A field that can accept a value out of a limited set of choices.
|
A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice.
|
||||||
|
|
||||||
|
**Signature:** `ChoiceField(choices=(), blank_display_value=None)`
|
||||||
|
|
||||||
## EmailField
|
## EmailField
|
||||||
|
|
||||||
|
@ -355,9 +357,16 @@ The following third party packages are also available.
|
||||||
|
|
||||||
The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
|
The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
|
||||||
|
|
||||||
|
## DRF Extra Fields
|
||||||
|
|
||||||
|
The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
|
||||||
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
|
||||||
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||||
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
|
||||||
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
[iso8601]: http://www.w3.org/TR/NOTE-datetime
|
||||||
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
|
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
|
||||||
|
[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields
|
|
@ -199,8 +199,7 @@ This enables us to make queries like:
|
||||||
|
|
||||||
http://example.com/api/products?manufacturer__name=foo
|
http://example.com/api/products?manufacturer__name=foo
|
||||||
|
|
||||||
This is nice, but it shows underlying model structure in REST API, which may
|
This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class:
|
||||||
be undesired, but you can use:
|
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from myapp.models import Product
|
from myapp.models import Product
|
||||||
|
@ -208,7 +207,6 @@ be undesired, but you can use:
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
manufacturer = django_filters.CharFilter(name="manufacturer__name")
|
manufacturer = django_filters.CharFilter(name="manufacturer__name")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -187,7 +187,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself,
|
||||||
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
|
||||||
|
|
||||||
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
|
||||||
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
|
* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
|
||||||
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
|
||||||
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
|
||||||
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
|
||||||
|
|
|
@ -36,6 +36,12 @@ For example:
|
||||||
self.check_object_permissions(self.request, obj)
|
self.check_object_permissions(self.request, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
#### Limitations of object level permissions
|
||||||
|
|
||||||
|
For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
|
||||||
|
|
||||||
|
Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view.
|
||||||
|
|
||||||
## Setting the permission policy
|
## Setting the permission policy
|
||||||
|
|
||||||
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
|
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
|
||||||
|
@ -237,6 +243,7 @@ The [REST Condition][rest-condition] package is another extension for building c
|
||||||
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
[throttling]: throttling.md
|
[throttling]: throttling.md
|
||||||
|
[filtering]: filtering.md
|
||||||
[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
|
[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
|
||||||
[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions
|
[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions
|
||||||
[guardian]: https://github.com/lukaszb/django-guardian
|
[guardian]: https://github.com/lukaszb/django-guardian
|
||||||
|
|
|
@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
|
||||||
|
|
||||||
app.router.register_model(MyModel)
|
app.router.register_model(MyModel)
|
||||||
|
|
||||||
|
## DRF-extensions
|
||||||
|
|
||||||
|
The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
|
||||||
|
|
||||||
[cite]: http://guides.rubyonrails.org/routing.html
|
[cite]: http://guides.rubyonrails.org/routing.html
|
||||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
[wq.db]: http://wq.io/wq.db
|
[wq.db]: http://wq.io/wq.db
|
||||||
[wq.db-router]: http://wq.io/docs/app.py
|
[wq.db-router]: http://wq.io/docs/app.py
|
||||||
|
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
|
||||||
|
[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
|
||||||
|
[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
|
||||||
|
[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
|
||||||
|
[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
|
|
@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa
|
||||||
|
|
||||||
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
|
||||||
|
|
||||||
description = serializers.TextField()
|
description = serializers.CharField()
|
||||||
description_html = serializers.TextField(source='description', read_only=True)
|
description_html = serializers.CharField(source='description', read_only=True)
|
||||||
|
|
||||||
def transform_description_html(self, obj, value):
|
def transform_description_html(self, obj, value):
|
||||||
from django.contrib.markup.templatetags.markup import markdown
|
from django.contrib.markup.templatetags.markup import markdown
|
||||||
|
|
|
@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
|
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
|
||||||
|
|
||||||
@action(methods=['POST', 'DELETE'])
|
@action(methods=['POST', 'DELETE'])
|
||||||
def unset_password(self, request, pk=None):
|
def unset_password(self, request, pk=None):
|
||||||
|
|
|
@ -9,6 +9,14 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### Django REST framework 3 - Kickstarter announcement!
|
||||||
|
|
||||||
|
We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3.
|
||||||
|
|
||||||
|
If you want to help drive sustainable open-source development **please [check out the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<h1 style="position: absolute;
|
<h1 style="position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
@ -201,6 +209,7 @@ General guides to using REST framework.
|
||||||
* [2.0 Announcement][rest-framework-2-announcement]
|
* [2.0 Announcement][rest-framework-2-announcement]
|
||||||
* [2.2 Announcement][2.2-announcement]
|
* [2.2 Announcement][2.2-announcement]
|
||||||
* [2.3 Announcement][2.3-announcement]
|
* [2.3 Announcement][2.3-announcement]
|
||||||
|
* [Kickstarter Announcement][kickstarter-announcement]
|
||||||
* [Release Notes][release-notes]
|
* [Release Notes][release-notes]
|
||||||
* [Credits][credits]
|
* [Credits][credits]
|
||||||
|
|
||||||
|
@ -325,6 +334,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
|
||||||
[2.2-announcement]: topics/2.2-announcement.md
|
[2.2-announcement]: topics/2.2-announcement.md
|
||||||
[2.3-announcement]: topics/2.3-announcement.md
|
[2.3-announcement]: topics/2.3-announcement.md
|
||||||
|
[kickstarter-announcement]: topics/kickstarter-announcement.md
|
||||||
[release-notes]: topics/release-notes.md
|
[release-notes]: topics/release-notes.md
|
||||||
[credits]: topics/credits.md
|
[credits]: topics/credits.md
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,21 @@
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
span.fusion-wrap a {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.fusion-poweredby {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
div.promo {display: none;}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body onload="prettyPrint()" class="{{ page_id }}-page">
|
<body onload="prettyPrint()" class="{{ page_id }}-page">
|
||||||
|
|
||||||
|
@ -106,6 +121,7 @@
|
||||||
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
|
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
|
||||||
|
<li><a href="{{ base_url }}/topics/kickstarter-announcement{{ suffix }}">Kickstarter Announcement</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
|
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
|
||||||
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
|
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -169,11 +185,9 @@
|
||||||
<div id="table-of-contents">
|
<div id="table-of-contents">
|
||||||
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
|
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
|
||||||
{{ toc }}
|
{{ toc }}
|
||||||
<div>
|
<div class="promo">
|
||||||
|
{{ ad_block }}
|
||||||
{{ ad_block }}
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -199,6 +213,7 @@
|
||||||
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
|
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
|
||||||
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
|
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
|
||||||
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
|
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//$('.side-nav').scrollspy()
|
//$('.side-nav').scrollspy()
|
||||||
var shiftWindow = function() { scrollBy(0, -50) };
|
var shiftWindow = function() { scrollBy(0, -50) };
|
||||||
|
|
31
docs/topics/kickstarter-announcement.md
Normal file
31
docs/topics/kickstarter-announcement.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Kickstarting Django REST framework 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<iframe width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
In order to continue to drive the project forward, I'm launching a Kickstarter campaign to help fund the development of a major new release - Django REST framework 3.
|
||||||
|
|
||||||
|
## Project details
|
||||||
|
|
||||||
|
This new release will allow us to comprehensively address some of the shortcomings of the framework, and will aim to include the following:
|
||||||
|
|
||||||
|
* Faster, simpler and easier-to-use serializers.
|
||||||
|
* An alternative admin-style interface for the browsable API.
|
||||||
|
* Search and filtering controls made accessible in the browsable API.
|
||||||
|
* Alternative API pagination styles.
|
||||||
|
* Documentation around API versioning.
|
||||||
|
* Triage of outstanding tickets.
|
||||||
|
* Improving the ongoing quality and maintainability of the project.
|
||||||
|
|
||||||
|
Full details are available now on the [project page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3).
|
||||||
|
|
||||||
|
If you're interested in helping make sustainable open source development a reality please [visit the Kickstarter page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding the project.
|
||||||
|
|
||||||
|
I can't wait to see where this takes us!
|
||||||
|
|
||||||
|
Many thanks to everyone for your support so far,
|
||||||
|
|
||||||
|
Tom Christie :)
|
|
@ -40,24 +40,28 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 2.3.x series
|
## 2.3.x series
|
||||||
|
|
||||||
### 2.3.x
|
### 2.3.14
|
||||||
|
|
||||||
**Date**: April 2014
|
**Date**: 12th June 2014
|
||||||
|
|
||||||
* Fix nested serializers linked through a backward foreign key relation
|
* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API.
|
||||||
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`
|
* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`.
|
||||||
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode
|
* Fix nested serializers linked through a backward foreign key relation.
|
||||||
* Fix `parse_header` argument convertion
|
* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`.
|
||||||
* Fix mediatype detection under Python3
|
* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
|
||||||
* Web browseable API now offers blank option on dropdown when the field is not required
|
* Fix `parse_header` argument convertion.
|
||||||
* `APIException` representation improved for logging purposes
|
* Fix mediatype detection under Python 3.
|
||||||
* Allow source="*" within nested serializers
|
* Web browseable API now offers blank option on dropdown when the field is not required.
|
||||||
* Better support for custom oauth2 provider backends
|
* `APIException` representation improved for logging purposes.
|
||||||
* Fix field validation if it's optional and has no value
|
* Allow source="*" within nested serializers.
|
||||||
* Add `SEARCH_PARAM` and `ORDERING_PARAM`
|
* Better support for custom oauth2 provider backends.
|
||||||
* Fix `APIRequestFactory` to support arguments within the url string for GET
|
* Fix field validation if it's optional and has no value.
|
||||||
* Allow three transport modes for access tokens when accessing a protected resource
|
* Add `SEARCH_PARAM` and `ORDERING_PARAM`.
|
||||||
* Fix `Request`'s `QueryDict` encoding
|
* Fix `APIRequestFactory` to support arguments within the url string for GET.
|
||||||
|
* Allow three transport modes for access tokens when accessing a protected resource.
|
||||||
|
* Fix `QueryDict` encoding on request objects.
|
||||||
|
* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`.
|
||||||
|
* Support `blank_display_value` on `ChoiceField`.
|
||||||
|
|
||||||
### 2.3.13
|
### 2.3.13
|
||||||
|
|
||||||
|
|
|
@ -162,8 +162,8 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
|
||||||
output = output.replace('{{ canonical_url }}', canonical_url)
|
output = output.replace('{{ canonical_url }}', canonical_url)
|
||||||
|
|
||||||
if filename =='index.md':
|
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>
|
output = output.replace('{{ ad_block }}', """<hr/>
|
||||||
<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>""")
|
<script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>""")
|
||||||
else:
|
else:
|
||||||
output = output.replace('{{ ad_block }}', '')
|
output = output.replace('{{ ad_block }}', '')
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '2.3.13'
|
__version__ = '2.3.14'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
__copyright__ = 'Copyright 2011-2014 Tom Christie'
|
||||||
|
|
|
@ -51,6 +51,7 @@ except ImportError:
|
||||||
# guardian is optional
|
# guardian is optional
|
||||||
try:
|
try:
|
||||||
import guardian
|
import guardian
|
||||||
|
import guardian.shortcuts # Fixes #1624
|
||||||
except ImportError:
|
except ImportError:
|
||||||
guardian = None
|
guardian = None
|
||||||
|
|
||||||
|
|
|
@ -187,7 +187,7 @@ class Field(object):
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
"""
|
"""
|
||||||
Given and object and a field name, returns the value that should be
|
Given an object and a field name, returns the value that should be
|
||||||
serialized for that field.
|
serialized for that field.
|
||||||
"""
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
|
@ -475,8 +475,12 @@ class CharField(WritableField):
|
||||||
self.validators.append(validators.MaxLengthValidator(max_length))
|
self.validators.append(validators.MaxLengthValidator(max_length))
|
||||||
|
|
||||||
def from_native(self, value):
|
def from_native(self, value):
|
||||||
if isinstance(value, six.string_types) or value is None:
|
if isinstance(value, six.string_types):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
|
||||||
return smart_text(value)
|
return smart_text(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -507,7 +511,7 @@ class SlugField(CharField):
|
||||||
|
|
||||||
class ChoiceField(WritableField):
|
class ChoiceField(WritableField):
|
||||||
type_name = 'ChoiceField'
|
type_name = 'ChoiceField'
|
||||||
type_label = 'multiple choice'
|
type_label = 'choice'
|
||||||
form_field_class = forms.ChoiceField
|
form_field_class = forms.ChoiceField
|
||||||
widget = widgets.Select
|
widget = widgets.Select
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
@ -515,12 +519,16 @@ class ChoiceField(WritableField):
|
||||||
'the available choices.'),
|
'the available choices.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, choices=(), *args, **kwargs):
|
def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
|
||||||
self.empty = kwargs.pop('empty', '')
|
self.empty = kwargs.pop('empty', '')
|
||||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
if not self.required:
|
if not self.required:
|
||||||
self.choices = BLANK_CHOICE_DASH + self.choices
|
if blank_display_value is None:
|
||||||
|
blank_choice = BLANK_CHOICE_DASH
|
||||||
|
else:
|
||||||
|
blank_choice = [('', blank_display_value)]
|
||||||
|
self.choices = blank_choice + self.choices
|
||||||
|
|
||||||
def _get_choices(self):
|
def _get_choices(self):
|
||||||
return self._choices
|
return self._choices
|
||||||
|
@ -1024,9 +1032,9 @@ class SerializerMethodField(Field):
|
||||||
A field that gets its value by calling a method on the serializer it's attached to.
|
A field that gets its value by calling a method on the serializer it's attached to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method_name):
|
def __init__(self, method_name, *args, **kwargs):
|
||||||
self.method_name = method_name
|
self.method_name = method_name
|
||||||
super(SerializerMethodField, self).__init__()
|
super(SerializerMethodField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def field_to_native(self, obj, field_name):
|
def field_to_native(self, obj, field_name):
|
||||||
value = getattr(self.parent, self.method_name)(obj)
|
value = getattr(self.parent, self.method_name)(obj)
|
||||||
|
|
|
@ -90,8 +90,8 @@ class GenericAPIView(views.APIView):
|
||||||
'view': self
|
'view': self
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_serializer(self, instance=None, data=None,
|
def get_serializer(self, instance=None, data=None, files=None, many=False,
|
||||||
files=None, many=False, partial=False):
|
partial=False, allow_add_remove=False):
|
||||||
"""
|
"""
|
||||||
Return the serializer instance that should be used for validating and
|
Return the serializer instance that should be used for validating and
|
||||||
deserializing input, and for serializing output.
|
deserializing input, and for serializing output.
|
||||||
|
@ -99,7 +99,9 @@ class GenericAPIView(views.APIView):
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
context = self.get_serializer_context()
|
context = self.get_serializer_context()
|
||||||
return serializer_class(instance, data=data, files=files,
|
return serializer_class(instance, data=data, files=files,
|
||||||
many=many, partial=partial, context=context)
|
many=many, partial=partial,
|
||||||
|
allow_add_remove=allow_add_remove,
|
||||||
|
context=context)
|
||||||
|
|
||||||
def get_pagination_serializer(self, page):
|
def get_pagination_serializer(self, page):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django.db import models
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework.compat import get_concrete_model, six
|
from rest_framework.compat import get_concrete_model, six
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
@ -33,8 +34,8 @@ from rest_framework.settings import api_settings
|
||||||
# This helps keep the separation between model fields, form fields, and
|
# This helps keep the separation between model fields, form fields, and
|
||||||
# serializer fields more explicit.
|
# serializer fields more explicit.
|
||||||
|
|
||||||
from rest_framework.relations import *
|
from rest_framework.relations import * # NOQA
|
||||||
from rest_framework.fields import *
|
from rest_framework.fields import * # NOQA
|
||||||
|
|
||||||
|
|
||||||
def _resolve_model(obj):
|
def _resolve_model(obj):
|
||||||
|
@ -49,7 +50,7 @@ def _resolve_model(obj):
|
||||||
String representations should have the format:
|
String representations should have the format:
|
||||||
'appname.ModelName'
|
'appname.ModelName'
|
||||||
"""
|
"""
|
||||||
if type(obj) == str and len(obj.split('.')) == 2:
|
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
|
||||||
app_name, model_name = obj.split('.')
|
app_name, model_name = obj.split('.')
|
||||||
return models.get_model(app_name, model_name)
|
return models.get_model(app_name, model_name)
|
||||||
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
elif inspect.isclass(obj) and issubclass(obj, models.Model):
|
||||||
|
@ -345,7 +346,7 @@ class BaseSerializer(WritableField):
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
if field.read_only and obj is None:
|
if field.read_only and obj is None:
|
||||||
continue
|
continue
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(field_name)
|
key = self.get_field_key(field_name)
|
||||||
value = field.field_to_native(obj, field_name)
|
value = field.field_to_native(obj, field_name)
|
||||||
|
@ -759,9 +760,9 @@ class ModelSerializer(Serializer):
|
||||||
field.read_only = True
|
field.read_only = True
|
||||||
|
|
||||||
ret[accessor_name] = field
|
ret[accessor_name] = field
|
||||||
|
|
||||||
# Ensure that 'read_only_fields' is an iterable
|
# 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'
|
assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
|
||||||
|
|
||||||
# Add the `read_only` flag to any fields that have been specified
|
# Add the `read_only` flag to any fields that have been specified
|
||||||
# in the `read_only_fields` option
|
# in the `read_only_fields` option
|
||||||
|
@ -776,10 +777,10 @@ class ModelSerializer(Serializer):
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].read_only = True
|
ret[field_name].read_only = True
|
||||||
|
|
||||||
# Ensure that 'write_only_fields' is an iterable
|
# 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'
|
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:
|
for field_name in self.opts.write_only_fields:
|
||||||
assert field_name not in self.base_fields.keys(), (
|
assert field_name not in self.base_fields.keys(), (
|
||||||
"field '%s' on serializer '%s' specified in "
|
"field '%s' on serializer '%s' specified in "
|
||||||
|
@ -790,7 +791,7 @@ class ModelSerializer(Serializer):
|
||||||
"Non-existant field '%s' specified in `write_only_fields` "
|
"Non-existant field '%s' specified in `write_only_fields` "
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].write_only = True
|
ret[field_name].write_only = True
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@ def optional_login(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ def optional_logout(request):
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
|
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
|
||||||
return snippet
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return ('', None)
|
return ('', content_type)
|
||||||
|
|
||||||
assert format is None or content_type is None, (
|
assert format is None or content_type is None, (
|
||||||
'You may not set both `format` and `content_type`.'
|
'You may not set both `format` and `content_type`.'
|
||||||
|
|
|
@ -105,6 +105,7 @@ class Album(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100, unique=True)
|
title = models.CharField(max_length=100, unique=True)
|
||||||
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
|
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class Photo(RESTFrameworkModel):
|
class Photo(RESTFrameworkModel):
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
album = models.ForeignKey(Album)
|
album = models.ForeignKey(Album)
|
||||||
|
@ -112,7 +113,8 @@ class Photo(RESTFrameworkModel):
|
||||||
|
|
||||||
# Model for issue #324
|
# Model for issue #324
|
||||||
class BlankFieldModel(RESTFrameworkModel):
|
class BlankFieldModel(RESTFrameworkModel):
|
||||||
title = models.CharField(max_length=100, blank=True, null=False)
|
title = models.CharField(max_length=100, blank=True, null=False,
|
||||||
|
default="title")
|
||||||
|
|
||||||
|
|
||||||
# Model for issue #380
|
# Model for issue #380
|
||||||
|
|
|
@ -717,6 +717,15 @@ class ChoiceFieldTests(TestCase):
|
||||||
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
|
||||||
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
|
||||||
|
|
||||||
|
def test_blank_choice_display(self):
|
||||||
|
blank = 'No Preference'
|
||||||
|
f = serializers.ChoiceField(
|
||||||
|
required=False,
|
||||||
|
choices=SAMPLE_CHOICES,
|
||||||
|
blank_display_value=blank,
|
||||||
|
)
|
||||||
|
self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
|
||||||
|
|
||||||
def test_invalid_choice_model(self):
|
def test_invalid_choice_model(self):
|
||||||
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
|
||||||
self.assertFalse(s.is_valid())
|
self.assertFalse(s.is_valid())
|
||||||
|
|
|
@ -1236,6 +1236,9 @@ class BlankFieldTests(TestCase):
|
||||||
def test_create_model_null_field(self):
|
def test_create_model_null_field(self):
|
||||||
serializer = self.model_serializer_class(data={'title': None})
|
serializer = self.model_serializer_class(data={'title': None})
|
||||||
self.assertEqual(serializer.is_valid(), True)
|
self.assertEqual(serializer.is_valid(), True)
|
||||||
|
serializer.save()
|
||||||
|
self.assertIsNot(serializer.object.pk, None)
|
||||||
|
self.assertEqual(serializer.object.title, '')
|
||||||
|
|
||||||
def test_create_not_blank_field(self):
|
def test_create_not_blank_field(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.serializers import _resolve_model
|
from rest_framework.serializers import _resolve_model
|
||||||
from rest_framework.tests.models import BasicModel
|
from rest_framework.tests.models import BasicModel
|
||||||
|
from rest_framework.compat import six
|
||||||
|
|
||||||
|
|
||||||
class ResolveModelTests(TestCase):
|
class ResolveModelTests(TestCase):
|
||||||
|
@ -19,6 +20,10 @@ class ResolveModelTests(TestCase):
|
||||||
resolved_model = _resolve_model('tests.BasicModel')
|
resolved_model = _resolve_model('tests.BasicModel')
|
||||||
self.assertEqual(resolved_model, BasicModel)
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
|
def test_resolve_unicode_representation(self):
|
||||||
|
resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
|
||||||
|
self.assertEqual(resolved_model, BasicModel)
|
||||||
|
|
||||||
def test_resolve_non_django_model(self):
|
def test_resolve_non_django_model(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
_resolve_model(TestCase)
|
_resolve_model(TestCase)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user