mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-01 19:10:12 +03:00
Merge remote-tracking branch 'samitnuk/issue/nullbooleanfield-with-choices' into test_null_boolean_field_choices
This commit is contained in:
commit
4f1fc62f6f
39
.travis.yml
39
.travis.yml
|
@ -1,26 +1,30 @@
|
||||||
language: python
|
language: python
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
python:
|
|
||||||
- "2.7"
|
|
||||||
- "3.4"
|
|
||||||
- "3.5"
|
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
env:
|
|
||||||
- DJANGO=1.11
|
|
||||||
- DJANGO=2.0
|
|
||||||
- DJANGO=2.1
|
|
||||||
- DJANGO=master
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- { python: "3.6", env: DJANGO=master }
|
- { python: "2.7", env: DJANGO=1.11 }
|
||||||
|
|
||||||
|
- { python: "3.4", env: DJANGO=1.11 }
|
||||||
|
- { python: "3.4", env: DJANGO=2.0 }
|
||||||
|
|
||||||
|
- { python: "3.5", env: DJANGO=1.11 }
|
||||||
|
- { python: "3.5", env: DJANGO=2.0 }
|
||||||
|
- { python: "3.5", env: DJANGO=2.1 }
|
||||||
|
- { python: "3.5", env: DJANGO=master }
|
||||||
|
|
||||||
- { python: "3.6", env: DJANGO=1.11 }
|
- { python: "3.6", env: DJANGO=1.11 }
|
||||||
- { python: "3.6", env: DJANGO=2.0 }
|
- { python: "3.6", env: DJANGO=2.0 }
|
||||||
- { python: "3.6", env: DJANGO=2.1 }
|
- { python: "3.6", env: DJANGO=2.1 }
|
||||||
|
- { python: "3.6", env: DJANGO=master }
|
||||||
|
|
||||||
|
- { python: "3.7", env: DJANGO=2.0, dist: xenial, sudo: true }
|
||||||
|
- { python: "3.7", env: DJANGO=2.1, dist: xenial, sudo: true }
|
||||||
|
- { python: "3.7", env: DJANGO=master, dist: xenial, sudo: true }
|
||||||
|
|
||||||
- { python: "3.6", env: TOXENV=base }
|
- { python: "3.6", env: TOXENV=base }
|
||||||
- { python: "2.7", env: TOXENV=lint }
|
- { python: "2.7", env: TOXENV=lint }
|
||||||
- { python: "2.7", env: TOXENV=docs }
|
- { python: "2.7", env: TOXENV=docs }
|
||||||
|
@ -29,22 +33,15 @@ matrix:
|
||||||
env: TOXENV=dist
|
env: TOXENV=dist
|
||||||
script:
|
script:
|
||||||
- python setup.py bdist_wheel
|
- python setup.py bdist_wheel
|
||||||
|
- rm -r djangorestframework.egg-info # see #6139
|
||||||
- tox --installpkg ./dist/djangorestframework-*.whl
|
- tox --installpkg ./dist/djangorestframework-*.whl
|
||||||
- tox # test sdist
|
- tox # test sdist
|
||||||
|
|
||||||
exclude:
|
|
||||||
- { python: "2.7", env: DJANGO=master }
|
|
||||||
- { python: "2.7", env: DJANGO=2.0 }
|
|
||||||
- { python: "2.7", env: DJANGO=2.1 }
|
|
||||||
- { python: "3.4", env: DJANGO=master }
|
|
||||||
- { python: "3.4", env: DJANGO=2.1 }
|
|
||||||
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: DJANGO=master
|
- env: DJANGO=master
|
||||||
- env: DJANGO=2.1
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install tox tox-travis
|
- pip install tox tox-venv tox-travis
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- tox
|
- tox
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
|
- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
|
||||||
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
|
- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
|
||||||
- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
|
- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
|
||||||
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](http://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
|
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/topics/third-party-resources/#about-third-party-packages) where possible.)
|
||||||
- [ ] I have reduced the issue to the simplest possible case.
|
- [ ] I have reduced the issue to the simplest possible case.
|
||||||
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
|
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Copyright © 2011-present, [Encode OSS Ltd](http://www.encode.io/).
|
Copyright © 2011-present, [Encode OSS Ltd](https://www.encode.io/).
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
64
README.md
64
README.md
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
**Awesome web-browsable Web APIs.**
|
**Awesome web-browsable Web APIs.**
|
||||||
|
|
||||||
Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
|
Full documentation for the project is available at [https://www.django-rest-framework.org/][docs].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -22,11 +22,13 @@ The initial aim is to provide a single full-time position on REST framework.
|
||||||
[![][rover-img]][rover-url]
|
[![][rover-img]][rover-url]
|
||||||
[![][sentry-img]][sentry-url]
|
[![][sentry-img]][sentry-url]
|
||||||
[![][stream-img]][stream-url]
|
[![][stream-img]][stream-url]
|
||||||
[![][machinalis-img]][machinalis-url]
|
|
||||||
[![][rollbar-img]][rollbar-url]
|
[![][rollbar-img]][rollbar-url]
|
||||||
[![][cadre-img]][cadre-url]
|
[![][cadre-img]][cadre-url]
|
||||||
|
[![][load-impact-img]][load-impact-url]
|
||||||
|
[![][kloudless-img]][kloudless-url]
|
||||||
|
[![][auklet-img]][auklet-url]
|
||||||
|
|
||||||
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Machinalis][machinalis-url], [Rollbar][rollbar-url], and [Cadre][cadre-url].
|
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Load Impact][load-impact-url], [Kloudless][kloudless-url], and [Auklet][auklet-url].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -52,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python (2.7, 3.4, 3.5, 3.6)
|
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||||
* Django (1.11, 2.0)
|
* Django (1.11, 2.0, 2.1)
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
@ -76,7 +78,7 @@ Startup up a new project like so...
|
||||||
|
|
||||||
pip install django
|
pip install django
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
django-admin.py startproject example .
|
django-admin startproject example .
|
||||||
./manage.py migrate
|
./manage.py migrate
|
||||||
./manage.py createsuperuser
|
./manage.py createsuperuser
|
||||||
|
|
||||||
|
@ -142,14 +144,14 @@ You can now open the API in your browser at `http://127.0.0.1:8000/`, and view y
|
||||||
You can also interact with the API using command line tools such as [`curl`](https://curl.haxx.se/). For example, to list the users endpoint:
|
You can also interact with the API using command line tools such as [`curl`](https://curl.haxx.se/). For example, to list the users endpoint:
|
||||||
|
|
||||||
$ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
|
$ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"url": "http://127.0.0.1:8000/users/1/",
|
"url": "http://127.0.0.1:8000/users/1/",
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"is_staff": true,
|
"is_staff": true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Or to create a new user:
|
Or to create a new user:
|
||||||
|
|
||||||
|
@ -163,7 +165,7 @@ Or to create a new user:
|
||||||
|
|
||||||
# Documentation & Support
|
# Documentation & Support
|
||||||
|
|
||||||
Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
|
Full documentation for the project is available at [https://www.django-rest-framework.org/][docs].
|
||||||
|
|
||||||
For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC.
|
For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC.
|
||||||
|
|
||||||
|
@ -191,28 +193,32 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
||||||
[rover-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rover-readme.png
|
[rover-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rover-readme.png
|
||||||
[sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png
|
[sentry-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/sentry-readme.png
|
||||||
[stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png
|
[stream-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png
|
||||||
[machinalis-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/machinalis-readme.png
|
|
||||||
[rollbar-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png
|
[rollbar-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png
|
||||||
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
|
[cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png
|
||||||
|
[load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png
|
||||||
|
[kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png
|
||||||
|
[auklet-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/auklet-readme.png
|
||||||
|
|
||||||
[rover-url]: http://jobs.rover.com/
|
[rover-url]: http://jobs.rover.com/
|
||||||
[sentry-url]: https://getsentry.com/welcome/
|
[sentry-url]: https://getsentry.com/welcome/
|
||||||
[stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf
|
[stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf
|
||||||
[machinalis-url]: https://hello.machinalis.co.uk/
|
|
||||||
[rollbar-url]: https://rollbar.com/
|
[rollbar-url]: https://rollbar.com/
|
||||||
[cadre-url]: https://cadre.com/
|
[cadre-url]: https://cadre.com/
|
||||||
|
[load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf
|
||||||
|
[kloudless-url]: https://hubs.ly/H0f30Lf0
|
||||||
|
[auklet-url]: https://auklet.io/
|
||||||
|
|
||||||
[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
|
[oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
|
||||||
[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
|
[oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
|
||||||
[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#serializers
|
[serializer-section]: https://www.django-rest-framework.org/api-guide/serializers/#serializers
|
||||||
[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#modelserializer
|
[modelserializer-section]: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
|
||||||
[functionview-section]: http://www.django-rest-framework.org/api-guide/views/#function-based-views
|
[functionview-section]: https://www.django-rest-framework.org/api-guide/views/#function-based-views
|
||||||
[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views/
|
[generic-views]: https://www.django-rest-framework.org/api-guide/generic-views/
|
||||||
[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets/
|
[viewsets]: https://www.django-rest-framework.org/api-guide/viewsets/
|
||||||
[routers]: http://www.django-rest-framework.org/api-guide/routers/
|
[routers]: https://www.django-rest-framework.org/api-guide/routers/
|
||||||
[serializers]: http://www.django-rest-framework.org/api-guide/serializers/
|
[serializers]: https://www.django-rest-framework.org/api-guide/serializers/
|
||||||
[authentication]: http://www.django-rest-framework.org/api-guide/authentication/
|
[authentication]: https://www.django-rest-framework.org/api-guide/authentication/
|
||||||
[image]: http://www.django-rest-framework.org/img/quickstart.png
|
[image]: https://www.django-rest-framework.org/img/quickstart.png
|
||||||
|
|
||||||
[docs]: http://www.django-rest-framework.org/
|
[docs]: https://www.django-rest-framework.org/
|
||||||
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
[security-mail]: mailto:rest-framework-security@googlegroups.com
|
||||||
|
|
15
codecov.yml
15
codecov.yml
|
@ -1,8 +1,11 @@
|
||||||
coverage:
|
coverage:
|
||||||
status:
|
precision: 2
|
||||||
project: false
|
round: down
|
||||||
patch: true
|
range: "80...100"
|
||||||
changes: true
|
|
||||||
|
|
||||||
comment:
|
status:
|
||||||
layout: "diff"
|
project: yes
|
||||||
|
patch: no
|
||||||
|
changes: no
|
||||||
|
|
||||||
|
comment: off
|
||||||
|
|
|
@ -137,7 +137,7 @@ You'll also need to create tokens for your users.
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
token = Token.objects.create(user=...)
|
token = Token.objects.create(user=...)
|
||||||
print token.key
|
print(token.key)
|
||||||
|
|
||||||
For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example:
|
For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example:
|
||||||
|
|
||||||
|
@ -397,7 +397,7 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
|
||||||
|
|
||||||
## JSON Web Token Authentication
|
## JSON Web Token Authentication
|
||||||
|
|
||||||
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password. An alternative package for JWT authentication is [djangorestframework-simplejwt][djangorestframework-simplejwt] which provides different features as well as a pluggable token blacklist app.
|
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. A package for JWT authentication is [djangorestframework-simplejwt][djangorestframework-simplejwt] which provides some features as well as a pluggable token blacklist app.
|
||||||
|
|
||||||
## Hawk HTTP Authentication
|
## Hawk HTTP Authentication
|
||||||
|
|
||||||
|
@ -445,8 +445,6 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
|
||||||
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
|
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
|
||||||
[evonove]: https://github.com/evonove/
|
[evonove]: https://github.com/evonove/
|
||||||
[oauthlib]: https://github.com/idan/oauthlib
|
[oauthlib]: https://github.com/idan/oauthlib
|
||||||
[blimp]: https://github.com/GetBlimp
|
|
||||||
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
|
|
||||||
[djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt
|
[djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt
|
||||||
[etoccalino]: https://github.com/etoccalino/
|
[etoccalino]: https://github.com/etoccalino/
|
||||||
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
|
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
|
||||||
|
|
|
@ -285,6 +285,12 @@ The `ordering` attribute may be either a string or a list/tuple of strings.
|
||||||
|
|
||||||
The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission.
|
The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** This filter has been deprecated as of version 3.9 and moved to the 3rd-party [`djangorestframework-guardian` package][django-rest-framework-guardian].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute.
|
If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute.
|
||||||
|
|
||||||
A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this.
|
A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this.
|
||||||
|
@ -388,6 +394,7 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
|
||||||
[view-permissions-blogpost]: https://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
[view-permissions-blogpost]: https://blog.nyaruka.com/adding-a-view-permission-to-django-models
|
||||||
[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
|
||||||
[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
|
[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
|
||||||
|
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||||
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
|
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
|
||||||
[django-url-filter]: https://github.com/miki725/django-url-filter
|
[django-url-filter]: https://github.com/miki725/django-url-filter
|
||||||
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters
|
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters
|
||||||
|
|
|
@ -90,4 +90,4 @@ It is actually a misconception. For example, take the following quote from Roy
|
||||||
The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern.
|
The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern.
|
||||||
|
|
||||||
[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857
|
[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857
|
||||||
[cite2]: http://tech.groups.yahoo.com/group/rest-discuss/message/14844
|
[cite2]: https://groups.yahoo.com/neo/groups/rest-discuss/conversations/topics/14844
|
||||||
|
|
|
@ -117,5 +117,5 @@ If you wish to do so, it also provides an exporter that can export those schema
|
||||||
|
|
||||||
[cite]: https://tools.ietf.org/html/rfc7231#section-4.3.7
|
[cite]: https://tools.ietf.org/html/rfc7231#section-4.3.7
|
||||||
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
||||||
[json-schema]: http://json-schema.org/
|
[json-schema]: https://json-schema.org/
|
||||||
[drf-schema-adapter]: https://github.com/drf-forms/drf-schema-adapter
|
[drf-schema-adapter]: https://github.com/drf-forms/drf-schema-adapter
|
||||||
|
|
|
@ -46,7 +46,7 @@ If you want to modify particular aspects of the pagination style, you'll want to
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
max_page_size = 1000
|
max_page_size = 1000
|
||||||
|
|
||||||
You can then apply your new style to a view using the `.pagination_class` attribute:
|
You can then apply your new style to a view using the `pagination_class` attribute:
|
||||||
|
|
||||||
class BillingRecordsView(generics.ListAPIView):
|
class BillingRecordsView(generics.ListAPIView):
|
||||||
queryset = Billing.objects.all()
|
queryset = Billing.objects.all()
|
||||||
|
@ -319,5 +319,5 @@ The [`django-rest-framework-link-header-pagination` package][drf-link-header-pag
|
||||||
[paginate-by-max-mixin]: https://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
[paginate-by-max-mixin]: https://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
|
||||||
[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination
|
[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination
|
||||||
[drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination
|
[drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination
|
||||||
[disqus-cursor-api]: http://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
|
[disqus-cursor-api]: https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
|
||||||
[float_cursor_pagination_example]: https://gist.github.com/keturn/8bc88525a183fd41c73ffb729b8865be#file-fpcursorpagination-py
|
[float_cursor_pagination_example]: https://gist.github.com/keturn/8bc88525a183fd41c73ffb729b8865be#file-fpcursorpagination-py
|
||||||
|
|
|
@ -102,6 +102,27 @@ Or, if you're using the `@api_view` decorator with function based views.
|
||||||
|
|
||||||
__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file.
|
__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file.
|
||||||
|
|
||||||
|
Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written:
|
||||||
|
|
||||||
|
from rest_framework.permissions import BasePermission, IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
class ReadOnly(BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return request.method in SAFE_METHODS
|
||||||
|
|
||||||
|
class ExampleView(APIView):
|
||||||
|
permission_classes = (IsAuthenticated|ReadOnly)
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
content = {
|
||||||
|
'status': 'request was permitted'
|
||||||
|
}
|
||||||
|
return Response(content)
|
||||||
|
|
||||||
|
__Note:__ it only supports & -and- and | -or-.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# API Reference
|
# API Reference
|
||||||
|
@ -168,9 +189,7 @@ As with `DjangoModelPermissions` you can use custom model permissions by overrid
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests, you'll want to consider also adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions.
|
**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests and are using django-guardian for your object-level permissions backend, you'll want to consider using the `DjangoObjectPermissionsFilter` class provided by the [`djangorestframework-guardian` package][django-rest-framework-guardian]. It ensures that list endpoints only return results including objects for which the user has appropriate view permissions.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -287,3 +306,4 @@ The [Django Rest Framework Role Filters][django-rest-framework-role-filters] pac
|
||||||
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
[django-rest-framework-roles]: https://github.com/computer-lab/django-rest-framework-roles
|
||||||
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key
|
[django-rest-framework-api-key]: https://github.com/manosim/django-rest-framework-api-key
|
||||||
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
|
[django-rest-framework-role-filters]: https://github.com/allisson/django-rest-framework-role-filters
|
||||||
|
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||||
|
|
|
@ -594,7 +594,7 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re
|
||||||
|
|
||||||
[cite]: https://lwn.net/Articles/193245/
|
[cite]: https://lwn.net/Articles/193245/
|
||||||
[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward
|
[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward
|
||||||
[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
|
[routers]: https://www.django-rest-framework.org/api-guide/routers#defaultrouter
|
||||||
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
|
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
|
||||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations
|
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations
|
||||||
|
|
|
@ -457,6 +457,43 @@ Modify your REST framework settings.
|
||||||
|
|
||||||
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
|
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
|
||||||
|
|
||||||
|
## XLSX (Binary Spreadsheet Endpoints)
|
||||||
|
|
||||||
|
XLSX is the world's most popular binary spreadsheet format. [Tim Allen][flipperpa] of [The Wharton School][wharton] maintains [drf-renderer-xlsx][drf-renderer-xlsx], which renders an endpoint as an XLSX spreadsheet using OpenPyXL, and allows the client to download it. Spreadsheets can be styled on a per-view basis.
|
||||||
|
|
||||||
|
#### Installation & configuration
|
||||||
|
|
||||||
|
Install using pip.
|
||||||
|
|
||||||
|
$ pip install drf-renderer-xlsx
|
||||||
|
|
||||||
|
Modify your REST framework settings.
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
...
|
||||||
|
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||||
|
'drf_renderer_xlsx.renderers.XLSXRenderer',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
To avoid having a file streamed without a filename (which the browser will often default to the filename "download", with no extension), we need to use a mixin to override the `Content-Disposition` header. If no filename is provided, it will default to `export.xlsx`. For example:
|
||||||
|
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
from drf_renderer_xlsx.mixins import XLSXFileMixin
|
||||||
|
from drf_renderer_xlsx.renderers import XLSXRenderer
|
||||||
|
|
||||||
|
from .models import MyExampleModel
|
||||||
|
from .serializers import MyExampleSerializer
|
||||||
|
|
||||||
|
class MyExampleViewSet(XLSXFileMixin, ReadOnlyModelViewSet):
|
||||||
|
queryset = MyExampleModel.objects.all()
|
||||||
|
serializer_class = MyExampleSerializer
|
||||||
|
renderer_classes = (XLSXRenderer,)
|
||||||
|
filename = 'my_export.xlsx'
|
||||||
|
|
||||||
## CSV
|
## CSV
|
||||||
|
|
||||||
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
|
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
|
||||||
|
@ -484,19 +521,22 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
[testing]: testing.md
|
[testing]: testing.md
|
||||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[quote]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[application/vnd.github+json]: https://developer.github.com/v3/media/
|
[application/vnd.github+json]: https://developer.github.com/v3/media/
|
||||||
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
||||||
[django-error-views]: https://docs.djangoproject.com/en/stable/topics/http/views/#customizing-error-views
|
[django-error-views]: https://docs.djangoproject.com/en/stable/topics/http/views/#customizing-error-views
|
||||||
[rest-framework-jsonp]: https://jpadilla.github.io/django-rest-framework-jsonp/
|
[rest-framework-jsonp]: https://jpadilla.github.io/django-rest-framework-jsonp/
|
||||||
[cors]: https://www.w3.org/TR/cors/
|
[cors]: https://www.w3.org/TR/cors/
|
||||||
[cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/
|
[cors-docs]: https://www.django-rest-framework.org/topics/ajax-csrf-cors/
|
||||||
[jsonp-security]: https://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
|
[jsonp-security]: https://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
|
||||||
[rest-framework-yaml]: https://jpadilla.github.io/django-rest-framework-yaml/
|
[rest-framework-yaml]: https://jpadilla.github.io/django-rest-framework-yaml/
|
||||||
[rest-framework-xml]: https://jpadilla.github.io/django-rest-framework-xml/
|
[rest-framework-xml]: https://jpadilla.github.io/django-rest-framework-xml/
|
||||||
[messagepack]: https://msgpack.org/
|
[messagepack]: https://msgpack.org/
|
||||||
[juanriaza]: https://github.com/juanriaza
|
[juanriaza]: https://github.com/juanriaza
|
||||||
[mjumbewu]: https://github.com/mjumbewu
|
[mjumbewu]: https://github.com/mjumbewu
|
||||||
|
[flipperpa]: https://githuc.com/flipperpa
|
||||||
|
[wharton]: https://github.com/wharton
|
||||||
|
[drf-renderer-xlsx]: https://github.com/wharton/drf-renderer-xlsx
|
||||||
[vbabiy]: https://github.com/vbabiy
|
[vbabiy]: https://github.com/vbabiy
|
||||||
[rest-framework-yaml]: https://jpadilla.github.io/django-rest-framework-yaml/
|
[rest-framework-yaml]: https://jpadilla.github.io/django-rest-framework-yaml/
|
||||||
[rest-framework-xml]: https://jpadilla.github.io/django-rest-framework-xml/
|
[rest-framework-xml]: https://jpadilla.github.io/django-rest-framework-xml/
|
||||||
|
|
|
@ -90,7 +90,7 @@ You won't typically need to access this property.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note:** You may see a `WrappedAttributeError` raised when calling the `.user` or `.auth` properties. These errors originate from an authenticator as a standard `AttributeError`, however it's necessary that they be re-raised as a different exception type in order to prevent them from being suppressed by the outer property access. Python will not recognize that the `AttributeError` orginates from the authenticator and will instaed assume that the request object does not have a `.user` or `.auth` property. The authenticator will need to be fixed.
|
**Note:** You may see a `WrappedAttributeError` raised when calling the `.user` or `.auth` properties. These errors originate from an authenticator as a standard `AttributeError`, however it's necessary that they be re-raised as a different exception type in order to prevent them from being suppressed by the outer property access. Python will not recognize that the `AttributeError` orginates from the authenticator and will instead assume that the request object does not have a `.user` or `.auth` property. The authenticator will need to be fixed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -325,7 +325,7 @@ The [wq.db package][wq.db] provides an advanced [ModelRouter][wq.db-router] clas
|
||||||
|
|
||||||
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].
|
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]: https://guides.rubyonrails.org/routing.html
|
||||||
[route-decorators]: viewsets.md#marking-extra-actions-for-routing
|
[route-decorators]: viewsets.md#marking-extra-actions-for-routing
|
||||||
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
|
||||||
[wq.db]: https://wq.io/wq.db
|
[wq.db]: https://wq.io/wq.db
|
||||||
|
|
|
@ -10,12 +10,50 @@ API schemas are a useful tool that allow for a range of use cases, including
|
||||||
generating reference documentation, or driving dynamic client libraries that
|
generating reference documentation, or driving dynamic client libraries that
|
||||||
can interact with your API.
|
can interact with your API.
|
||||||
|
|
||||||
## Install Core API
|
## Install Core API & PyYAML
|
||||||
|
|
||||||
You'll need to install the `coreapi` package in order to add schema support
|
You'll need to install the `coreapi` package in order to add schema support
|
||||||
for REST framework.
|
for REST framework. You probably also want to install `pyyaml`, so that you
|
||||||
|
can render the schema into the commonly used YAML-based OpenAPI format.
|
||||||
|
|
||||||
pip install coreapi
|
pip install coreapi pyyaml
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
There are two different ways you can serve a schema description for you API.
|
||||||
|
|
||||||
|
### Generating a schema with the `generateschema` management command
|
||||||
|
|
||||||
|
To generate a static API schema, use the `generateschema` management command.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ python manage.py generateschema > schema.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you've generated a schema in this way you can annotate it with any
|
||||||
|
additional information that cannot be automatically inferred by the schema
|
||||||
|
generator.
|
||||||
|
|
||||||
|
You might want to check your API schema into version control and update it
|
||||||
|
with each new release, or serve the API schema from your site's static media.
|
||||||
|
|
||||||
|
### Adding a view with `get_schema_view`
|
||||||
|
|
||||||
|
To add a dynamically generated schema view to your API, use `get_schema_view`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
|
schema_view = get_schema_view(title="Example API")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url('^schema$', schema_view),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
See below [for more details](#the-get_schema_view-shortcut) on customizing a
|
||||||
|
dynamically generated schema view.
|
||||||
|
|
||||||
## Internal schema representation
|
## Internal schema representation
|
||||||
|
|
||||||
|
@ -71,38 +109,19 @@ endpoint:
|
||||||
In order to be presented in an HTTP response, the internal representation
|
In order to be presented in an HTTP response, the internal representation
|
||||||
has to be rendered into the actual bytes that are used in the response.
|
has to be rendered into the actual bytes that are used in the response.
|
||||||
|
|
||||||
|
REST framework includes a few different renderers that you can use for
|
||||||
|
encoding the API schema.
|
||||||
|
|
||||||
|
* `renderers.OpenAPIRenderer` - Renders into YAML-based [OpenAPI][openapi], the most widely used API schema format.
|
||||||
|
* `renderers.JSONOpenAPIRenderer` - Renders into JSON-based [OpenAPI][openapi].
|
||||||
|
* `renderers.CoreJSONRenderer` - Renders into [Core JSON][corejson], a format designed for
|
||||||
|
use with the `coreapi` client library.
|
||||||
|
|
||||||
|
|
||||||
[Core JSON][corejson] is designed as a canonical format for use with Core API.
|
[Core JSON][corejson] is designed as a canonical format for use with Core API.
|
||||||
REST framework includes a renderer class for handling this media type, which
|
REST framework includes a renderer class for handling this media type, which
|
||||||
is available as `renderers.CoreJSONRenderer`.
|
is available as `renderers.CoreJSONRenderer`.
|
||||||
|
|
||||||
### Alternate schema formats
|
|
||||||
|
|
||||||
Other schema formats such as [Open API][open-api] ("Swagger"),
|
|
||||||
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can also
|
|
||||||
be supported by implementing a custom renderer class that handles converting a
|
|
||||||
`Document` instance into a bytestring representation.
|
|
||||||
|
|
||||||
If there is a Core API codec package that supports encoding into the format you
|
|
||||||
want to use then implementing the renderer class can be done by using the codec.
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
For example, the `openapi_codec` package provides support for encoding or decoding
|
|
||||||
to the Open API ("Swagger") format:
|
|
||||||
|
|
||||||
from rest_framework import renderers
|
|
||||||
from openapi_codec import OpenAPICodec
|
|
||||||
|
|
||||||
class SwaggerRenderer(renderers.BaseRenderer):
|
|
||||||
media_type = 'application/openapi+json'
|
|
||||||
format = 'swagger'
|
|
||||||
|
|
||||||
def render(self, data, media_type=None, renderer_context=None):
|
|
||||||
codec = OpenAPICodec()
|
|
||||||
return codec.dump(data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Schemas vs Hypermedia
|
## Schemas vs Hypermedia
|
||||||
|
|
||||||
|
@ -146,7 +165,7 @@ example above.
|
||||||
|
|
||||||
Automatic schema generation is provided by the `SchemaGenerator` class.
|
Automatic schema generation is provided by the `SchemaGenerator` class.
|
||||||
|
|
||||||
`SchemaGenerator` processes a list of routed URL pattterns and compiles the
|
`SchemaGenerator` processes a list of routed URL patterns and compiles the
|
||||||
appropriately structured Core API Document.
|
appropriately structured Core API Document.
|
||||||
|
|
||||||
Basic usage is just to provide the title for your schema and call
|
Basic usage is just to provide the title for your schema and call
|
||||||
|
@ -325,13 +344,12 @@ ROOT_URLCONF setting.
|
||||||
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
|
||||||
|
|
||||||
from rest_framework.schemas import get_schema_view
|
from rest_framework.schemas import get_schema_view
|
||||||
from rest_framework.renderers import CoreJSONRenderer
|
from rest_framework.renderers import JSONOpenAPIRenderer
|
||||||
from my_custom_package import APIBlueprintRenderer
|
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
title='Server Monitoring API',
|
title='Server Monitoring API',
|
||||||
url='https://www.example.org/api/',
|
url='https://www.example.org/api/',
|
||||||
renderer_classes=[CoreJSONRenderer, APIBlueprintRenderer]
|
renderer_classes=[JSONOpenAPIRenderer]
|
||||||
)
|
)
|
||||||
|
|
||||||
#### `patterns`
|
#### `patterns`
|
||||||
|
@ -364,7 +382,6 @@ Defaults to `settings.DEFAULT_AUTHENTICATION_CLASSES`
|
||||||
May be used to specify the list of permission classes that will apply to the schema endpoint.
|
May be used to specify the list of permission classes that will apply to the schema endpoint.
|
||||||
Defaults to `settings.DEFAULT_PERMISSION_CLASSES`
|
Defaults to `settings.DEFAULT_PERMISSION_CLASSES`
|
||||||
|
|
||||||
|
|
||||||
## Using an explicit schema view
|
## Using an explicit schema view
|
||||||
|
|
||||||
If you need a little more control than the `get_schema_view()` shortcut gives you,
|
If you need a little more control than the `get_schema_view()` shortcut gives you,
|
||||||
|
@ -386,7 +403,7 @@ return the schema.
|
||||||
generator = schemas.SchemaGenerator(title='Bookings API')
|
generator = schemas.SchemaGenerator(title='Bookings API')
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
@renderer_classes([renderers.CoreJSONRenderer])
|
@renderer_classes([renderers.OpenAPIRenderer])
|
||||||
def schema_view(request):
|
def schema_view(request):
|
||||||
schema = generator.get_schema(request)
|
schema = generator.get_schema(request)
|
||||||
return response.Response(schema)
|
return response.Response(schema)
|
||||||
|
@ -408,7 +425,7 @@ In order to present a schema with endpoints filtered by user permissions,
|
||||||
you need to pass the `request` argument to the `get_schema()` method, like so:
|
you need to pass the `request` argument to the `get_schema()` method, like so:
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
@renderer_classes([renderers.CoreJSONRenderer])
|
@renderer_classes([renderers.OpenAPIRenderer])
|
||||||
def schema_view(request):
|
def schema_view(request):
|
||||||
generator = schemas.SchemaGenerator(title='Bookings API')
|
generator = schemas.SchemaGenerator(title='Bookings API')
|
||||||
return response.Response(generator.get_schema(request=request))
|
return response.Response(generator.get_schema(request=request))
|
||||||
|
@ -432,21 +449,10 @@ representation.
|
||||||
)
|
)
|
||||||
|
|
||||||
@api_view()
|
@api_view()
|
||||||
@renderer_classes([renderers.CoreJSONRenderer])
|
@renderer_classes([renderers.OpenAPIRenderer])
|
||||||
def schema_view(request):
|
def schema_view(request):
|
||||||
return response.Response(schema)
|
return response.Response(schema)
|
||||||
|
|
||||||
## Static schema file
|
|
||||||
|
|
||||||
A final option is to write your API schema as a static file, using one
|
|
||||||
of the available formats, such as Core JSON or Open API.
|
|
||||||
|
|
||||||
You could then either:
|
|
||||||
|
|
||||||
* Write a schema definition as a static file, and [serve the static file directly][static-files].
|
|
||||||
* Write a schema definition that is loaded using `Core API`, and then
|
|
||||||
rendered to one of many available formats, depending on the client request.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Schemas as documentation
|
# Schemas as documentation
|
||||||
|
@ -535,7 +541,7 @@ Arguments:
|
||||||
Returns a `coreapi.Document` instance that represents the API schema.
|
Returns a `coreapi.Document` instance that represents the API schema.
|
||||||
|
|
||||||
@api_view
|
@api_view
|
||||||
@renderer_classes([renderers.CoreJSONRenderer])
|
@renderer_classes([renderers.OpenAPIRenderer])
|
||||||
def schema_view(request):
|
def schema_view(request):
|
||||||
generator = schemas.SchemaGenerator(title='Bookings API')
|
generator = schemas.SchemaGenerator(title='Bookings API')
|
||||||
return Response(generator.get_schema())
|
return Response(generator.get_schema())
|
||||||
|
@ -818,7 +824,7 @@ A short description of the meaning and intended usage of the input field.
|
||||||
|
|
||||||
## drf-yasg - Yet Another Swagger Generator
|
## drf-yasg - Yet Another Swagger Generator
|
||||||
|
|
||||||
[drf-yasg][drf-yasg] generates [OpenAPI][open-api] documents suitable for code generation - nested schemas,
|
[drf-yasg][drf-yasg] generates [OpenAPI][open-api] documents suitable for code generation - nested schemas,
|
||||||
named models, response bodies, enum/pattern/min/max validators, form parameters, etc.
|
named models, response bodies, enum/pattern/min/max validators, form parameters, etc.
|
||||||
|
|
||||||
|
|
||||||
|
@ -829,12 +835,12 @@ in [OpenAPI][open-api] format.
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api
|
[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api
|
||||||
[coreapi]: http://www.coreapi.org/
|
[coreapi]: https://www.coreapi.org/
|
||||||
[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
[corejson]: https://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||||
[open-api]: https://openapis.org/
|
[open-api]: https://openapis.org/
|
||||||
[drf-openapi]: https://github.com/limdauto/drf_openapi
|
[drf-openapi]: https://github.com/limdauto/drf_openapi
|
||||||
[json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html
|
[json-hyperschema]: https://json-schema.org/latest/json-schema-hypermedia.html
|
||||||
[api-blueprint]: https://apiblueprint.org/
|
[api-blueprint]: https://apiblueprint.org/
|
||||||
[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/
|
[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/
|
||||||
[named-arguments]: https://docs.djangoproject.com/en/stable/topics/http/urls/#named-groups
|
[named-arguments]: https://docs.djangoproject.com/en/stable/topics/http/urls/#named-groups
|
||||||
|
|
|
@ -57,10 +57,10 @@ At this point we've translated the model instance into Python native datatypes.
|
||||||
|
|
||||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
|
||||||
from django.utils.six import BytesIO
|
import io
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
|
|
||||||
stream = BytesIO(json)
|
stream = io.BytesIO(json)
|
||||||
data = JSONParser().parse(stream)
|
data = JSONParser().parse(stream)
|
||||||
|
|
||||||
...then we restore those native datatypes into a dictionary of validated data.
|
...then we restore those native datatypes into a dictionary of validated data.
|
||||||
|
@ -1030,7 +1030,7 @@ Similar to Django forms, you can extend and reuse serializers through inheritanc
|
||||||
class MyBaseSerializer(Serializer):
|
class MyBaseSerializer(Serializer):
|
||||||
my_field = serializers.CharField()
|
my_field = serializers.CharField()
|
||||||
|
|
||||||
def validate_my_field(self):
|
def validate_my_field(self, value):
|
||||||
...
|
...
|
||||||
|
|
||||||
class MySerializer(MyBaseSerializer):
|
class MySerializer(MyBaseSerializer):
|
||||||
|
|
|
@ -119,7 +119,7 @@ Extends [Django's existing `Client` class][client].
|
||||||
|
|
||||||
## Making requests
|
## Making requests
|
||||||
|
|
||||||
The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
|
The `APIClient` class supports the same request interface as Django's standard `Client` class. This means that the standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
|
||||||
|
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
|
|
@ -275,7 +275,7 @@ A validator may be any callable that raises a `serializers.ValidationError` on f
|
||||||
|
|
||||||
You can specify custom field-level validation by adding `.validate_<field_name>` methods
|
You can specify custom field-level validation by adding `.validate_<field_name>` methods
|
||||||
to your `Serializer` subclass. This is documented in the
|
to your `Serializer` subclass. This is documented in the
|
||||||
[Serializer docs](http://www.django-rest-framework.org/api-guide/serializers/#field-level-validation)
|
[Serializer docs](https://www.django-rest-framework.org/api-guide/serializers/#field-level-validation)
|
||||||
|
|
||||||
## Class-based
|
## Class-based
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,7 @@ You may inspect these attributes to adjust behaviour based on the current action
|
||||||
|
|
||||||
## Marking extra actions for routing
|
## Marking extra actions for routing
|
||||||
|
|
||||||
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a list of objects, or a single instance. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
|
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
|
||||||
|
|
||||||
A more complete example of extra actions:
|
A more complete example of extra actions:
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ A more complete example of extra actions:
|
||||||
|
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def recent_users(self, request):
|
def recent_users(self, request):
|
||||||
recent_users = User.objects.all().order('-last_login')
|
recent_users = User.objects.all().order_by('-last_login')
|
||||||
|
|
||||||
page = self.paginate_queryset(recent_users)
|
page = self.paginate_queryset(recent_users)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
@ -174,7 +174,7 @@ The decorator can additionally take extra arguments that will be set for the rou
|
||||||
def set_password(self, request, pk=None):
|
def set_password(self, request, pk=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
These decorator will route `GET` requests by default, but may also accept other HTTP methods by setting the `methods` argument. For example:
|
The `action` decorator will route `GET` requests by default, but may also accept other HTTP methods by setting the `methods` argument. For example:
|
||||||
|
|
||||||
@action(detail=True, methods=['post', 'delete'])
|
@action(detail=True, methods=['post', 'delete'])
|
||||||
def unset_password(self, request, pk=None):
|
def unset_password(self, request, pk=None):
|
||||||
|
@ -186,7 +186,7 @@ To view all extra actions, call the `.get_extra_actions()` method.
|
||||||
|
|
||||||
### Routing additional HTTP methods for extra actions
|
### Routing additional HTTP methods for extra actions
|
||||||
|
|
||||||
Extra actions can be mapped to different `ViewSet` methods. For example, the above password set/unset methods could be consolidated into a single route. Note that additional mappings do not accept arguments.
|
Extra actions can map additional HTTP methods to separate `ViewSet` methods. For example, the above password set/unset methods could be consolidated into a single route. Note that additional mappings do not accept arguments.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@action(detail=True, methods=['put'], name='Change Password')
|
@action(detail=True, methods=['put'], name='Change Password')
|
||||||
|
@ -314,5 +314,5 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
|
||||||
|
|
||||||
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
|
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
|
||||||
|
|
||||||
[cite]: http://guides.rubyonrails.org/routing.html
|
[cite]: https://guides.rubyonrails.org/routing.html
|
||||||
[routers]: routers.md
|
[routers]: routers.md
|
||||||
|
|
|
@ -960,6 +960,6 @@ The 3.2 release is planned to introduce an alternative admin-style interface to
|
||||||
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/encode/django-rest-framework/milestones).
|
You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/encode/django-rest-framework/milestones).
|
||||||
|
|
||||||
[kickstarter]: https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3
|
[kickstarter]: https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3
|
||||||
[sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
|
[sponsors]: https://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
|
||||||
[mixins.py]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py
|
[mixins.py]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py
|
||||||
[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files
|
[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files
|
||||||
|
|
|
@ -10,7 +10,7 @@ We've also fixed a huge number of issues, and made numerous cleanups and improve
|
||||||
|
|
||||||
Over the course of the 3.1.x series we've [resolved nearly 600 tickets](https://github.com/encode/django-rest-framework/issues?utf8=%E2%9C%93&q=closed%3A%3E2015-03-05) on our GitHub issue tracker. This means we're currently running at a rate of **closing around 100 issues or pull requests per month**.
|
Over the course of the 3.1.x series we've [resolved nearly 600 tickets](https://github.com/encode/django-rest-framework/issues?utf8=%E2%9C%93&q=closed%3A%3E2015-03-05) on our GitHub issue tracker. This means we're currently running at a rate of **closing around 100 issues or pull requests per month**.
|
||||||
|
|
||||||
None of this would have been possible without the support of our wonderful Kickstarter backers. If you're looking for a job in Django development we'd strongly recommend taking [a look through our sponsors](http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors) and finding out who's hiring.
|
None of this would have been possible without the support of our wonderful Kickstarter backers. If you're looking for a job in Django development we'd strongly recommend taking [a look through our sponsors](https://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors) and finding out who's hiring.
|
||||||
|
|
||||||
## AdminRenderer
|
## AdminRenderer
|
||||||
|
|
||||||
|
|
|
@ -178,12 +178,12 @@ The full set of itemized release notes [are available here][release-notes].
|
||||||
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
||||||
[moss]: mozilla-grant.md
|
[moss]: mozilla-grant.md
|
||||||
[funding]: funding.md
|
[funding]: funding.md
|
||||||
[core-api]: http://www.coreapi.org/
|
[core-api]: https://www.coreapi.org/
|
||||||
[command-line-client]: api-clients#command-line-client
|
[command-line-client]: api-clients#command-line-client
|
||||||
[client-library]: api-clients#python-client-library
|
[client-library]: api-clients#python-client-library
|
||||||
[core-json]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
[core-json]: https://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||||
[swagger]: https://openapis.org/specification
|
[swagger]: https://openapis.org/specification
|
||||||
[hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html
|
[hyperschema]: https://json-schema.org/latest/json-schema-hypermedia.html
|
||||||
[api-blueprint]: https://apiblueprint.org/
|
[api-blueprint]: https://apiblueprint.org/
|
||||||
[tut-7]: ../tutorial/7-schemas-and-client-libraries/
|
[tut-7]: ../tutorial/7-schemas-and-client-libraries/
|
||||||
[schema-generation]: ../api-guide/schemas/
|
[schema-generation]: ../api-guide/schemas/
|
||||||
|
|
212
docs/community/3.9-announcement.md
Normal file
212
docs/community/3.9-announcement.md
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
<style>
|
||||||
|
.promo li a {
|
||||||
|
float: left;
|
||||||
|
width: 130px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 30px;
|
||||||
|
padding: 150px 0 0 0;
|
||||||
|
background-position: 0 50%;
|
||||||
|
background-size: 130px auto;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
font-size: 120%;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.promo li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
# Django REST framework 3.9
|
||||||
|
|
||||||
|
The 3.9 release gives access to _extra actions_ in the Browsable API, introduces composable permissions and built-in [OpenAPI][openapi] schema support. (Formerly known as Swagger)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funding
|
||||||
|
|
||||||
|
If you use REST framework commercially and would like to see this work continue, we strongly encourage you to invest in its continued development by
|
||||||
|
**[signing up for a paid plan][funding]**.
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="premium-promo promo">
|
||||||
|
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
||||||
|
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||||
|
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||||
|
<li><a href="https://auklet.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/auklet-new.png)">Auklet</a></li>
|
||||||
|
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
||||||
|
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
||||||
|
<li><a href="https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/load-impact.png)">Load Impact</a></li>
|
||||||
|
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless.png)">Kloudless</a></li>
|
||||||
|
</ul>
|
||||||
|
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||||
|
|
||||||
|
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), and [Kloudless](https://hubs.ly/H0f30Lf0).*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in OpenAPI schema support
|
||||||
|
|
||||||
|
REST framework now has a first-pass at directly including OpenAPI schema support. (Formerly known as Swagger)
|
||||||
|
|
||||||
|
Specifically:
|
||||||
|
|
||||||
|
* There are now `OpenAPIRenderer`, and `JSONOpenAPIRenderer` classes that deal with encoding `coreapi.Document` instances into OpenAPI YAML or OpenAPI JSON.
|
||||||
|
* The `get_schema_view(...)` method now defaults to OpenAPI YAML, with CoreJSON as a secondary
|
||||||
|
option if it is selected via HTTP content negotiation.
|
||||||
|
* There is a new management command `generateschema`, which you can use to dump
|
||||||
|
the schema into your repository.
|
||||||
|
|
||||||
|
Here's an example of adding an OpenAPI schema to the URL conf:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from rest_framework.schemas import get_schema_view
|
||||||
|
from rest_framework.renderers import JSONOpenAPIRenderer
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
title='Server Monitoring API',
|
||||||
|
url='https://www.example.org/api/',
|
||||||
|
renderer_classes=[JSONOpenAPIRenderer]
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url('^schema.json$', schema_view),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's how you can use the `generateschema` management command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ python manage.py generateschema --format openapi > schema.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
There's lots of different tooling that you can use for working with OpenAPI
|
||||||
|
schemas. One option that we're working on is the [API Star](https://docs.apistar.com/)
|
||||||
|
command line tool.
|
||||||
|
|
||||||
|
You can use `apistar` to validate your API schema:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ apistar validate --path schema.json --format openapi
|
||||||
|
✓ Valid OpenAPI schema.
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to build API documentation:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ apistar docs --path schema.json --format openapi
|
||||||
|
✓ Documentation built at "build/index.html".
|
||||||
|
```
|
||||||
|
|
||||||
|
API Star also includes a [dynamic client library](https://docs.apistar.com/client-library/)
|
||||||
|
that uses an API schema to automatically provide a client library interface for making requests.
|
||||||
|
|
||||||
|
## Composable permission classes
|
||||||
|
|
||||||
|
You can now compose permission classes using the and/or operators, `&` and `|`.
|
||||||
|
|
||||||
|
For example...
|
||||||
|
|
||||||
|
```python
|
||||||
|
permission_classes = [IsAuthenticated & (ReadOnly | IsAdmin)]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're using custom permission classes then make sure that you are subclassing
|
||||||
|
from `BasePermission` in order to enable this support.
|
||||||
|
|
||||||
|
## ViewSet _Extra Actions_ available in the Browsable API
|
||||||
|
|
||||||
|
Following the introduction of the `action` decorator in v3.8, _extra actions_ defined on a ViewSet are now available
|
||||||
|
from the Browsable API.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When defined, a dropdown of "Extra Actions", appropriately filtered to detail/non-detail actions, is displayed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
REST framework 3.9 supports Django versions 1.11, 2.0, and 2.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deprecations
|
||||||
|
|
||||||
|
### `DjangoObjectPermissionsFilter` moved to third-party package.
|
||||||
|
|
||||||
|
The `DjangoObjectPermissionsFilter` class is pending deprecation, will be deprecated in 3.10 and removed entirely in 3.11.
|
||||||
|
|
||||||
|
It has been moved to the third-party [`djangorestframework-guardian`](https://github.com/rpkilby/django-rest-framework-guardian)
|
||||||
|
package. Please use this instead.
|
||||||
|
|
||||||
|
### Router argument/method renamed to use `basename` for consistency.
|
||||||
|
|
||||||
|
* The `Router.register` `base_name` argument has been renamed in favor of `basename`.
|
||||||
|
* The `Router.get_default_base_name` method has been renamed in favor of `Router.get_default_basename`. [#5990][gh5990]
|
||||||
|
|
||||||
|
See [#5990][gh5990].
|
||||||
|
|
||||||
|
[gh5990]: https://github.com/encode/django-rest-framework/pull/5990
|
||||||
|
|
||||||
|
`base_name` and `get_default_base_name()` are pending deprecation. They will be deprecated in 3.10 and removed entirely in 3.11.
|
||||||
|
|
||||||
|
### `action` decorator replaces `list_route` and `detail_route`
|
||||||
|
|
||||||
|
Both `list_route` and `detail_route` are now deprecated in favour of the single `action` decorator.
|
||||||
|
They will be removed entirely in 3.10.
|
||||||
|
|
||||||
|
The `action` decorator takes a boolean `detail` argument.
|
||||||
|
|
||||||
|
* Replace `detail_route` uses with `@action(detail=True)`.
|
||||||
|
* Replace `list_route` uses with `@action(detail=False)`.
|
||||||
|
|
||||||
|
### `exclude_from_schema`
|
||||||
|
|
||||||
|
Both `APIView.exclude_from_schema` and the `exclude_from_schema` argument to the `@api_view` have now been removed.
|
||||||
|
|
||||||
|
For `APIView` you should instead set a `schema = None` attribute on the view class.
|
||||||
|
|
||||||
|
For function based views the `@schema` decorator can be used to exclude the view from the schema, by using `@schema(None)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Minor fixes and improvements
|
||||||
|
|
||||||
|
There are a large number of minor fixes and improvements in this release. See the [release notes](release-notes.md) page for a complete listing.
|
||||||
|
|
||||||
|
|
||||||
|
## What's next
|
||||||
|
|
||||||
|
We're planning to iteratively working towards OpenAPI becoming the standard schema
|
||||||
|
representation. This will mean that the `coreapi` dependency will gradually become
|
||||||
|
removed, and we'll instead generate the schema directly, rather than building
|
||||||
|
a CoreAPI `Document` object.
|
||||||
|
|
||||||
|
OpenAPI has clearly become the standard for specifying Web APIs, so there's not
|
||||||
|
much value any more in our schema-agnostic document model. Making this change
|
||||||
|
will mean that we'll more easily be able to take advantage of the full set of
|
||||||
|
OpenAPI functionality.
|
||||||
|
|
||||||
|
This will also make a wider range of tooling available.
|
||||||
|
|
||||||
|
We'll focus on continuing to develop the [API Star](https://docs.apistar.com/)
|
||||||
|
library and client tool into a recommended option for generating API docs,
|
||||||
|
validating API schemas, and providing a dynamic client library.
|
||||||
|
|
||||||
|
There's also a huge amount of ongoing work on maturing the ASGI landscape,
|
||||||
|
with the possibility that some of this work will eventually [feed back into
|
||||||
|
Django](https://www.aeracode.org/2018/06/04/django-async-roadmap/).
|
||||||
|
|
||||||
|
There will be further work on the [Uvicorn](https://www.uvicorn.org/)
|
||||||
|
webserver, as well as lots of functionality planned for the [Starlette](https://www.starlette.io/)
|
||||||
|
web framework, which is building a foundational set of tooling for working with
|
||||||
|
ASGI.
|
||||||
|
|
||||||
|
|
||||||
|
[funding]: funding.md
|
||||||
|
[gh5886]: https://github.com/encode/django-rest-framework/issues/5886
|
||||||
|
[gh5705]: https://github.com/encode/django-rest-framework/issues/5705
|
||||||
|
[openapi]: https://www.openapis.org/
|
||||||
|
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
|
@ -123,10 +123,10 @@ REST framework continues to be open-source and permissively licensed, but we fir
|
||||||
|
|
||||||
## What funding has enabled so far
|
## What funding has enabled so far
|
||||||
|
|
||||||
* The [3.4](http://www.django-rest-framework.org/topics/3.4-announcement/) and [3.5](http://www.django-rest-framework.org/topics/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
|
* The [3.4](https://www.django-rest-framework.org/topics/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/topics/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
|
||||||
* The [3.6](http://www.django-rest-framework.org/topics/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
|
* The [3.6](https://www.django-rest-framework.org/topics/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
|
||||||
* The [3.7 release](http://www.django-rest-framework.org/topics/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
|
* The [3.7 release](https://www.django-rest-framework.org/topics/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
|
||||||
* The recent [3.8 release](http://www.django-rest-framework.org/topics/3.8-announcement/).
|
* The recent [3.8 release](https://www.django-rest-framework.org/topics/3.8-announcement/).
|
||||||
* Tom Christie, the creator of Django REST framework, working on the project full-time.
|
* Tom Christie, the creator of Django REST framework, working on the project full-time.
|
||||||
* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time.
|
* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time.
|
||||||
* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship.
|
* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship.
|
||||||
|
@ -341,7 +341,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
|
||||||
|
|
||||||
## Accountability
|
## Accountability
|
||||||
|
|
||||||
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/march-2018) and regularly include financial reports and cost breakdowns.
|
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018) and regularly include financial reports and cost breakdowns.
|
||||||
|
|
||||||
<!-- Begin MailChimp Signup Form -->
|
<!-- Begin MailChimp Signup Form -->
|
||||||
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
||||||
|
|
|
@ -78,7 +78,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
|
||||||
<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-mirus_research.png);">Mirus Research</a></li>
|
<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-mirus_research.png);">Mirus Research</a></li>
|
||||||
<li><a href="https://hipolabs.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipo.png);">Hipo</a></li>
|
<li><a href="https://hipolabs.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipo.png);">Hipo</a></li>
|
||||||
<li><a href="https://www.byte.nl/" rel="nofollow" style="background-image:url(../../img/sponsors/2-byte.png);">Byte</a></li>
|
<li><a href="https://www.byte.nl/" rel="nofollow" style="background-image:url(../../img/sponsors/2-byte.png);">Byte</a></li>
|
||||||
<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
|
<li><a href="https://www.lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
|
||||||
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-opbeat.png);">Opbeat</a></li>
|
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-opbeat.png);">Opbeat</a></li>
|
||||||
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-koordinates.png);">Koordinates</a></li>
|
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-koordinates.png);">Koordinates</a></li>
|
||||||
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
|
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
|
||||||
|
@ -116,7 +116,7 @@ The serious financial contribution that our silver sponsors have made is very mu
|
||||||
<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-garfo.png);">Garfo</a></li>
|
<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-garfo.png);">Garfo</a></li>
|
||||||
<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-shippo.png);">Shippo</a></li>
|
<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-shippo.png);">Shippo</a></li>
|
||||||
<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-gizmag.png);">Gizmag</a></li>
|
<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-gizmag.png);">Gizmag</a></li>
|
||||||
<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-tivix.png);">Tivix</a></li>
|
<li><a href="https://www.tivix.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-tivix.png);">Tivix</a></li>
|
||||||
<li><a href="https://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-safari.png);">Safari</a></li>
|
<li><a href="https://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-safari.png);">Safari</a></li>
|
||||||
<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-brightloop.png);">Bright Loop</a></li>
|
<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-brightloop.png);">Bright Loop</a></li>
|
||||||
<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aba.png);">ABA Systems</a></li>
|
<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aba.png);">ABA Systems</a></li>
|
||||||
|
@ -131,7 +131,7 @@ The serious financial contribution that our silver sponsors have made is very mu
|
||||||
<li><a href="https://fluxility.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-fluxility.png);">Fluxility</a></li>
|
<li><a href="https://fluxility.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-fluxility.png);">Fluxility</a></li>
|
||||||
<li><a href="https://teonite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-teonite.png);">Teonite</a></li>
|
<li><a href="https://teonite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-teonite.png);">Teonite</a></li>
|
||||||
<li><a href="https://trackmaven.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-trackmaven.png);">TrackMaven</a></li>
|
<li><a href="https://trackmaven.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-trackmaven.png);">TrackMaven</a></li>
|
||||||
<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../../img/sponsors/3-phurba.png);">Phurba</a></li>
|
<li><a href="https://www.phurba.net/" rel="nofollow" style="background-image:url(../../img/sponsors/3-phurba.png);">Phurba</a></li>
|
||||||
<li><a href="https://www.nephila.it/it/" rel="nofollow" style="background-image:url(../../img/sponsors/3-nephila.png);">Nephila</a></li>
|
<li><a href="https://www.nephila.it/it/" rel="nofollow" style="background-image:url(../../img/sponsors/3-nephila.png);">Nephila</a></li>
|
||||||
<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aditium.png);">Aditium</a></li>
|
<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aditium.png);">Aditium</a></li>
|
||||||
<li><a href="https://www.eyesopen.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li>
|
<li><a href="https://www.eyesopen.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li>
|
||||||
|
|
|
@ -4,7 +4,7 @@ We have recently been [awarded a Mozilla grant](https://blog.mozilla.org/blog/20
|
||||||
|
|
||||||
Additionally, we will be building on the realtime support that Django Channels provides, supporting and documenting how to build realtime APIs with REST framework. Again, this will include supporting work in the associated client libraries, making it easier to build richly interactive applications.
|
Additionally, we will be building on the realtime support that Django Channels provides, supporting and documenting how to build realtime APIs with REST framework. Again, this will include supporting work in the associated client libraries, making it easier to build richly interactive applications.
|
||||||
|
|
||||||
The [Core API](http://www.coreapi.org) project will provide the foundations for our client library support, and will allow us to support interaction using a wide range of schemas and hypermedia formats. It's worth noting that these client libraries won't be tightly coupled to solely REST framework APIs either, and will be able to interact with *any* API that exposes a supported schema or hypermedia format.
|
The [Core API](https://www.coreapi.org/) project will provide the foundations for our client library support, and will allow us to support interaction using a wide range of schemas and hypermedia formats. It's worth noting that these client libraries won't be tightly coupled to solely REST framework APIs either, and will be able to interact with *any* API that exposes a supported schema or hypermedia format.
|
||||||
|
|
||||||
Specifically, the work includes:
|
Specifically, the work includes:
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ In order to ensure that I can be fully focused on trying to secure a sustainable
|
||||||
& well-funded open source business I will be leaving my current role at [DabApps](https://www.dabapps.com/)
|
& well-funded open source business I will be leaving my current role at [DabApps](https://www.dabapps.com/)
|
||||||
at the end of May 2016.
|
at the end of May 2016.
|
||||||
|
|
||||||
I have formed a UK limited company, [Encode](http://www.encode.io), which will
|
I have formed a UK limited company, [Encode](https://www.encode.io/), which will
|
||||||
act as the business entity behind REST framework. I will be issuing monthly reports
|
act as the business entity behind REST framework. I will be issuing monthly reports
|
||||||
from Encode on progress both towards the Mozilla grant, and for development time
|
from Encode on progress both towards the Mozilla grant, and for development time
|
||||||
funded via the [REST framework paid plans](funding.md).
|
funded via the [REST framework paid plans](funding.md).
|
||||||
|
|
|
@ -39,7 +39,7 @@ The following template should be used for the description of the issue, and serv
|
||||||
|
|
||||||
This issue is for determining the maintenance team for the *** period.
|
This issue is for determining the maintenance team for the *** period.
|
||||||
|
|
||||||
Please see the [Project management](http://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details.
|
Please see the [Project management](https://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ The following template should be used for the description of the issue, and serv
|
||||||
|
|
||||||
If you wish to be considered for this or a future date, please comment against this or subsequent issues.
|
If you wish to be considered for this or a future date, please comment against this or subsequent issues.
|
||||||
|
|
||||||
To modify this process for future maintenance cycles make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
|
To modify this process for future maintenance cycles make a pull request to the [project management](https://www.django-rest-framework.org/topics/project-management/) documentation.
|
||||||
|
|
||||||
#### Responsibilities of team members
|
#### Responsibilities of team members
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ The following template should be used for the description of the issue, and serv
|
||||||
|
|
||||||
During development cycle:
|
During development cycle:
|
||||||
|
|
||||||
- [ ] Upload the new content to be translated to [transifex](http://www.django-rest-framework.org/topics/project-management/#translations).
|
- [ ] Upload the new content to be translated to [transifex](https://www.django-rest-framework.org/topics/project-management/#translations).
|
||||||
|
|
||||||
|
|
||||||
Checklist:
|
Checklist:
|
||||||
|
@ -110,7 +110,7 @@ The following template should be used for the description of the issue, and serv
|
||||||
- [ ] `setup.py` Python & Django version trove classifiers
|
- [ ] `setup.py` Python & Django version trove classifiers
|
||||||
- [ ] `README` Python & Django versions
|
- [ ] `README` Python & Django versions
|
||||||
- [ ] `docs` Python & Django versions
|
- [ ] `docs` Python & Django versions
|
||||||
- [ ] Update the translations from [transifex](http://www.django-rest-framework.org/topics/project-management/#translations).
|
- [ ] Update the translations from [transifex](https://www.django-rest-framework.org/topics/project-management/#translations).
|
||||||
- [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/encode/django-rest-framework/blob/master/rest_framework/__init__.py).
|
- [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/encode/django-rest-framework/blob/master/rest_framework/__init__.py).
|
||||||
- [ ] Confirm with @tomchristie that release is finalized and ready to go.
|
- [ ] Confirm with @tomchristie that release is finalized and ready to go.
|
||||||
- [ ] Ensure that release date is included in pull request.
|
- [ ] Ensure that release date is included in pull request.
|
||||||
|
@ -122,7 +122,7 @@ The following template should be used for the description of the issue, and serv
|
||||||
- [ ] Make a release announcement on twitter.
|
- [ ] Make a release announcement on twitter.
|
||||||
- [ ] Close the milestone on GitHub.
|
- [ ] Close the milestone on GitHub.
|
||||||
|
|
||||||
To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
|
To modify this process for future releases make a pull request to the [project management](https://www.django-rest-framework.org/topics/project-management/) documentation.
|
||||||
|
|
||||||
When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages.
|
When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages.
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ When any user visible strings are changed, they should be uploaded to Transifex
|
||||||
|
|
||||||
# 1. Update the source django.po file, which is the US English version.
|
# 1. Update the source django.po file, which is the US English version.
|
||||||
cd rest_framework
|
cd rest_framework
|
||||||
django-admin.py makemessages -l en_US
|
django-admin makemessages -l en_US
|
||||||
# 2. Push the source django.po file to Transifex.
|
# 2. Push the source django.po file to Transifex.
|
||||||
cd ..
|
cd ..
|
||||||
tx push -s
|
tx push -s
|
||||||
|
@ -173,7 +173,7 @@ When a translator has finished translating their work needs to be downloaded fro
|
||||||
tx pull -a --minimum-perc 10
|
tx pull -a --minimum-perc 10
|
||||||
cd rest_framework
|
cd rest_framework
|
||||||
# 4. Compile the binary .mo files for all supported languages.
|
# 4. Compile the binary .mo files for all supported languages.
|
||||||
django-admin.py compilemessages
|
django-admin compilemessages
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,52 @@ You can determine your currently installed version using `pip show`:
|
||||||
|
|
||||||
### 3.9.0
|
### 3.9.0
|
||||||
|
|
||||||
**Date**: Unreleased
|
**Date**: [18th October 2018][3.9.0-milestone]
|
||||||
|
|
||||||
|
* Improvements to ViewSet extra actions [#5605][gh5605]
|
||||||
|
* Fix `action` support for ViewSet suffixes [#6081][gh6081]
|
||||||
|
* Allow `action` docs sections [#6060][gh6060]
|
||||||
* Deprecate the `Router.register` `base_name` argument in favor of `basename`. [#5990][gh5990]
|
* Deprecate the `Router.register` `base_name` argument in favor of `basename`. [#5990][gh5990]
|
||||||
* Deprecate the `Router.get_default_base_name` method in favor of `Router.get_default_basename`. [#5990][gh5990]
|
* Deprecate the `Router.get_default_base_name` method in favor of `Router.get_default_basename`. [#5990][gh5990]
|
||||||
|
* Change `CharField` to disallow null bytes. [#6073][gh6073]
|
||||||
|
To revert to the old behavior, subclass `CharField` and remove `ProhibitNullCharactersValidator` from the validators.
|
||||||
|
```python
|
||||||
|
class NullableCharField(serializers.CharField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validators = [v for v in self.validators if not isinstance(v, ProhibitNullCharactersValidator)]
|
||||||
|
```
|
||||||
|
* Add `OpenAPIRenderer` and `generate_schema` management command. [#6229][gh6229]
|
||||||
|
* Add OpenAPIRenderer by default, and add schema docs. [#6233][gh6233]
|
||||||
|
* Allow permissions to be composed [#5753][gh5753]
|
||||||
|
* Allow nullable BooleanField in Django 2.1 [#6183][gh6183]
|
||||||
|
* Add testing of Python 3.7 support [#6141][gh6141]
|
||||||
|
* Test using Django 2.1 final release. [#6109][gh6109]
|
||||||
|
* Added djangorestframework-datatables to third-party packages [#5931][gh5931]
|
||||||
|
* Change ISO 8601 date format to exclude year/month [#5936][gh5936]
|
||||||
|
* Update all pypi.python.org URLs to pypi.org [#5942][gh5942]
|
||||||
|
* Ensure that html forms (multipart form data) respect optional fields [#5927][gh5927]
|
||||||
|
* Allow hashing of ErrorDetail. [#5932][gh5932]
|
||||||
|
* Correct schema parsing for JSONField [#5878][gh5878]
|
||||||
|
* Render descriptions (from help_text) using safe [#5869][gh5869]
|
||||||
|
* Removed input value from deault_error_message [#5881][gh5881]
|
||||||
|
* Added min_value/max_value support in DurationField [#5643][gh5643]
|
||||||
|
* Fixed instance being overwritten in pk-only optimization try/except block [#5747][gh5747]
|
||||||
|
* Fixed AttributeError from items filter when value is None [#5981][gh5981]
|
||||||
|
* Fixed Javascript `e.indexOf` is not a function error [#5982][gh5982]
|
||||||
|
* Fix schemas for extra actions [#5992][gh5992]
|
||||||
|
* Improved get_error_detail to use error_dict/error_list [#5785][gh5785]
|
||||||
|
* Imprvied URLs in Admin renderer [#5988][gh5988]
|
||||||
|
* Add "Community" section to docs, minor cleanup [#5993][gh5993]
|
||||||
|
* Moved guardian imports out of compat [#6054][gh6054]
|
||||||
|
* Deprecate the `DjangoObjectPermissionsFilter` class, moved to the `djangorestframework-guardian` package. [#6075][gh6075]
|
||||||
|
* Drop Django 1.10 support [#5657][gh5657]
|
||||||
|
* Only catch TypeError/ValueError for object lookups [#6028][gh6028]
|
||||||
|
* Handle models without .objects manager in ModelSerializer. [#6111][gh6111]
|
||||||
|
* Improve ModelSerializer.create() error message. [#6112][gh6112]
|
||||||
|
* Fix CSRF cookie check failure when using session auth with django 1.11.6+ [#6113][gh6113]
|
||||||
|
* Updated JWT docs. [#6138][gh6138]
|
||||||
|
* Fix autoescape not getting passed to urlize_quoted_links filter [#6191][gh6191]
|
||||||
|
|
||||||
|
|
||||||
## 3.8.x series
|
## 3.8.x series
|
||||||
|
@ -1092,6 +1134,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
||||||
[3.8.0-milestone]: https://github.com/encode/django-rest-framework/milestone/61?closed=1
|
[3.8.0-milestone]: https://github.com/encode/django-rest-framework/milestone/61?closed=1
|
||||||
[3.8.1-milestone]: https://github.com/encode/django-rest-framework/milestone/67?closed=1
|
[3.8.1-milestone]: https://github.com/encode/django-rest-framework/milestone/67?closed=1
|
||||||
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
|
[3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1
|
||||||
|
[3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1
|
||||||
|
|
||||||
<!-- 3.0.1 -->
|
<!-- 3.0.1 -->
|
||||||
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
|
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
|
||||||
|
@ -1973,4 +2016,39 @@ For older release notes, [please see the version 2.x documentation][old-release-
|
||||||
[gh5920]: https://github.com/encode/django-rest-framework/issues/5920
|
[gh5920]: https://github.com/encode/django-rest-framework/issues/5920
|
||||||
|
|
||||||
<!-- 3.9.0 -->
|
<!-- 3.9.0 -->
|
||||||
|
[gh6109]: https://github.com/encode/django-rest-framework/issues/6109
|
||||||
|
[gh6141]: https://github.com/encode/django-rest-framework/issues/6141
|
||||||
|
[gh6113]: https://github.com/encode/django-rest-framework/issues/6113
|
||||||
|
[gh6112]: https://github.com/encode/django-rest-framework/issues/6112
|
||||||
|
[gh6111]: https://github.com/encode/django-rest-framework/issues/6111
|
||||||
|
[gh6028]: https://github.com/encode/django-rest-framework/issues/6028
|
||||||
|
[gh5657]: https://github.com/encode/django-rest-framework/issues/5657
|
||||||
|
[gh6054]: https://github.com/encode/django-rest-framework/issues/6054
|
||||||
|
[gh5993]: https://github.com/encode/django-rest-framework/issues/5993
|
||||||
[gh5990]: https://github.com/encode/django-rest-framework/issues/5990
|
[gh5990]: https://github.com/encode/django-rest-framework/issues/5990
|
||||||
|
[gh5988]: https://github.com/encode/django-rest-framework/issues/5988
|
||||||
|
[gh5785]: https://github.com/encode/django-rest-framework/issues/5785
|
||||||
|
[gh5992]: https://github.com/encode/django-rest-framework/issues/5992
|
||||||
|
[gh5605]: https://github.com/encode/django-rest-framework/issues/5605
|
||||||
|
[gh5982]: https://github.com/encode/django-rest-framework/issues/5982
|
||||||
|
[gh5981]: https://github.com/encode/django-rest-framework/issues/5981
|
||||||
|
[gh5747]: https://github.com/encode/django-rest-framework/issues/5747
|
||||||
|
[gh5643]: https://github.com/encode/django-rest-framework/issues/5643
|
||||||
|
[gh5881]: https://github.com/encode/django-rest-framework/issues/5881
|
||||||
|
[gh5869]: https://github.com/encode/django-rest-framework/issues/5869
|
||||||
|
[gh5878]: https://github.com/encode/django-rest-framework/issues/5878
|
||||||
|
[gh5932]: https://github.com/encode/django-rest-framework/issues/5932
|
||||||
|
[gh5927]: https://github.com/encode/django-rest-framework/issues/5927
|
||||||
|
[gh5942]: https://github.com/encode/django-rest-framework/issues/5942
|
||||||
|
[gh5936]: https://github.com/encode/django-rest-framework/issues/5936
|
||||||
|
[gh5931]: https://github.com/encode/django-rest-framework/issues/5931
|
||||||
|
[gh6183]: https://github.com/encode/django-rest-framework/issues/6183
|
||||||
|
[gh6075]: https://github.com/encode/django-rest-framework/issues/6075
|
||||||
|
[gh6138]: https://github.com/encode/django-rest-framework/issues/6138
|
||||||
|
[gh6081]: https://github.com/encode/django-rest-framework/issues/6081
|
||||||
|
[gh6073]: https://github.com/encode/django-rest-framework/issues/6073
|
||||||
|
[gh6191]: https://github.com/encode/django-rest-framework/issues/6191
|
||||||
|
[gh6060]: https://github.com/encode/django-rest-framework/issues/6060
|
||||||
|
[gh6233]: https://github.com/encode/django-rest-framework/issues/6233
|
||||||
|
[gh5753]: https://github.com/encode/django-rest-framework/issues/5753
|
||||||
|
[gh6229]: https://github.com/encode/django-rest-framework/issues/6229
|
||||||
|
|
|
@ -85,8 +85,8 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
|
|
||||||
|
|
||||||
[beginners-guide-to-the-django-rest-framework]: https://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
|
[beginners-guide-to-the-django-rest-framework]: https://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
|
||||||
[getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
|
[getting-started-with-django-rest-framework-and-angularjs]: https://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
|
||||||
[end-to-end-web-app-with-django-rest-framework-angularjs]: http://mourafiq.com/2013/07/01/end-to-end-web-app-with-django-angular-1.html
|
[end-to-end-web-app-with-django-rest-framework-angularjs]: https://mourafiq.com/2013/07/01/end-to-end-web-app-with-django-angular-1.html
|
||||||
[start-your-api-django-rest-framework-part-1]: https://godjango.com/41-start-your-api-django-rest-framework-part-1/
|
[start-your-api-django-rest-framework-part-1]: https://godjango.com/41-start-your-api-django-rest-framework-part-1/
|
||||||
[permissions-authentication-django-rest-framework-part-2]: https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/
|
[permissions-authentication-django-rest-framework-part-2]: https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/
|
||||||
[viewsets-and-routers-django-rest-framework-part-3]: https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/
|
[viewsets-and-routers-django-rest-framework-part-3]: https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 74 KiB |
BIN
docs/img/premium/auklet-readme.png
Normal file
BIN
docs/img/premium/auklet-readme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
docs/img/premium/kloudless-readme.png
Normal file
BIN
docs/img/premium/kloudless-readme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/img/premium/load-impact-readme.png
Normal file
BIN
docs/img/premium/load-impact-readme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -69,13 +69,15 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
|
||||||
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
|
||||||
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
|
||||||
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
|
<li><a href="https://auklet.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/auklet-new.png)">Auklet</a></li>
|
||||||
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar2.png)">Rollbar</a></li>
|
||||||
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
<li><a href="https://cadre.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cadre.png)">Cadre</a></li>
|
||||||
|
<li><a href="https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/load-impact.png)">Load Impact</a></li>
|
||||||
|
<li><a href="https://hubs.ly/H0f30Lf0" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/kloudless.png)">Kloudless</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
<div style="clear: both; padding-bottom: 20px;"></div>
|
||||||
|
|
||||||
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [Cadre](https://cadre.com).*
|
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), and [Kloudless](https://hubs.ly/H0f30Lf0).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -83,8 +85,8 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
|
|
||||||
REST framework requires the following:
|
REST framework requires the following:
|
||||||
|
|
||||||
* Python (2.7, 3.4, 3.5, 3.6)
|
* Python (2.7, 3.4, 3.5, 3.6, 3.7)
|
||||||
* Django (1.11, 2.0)
|
* Django (1.11, 2.0, 2.1)
|
||||||
|
|
||||||
The following packages are optional:
|
The following packages are optional:
|
||||||
|
|
||||||
|
@ -200,17 +202,22 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2011-2017, Tom Christie
|
Copyright © 2011-present, [Encode OSS Ltd](https://www.encode.io/).
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are met:
|
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.
|
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,
|
||||||
other materials provided with the distribution.
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
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
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
|
|
@ -521,7 +521,7 @@ You'll either want to include the API schema in your codebase directly, by copyi
|
||||||
})
|
})
|
||||||
|
|
||||||
[heroku-api]: https://devcenter.heroku.com/categories/platform-api
|
[heroku-api]: https://devcenter.heroku.com/categories/platform-api
|
||||||
[heroku-example]: http://www.coreapi.org/tools-and-resources/example-services/#heroku-json-hyper-schema
|
[heroku-example]: https://www.coreapi.org/tools-and-resources/example-services/#heroku-json-hyper-schema
|
||||||
[core-api]: http://www.coreapi.org/
|
[core-api]: https://www.coreapi.org/
|
||||||
[schema-generation]: ../api-guide/schemas.md
|
[schema-generation]: ../api-guide/schemas.md
|
||||||
[transport-adaptors]: http://docs.python-requests.org/en/master/user/advanced/#transport-adapters
|
[transport-adaptors]: http://docs.python-requests.org/en/master/user/advanced/#transport-adapters
|
||||||
|
|
|
@ -82,6 +82,6 @@ as well as how to support content types other than form-encoded data.
|
||||||
|
|
||||||
[cite]: https://www.amazon.com/RESTful-Web-Services-Leonard-Richardson/dp/0596529260
|
[cite]: https://www.amazon.com/RESTful-Web-Services-Leonard-Richardson/dp/0596529260
|
||||||
[ajax-form]: https://github.com/encode/ajax-form
|
[ajax-form]: https://github.com/encode/ajax-form
|
||||||
[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
|
[rails]: https://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
|
||||||
[html5]: https://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
[html5]: https://www.w3.org/TR/html5-diff/#changes-2010-06-24
|
||||||
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
[put_delete]: http://amundsen.com/examples/put-delete-forms/
|
||||||
|
|
|
@ -16,11 +16,11 @@ The built-in API documentation includes:
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
The `coreapi` library is required as a dependancy for the API docs. Make sure
|
The `coreapi` library is required as a dependency for the API docs. Make sure
|
||||||
to install the latest version. The `pygments` and `markdown` libraries
|
to install the latest version. The `pygments` and `markdown` libraries
|
||||||
are optional but recommended.
|
are optional but recommended.
|
||||||
|
|
||||||
To install the API documentation, you'll need to include it in your projects URLconf:
|
To install the API documentation, you'll need to include it in your project's URLconf:
|
||||||
|
|
||||||
from rest_framework.documentation import include_docs_urls
|
from rest_framework.documentation import include_docs_urls
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ This will include two different views:
|
||||||
**Note**: By default `include_docs_urls` configures the underlying `SchemaView` to generate _public_ schemas.
|
**Note**: By default `include_docs_urls` configures the underlying `SchemaView` to generate _public_ schemas.
|
||||||
This means that views will not be instantiated with a `request` instance. i.e. Inside the view `self.request` will be `None`.
|
This means that views will not be instantiated with a `request` instance. i.e. Inside the view `self.request` will be `None`.
|
||||||
|
|
||||||
To be compatible with this behaviour methods (such as `get_serializer` or `get_serializer_class` etc.) which inspect `self.request` or, particularly, `self.request.user` may need to be adjusted to handle this case.
|
To be compatible with this behaviour, methods (such as `get_serializer` or `get_serializer_class` etc.) which inspect `self.request` or, particularly, `self.request.user` may need to be adjusted to handle this case.
|
||||||
|
|
||||||
You may ensure views are given a `request` instance by calling `include_docs_urls` with `public=False`:
|
You may ensure views are given a `request` instance by calling `include_docs_urls` with `public=False`:
|
||||||
|
|
||||||
|
@ -90,6 +90,28 @@ When using viewsets, you should use the relevant action names as delimiters.
|
||||||
Create a new user instance.
|
Create a new user instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Custom actions on viewsets can also be documented in a similar way using the method names
|
||||||
|
as delimiters or by attaching the documentation to action mapping methods.
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ModelViewset):
|
||||||
|
...
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get', 'post'])
|
||||||
|
def some_action(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
get:
|
||||||
|
A description of the get method on the custom action.
|
||||||
|
|
||||||
|
post:
|
||||||
|
A description of the post method on the custom action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@some_action.mapping.put
|
||||||
|
def put_some_action():
|
||||||
|
"""
|
||||||
|
A description of the put method on the custom action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
### `documentation` API Reference
|
### `documentation` API Reference
|
||||||
|
|
||||||
|
@ -171,22 +193,6 @@ This also translates into a very useful interactive documentation viewer in the
|
||||||
|
|
||||||
![Screenshot - drf-yasg][image-drf-yasg]
|
![Screenshot - drf-yasg][image-drf-yasg]
|
||||||
|
|
||||||
|
|
||||||
#### DRF OpenAPI
|
|
||||||
|
|
||||||
[DRF OpenAPI][drf-openapi] bridges the gap between OpenAPI specification and tool chain with the schema exposed
|
|
||||||
out-of-the-box by Django Rest Framework. Its goals are:
|
|
||||||
|
|
||||||
* To be dropped into any existing DRF project without any code change necessary.
|
|
||||||
* Provide clear disctinction between request schema and response schema.
|
|
||||||
* Provide a versioning mechanism for each schema. Support defining schema by version range syntax, e.g. >1.0, <=2.0
|
|
||||||
* Support multiple response codes, not just 200
|
|
||||||
* All this information should be bound to view methods, not view classes.
|
|
||||||
|
|
||||||
It also tries to stay current with the maturing schema generation mechanism provided by DRF.
|
|
||||||
|
|
||||||
![Screenshot - DRF OpenAPI][image-drf-openapi]
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### DRF Docs
|
#### DRF Docs
|
||||||
|
@ -313,11 +319,9 @@ In this approach, rather than documenting the available API endpoints up front,
|
||||||
|
|
||||||
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
|
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
|
||||||
|
|
||||||
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[cite]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
[drf-yasg]: https://github.com/axnsan12/drf-yasg/
|
||||||
[image-drf-yasg]: ../img/drf-yasg.png
|
[image-drf-yasg]: ../img/drf-yasg.png
|
||||||
[drf-openapi]: https://github.com/limdauto/drf_openapi/
|
|
||||||
[image-drf-openapi]: ../img/drf-openapi.png
|
|
||||||
[drfdocs-repo]: https://github.com/ekonstantinidis/django-rest-framework-docs
|
[drfdocs-repo]: https://github.com/ekonstantinidis/django-rest-framework-docs
|
||||||
[drfdocs-website]: https://www.drfdocs.com/
|
[drfdocs-website]: https://www.drfdocs.com/
|
||||||
[drfdocs-demo]: http://demo.drfdocs.com/
|
[drfdocs-demo]: http://demo.drfdocs.com/
|
||||||
|
|
|
@ -4,7 +4,7 @@ REST framework is suitable for returning both API style responses, and regular H
|
||||||
|
|
||||||
## Rendering HTML
|
## Rendering HTML
|
||||||
|
|
||||||
In order to return HTML responses you'll need to either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
|
In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
|
||||||
|
|
||||||
The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response.
|
The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response.
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ REST framework includes these built-in translations both for standard exception
|
||||||
|
|
||||||
Note that the translations only apply to the error strings themselves. The format of error messages, and the keys of field names will remain the same. An example `400 Bad Request` response body might look like this:
|
Note that the translations only apply to the error strings themselves. The format of error messages, and the keys of field names will remain the same. An example `400 Bad Request` response body might look like this:
|
||||||
|
|
||||||
{"detail": {"username": ["Esse campo deve ser unico."]}}
|
{"detail": {"username": ["Esse campo deve ser único."]}}
|
||||||
|
|
||||||
If you want to use different string for parts of the response such as `detail` and `non_field_errors` then you can modify this behavior by using a [custom exception handler][custom-exception-handler].
|
If you want to use different string for parts of the response such as `detail` and `non_field_errors` then you can modify this behavior by using a [custom exception handler][custom-exception-handler].
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ What REST framework doesn't do is give you machine readable hypermedia formats s
|
||||||
|
|
||||||
[cite]: https://vimeo.com/channels/restfest/page:2
|
[cite]: https://vimeo.com/channels/restfest/page:2
|
||||||
[dissertation]: https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
[dissertation]: https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
|
||||||
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[hypertext-driven]: https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[restful-web-apis]: http://restfulwebapis.org/
|
[restful-web-apis]: http://restfulwebapis.org/
|
||||||
[building-hypermedia-apis]: https://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
|
[building-hypermedia-apis]: https://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
|
||||||
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
|
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
|
||||||
|
|
|
@ -27,7 +27,7 @@ Nested data structures are easy enough to work with if they're read-only - simpl
|
||||||
Some example output from our serializer.
|
Some example output from our serializer.
|
||||||
|
|
||||||
{
|
{
|
||||||
'title': 'Leaving party preperations',
|
'title': 'Leaving party preparations',
|
||||||
'items': [
|
'items': [
|
||||||
{'text': 'Compile playlist', 'is_completed': True},
|
{'text': 'Compile playlist', 'is_completed': True},
|
||||||
{'text': 'Send invites', 'is_completed': False},
|
{'text': 'Send invites', 'is_completed': False},
|
||||||
|
|
|
@ -33,7 +33,7 @@ Okay, we're ready to get coding.
|
||||||
To get started, let's create a new project to work with.
|
To get started, let's create a new project to work with.
|
||||||
|
|
||||||
cd ~
|
cd ~
|
||||||
django-admin.py startproject tutorial
|
django-admin startproject tutorial
|
||||||
cd tutorial
|
cd tutorial
|
||||||
|
|
||||||
Once that's done we can create an app that we'll use to create a simple Web API.
|
Once that's done we can create an app that we'll use to create a simple Web API.
|
||||||
|
@ -154,9 +154,9 @@ At this point we've translated the model instance into Python native datatypes.
|
||||||
|
|
||||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
|
||||||
from django.utils.six import BytesIO
|
import io
|
||||||
|
|
||||||
stream = BytesIO(content)
|
stream = io.BytesIO(content)
|
||||||
data = JSONParser().parse(stream)
|
data = JSONParser().parse(stream)
|
||||||
|
|
||||||
...then we restore those native datatypes into a fully populated object instance.
|
...then we restore those native datatypes into a fully populated object instance.
|
||||||
|
|
|
@ -106,7 +106,7 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
|
||||||
|
|
||||||
After adding all those names into our URLconf, our final `snippets/urls.py` file should look like this:
|
After adding all those names into our URLconf, our final `snippets/urls.py` file should look like this:
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.urls import path
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
from snippets import views
|
from snippets import views
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ Now check that it is available on the command line...
|
||||||
|
|
||||||
Command line client for interacting with CoreAPI services.
|
Command line client for interacting with CoreAPI services.
|
||||||
|
|
||||||
Visit http://www.coreapi.org for more information.
|
Visit https://www.coreapi.org/ for more information.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--version Display the package version number.
|
--version Display the package version number.
|
||||||
|
@ -220,8 +220,8 @@ We've reached the end of our tutorial. If you want to get more involved in the
|
||||||
|
|
||||||
**Now go build awesome things.**
|
**Now go build awesome things.**
|
||||||
|
|
||||||
[coreapi]: http://www.coreapi.org
|
[coreapi]: https://www.coreapi.org/
|
||||||
[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
[corejson]: https://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||||
[openapi]: https://openapis.org/
|
[openapi]: https://openapis.org/
|
||||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||||
[sandbox]: https://restframework.herokuapp.com/
|
[sandbox]: https://restframework.herokuapp.com/
|
||||||
|
|
|
@ -19,9 +19,9 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
|
|
||||||
# Set up a new project with a single application
|
# Set up a new project with a single application
|
||||||
django-admin.py startproject tutorial . # Note the trailing '.' character
|
django-admin startproject tutorial . # Note the trailing '.' character
|
||||||
cd tutorial
|
cd tutorial
|
||||||
django-admin.py startapp quickstart
|
django-admin startapp quickstart
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
The project layout should look like:
|
The project layout should look like:
|
||||||
|
@ -56,7 +56,7 @@ We'll also create an initial user named `admin` with a password of `password123`
|
||||||
|
|
||||||
python manage.py createsuperuser --email admin@example.com --username admin
|
python manage.py createsuperuser --email admin@example.com --username admin
|
||||||
|
|
||||||
Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding...
|
Once you've set up a database and the initial user is created and ready to go, open up the app's directory and we'll get coding...
|
||||||
|
|
||||||
## Serializers
|
## Serializers
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
|
|
||||||
<h1 id="404-page-not-found" style="text-align: center">404</h1>
|
<h1 id="404-page-not-found" style="text-align: center">404</h1>
|
||||||
<p style="text-align: center"><strong>Page not found</strong></p>
|
<p style="text-align: center"><strong>Page not found</strong></p>
|
||||||
<p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
|
<p style="text-align: center">Try the <a href="https://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
2
docs_theme/css/bootstrap-responsive.css
vendored
2
docs_theme/css/bootstrap-responsive.css
vendored
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Copyright 2012 Twitter, Inc
|
* Copyright 2012 Twitter, Inc
|
||||||
* Licensed under the Apache License v2.0
|
* Licensed under the Apache License v2.0
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Designed and built with all the love in the world @twitter by @mdo and @fat.
|
* Designed and built with all the love in the world @twitter by @mdo and @fat.
|
||||||
*/
|
*/
|
||||||
|
|
2
docs_theme/css/bootstrap.css
vendored
2
docs_theme/css/bootstrap.css
vendored
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Copyright 2012 Twitter, Inc
|
* Copyright 2012 Twitter, Inc
|
||||||
* Licensed under the Apache License v2.0
|
* Licensed under the Apache License v2.0
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Designed and built with all the love in the world @twitter by @mdo and @fat.
|
* Designed and built with all the love in the world @twitter by @mdo and @fat.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
|
<a class="brand" href="https://www.django-rest-framework.org/">Django REST framework</a>
|
||||||
<div class="nav-collapse collapse">
|
<div class="nav-collapse collapse">
|
||||||
{% if nav|length>1 %}
|
{% if nav|length>1 %}
|
||||||
<!-- Main navigation -->
|
<!-- Main navigation -->
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
site_name: Django REST framework
|
site_name: Django REST framework
|
||||||
site_url: http://www.django-rest-framework.org/
|
site_url: https://www.django-rest-framework.org/
|
||||||
site_description: Django REST framework - Web APIs for Django
|
site_description: Django REST framework - Web APIs for Django
|
||||||
|
|
||||||
repo_url: https://github.com/encode/django-rest-framework
|
repo_url: https://github.com/encode/django-rest-framework
|
||||||
|
@ -65,6 +65,7 @@ pages:
|
||||||
- 'Contributing to REST framework': 'community/contributing.md'
|
- 'Contributing to REST framework': 'community/contributing.md'
|
||||||
- 'Project management': 'community/project-management.md'
|
- 'Project management': 'community/project-management.md'
|
||||||
- 'Release Notes': 'community/release-notes.md'
|
- 'Release Notes': 'community/release-notes.md'
|
||||||
|
- '3.9 Announcement': 'community/3.9-announcement.md'
|
||||||
- '3.8 Announcement': 'community/3.8-announcement.md'
|
- '3.8 Announcement': 'community/3.8-announcement.md'
|
||||||
- '3.7 Announcement': 'community/3.7-announcement.md'
|
- '3.7 Announcement': 'community/3.7-announcement.md'
|
||||||
- '3.6 Announcement': 'community/3.6-announcement.md'
|
- '3.6 Announcement': 'community/3.6-announcement.md'
|
||||||
|
|
|
@ -5,3 +5,4 @@ django-guardian==1.4.9
|
||||||
django-filter==1.1.0
|
django-filter==1.1.0
|
||||||
coreapi==2.3.1
|
coreapi==2.3.1
|
||||||
coreschema==0.0.4
|
coreschema==0.0.4
|
||||||
|
pyyaml
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.8.2'
|
__version__ = '3.9.0'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2018 Tom Christie'
|
__copyright__ = 'Copyright 2011-2018 Tom Christie'
|
||||||
|
|
|
@ -10,6 +10,20 @@ from django.core import validators
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Python 3
|
||||||
|
from collections.abc import Mapping # noqa
|
||||||
|
except ImportError:
|
||||||
|
# Python 2.7
|
||||||
|
from collections import Mapping # noqa
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Python 3
|
||||||
|
import urllib.parse as urlparse # noqa
|
||||||
|
except ImportError:
|
||||||
|
# Python 2.7
|
||||||
|
from urlparse import urlparse # noqa
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.urls import ( # noqa
|
from django.urls import ( # noqa
|
||||||
URLPattern,
|
URLPattern,
|
||||||
|
@ -22,6 +36,11 @@ except ImportError:
|
||||||
RegexURLResolver as URLResolver,
|
RegexURLResolver as URLResolver,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.core.validators import ProhibitNullCharactersValidator # noqa
|
||||||
|
except ImportError:
|
||||||
|
ProhibitNullCharactersValidator = None
|
||||||
|
|
||||||
|
|
||||||
def get_original_route(urlpattern):
|
def get_original_route(urlpattern):
|
||||||
"""
|
"""
|
||||||
|
@ -89,7 +108,7 @@ def unicode_to_repr(value):
|
||||||
|
|
||||||
def unicode_http_header(value):
|
def unicode_http_header(value):
|
||||||
# Coerce HTTP header value to unicode.
|
# Coerce HTTP header value to unicode.
|
||||||
if isinstance(value, six.binary_type):
|
if isinstance(value, bytes):
|
||||||
return value.decode('iso-8859-1')
|
return value.decode('iso-8859-1')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -124,6 +143,13 @@ except ImportError:
|
||||||
coreschema = None
|
coreschema = None
|
||||||
|
|
||||||
|
|
||||||
|
# pyyaml is optional
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
|
||||||
# django-crispy-forms is optional
|
# django-crispy-forms is optional
|
||||||
try:
|
try:
|
||||||
import crispy_forms
|
import crispy_forms
|
||||||
|
|
|
@ -17,7 +17,7 @@ from django.utils import six
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
def api_view(http_method_names=None, exclude_from_schema=False):
|
def api_view(http_method_names=None):
|
||||||
"""
|
"""
|
||||||
Decorator that converts a function-based view into an APIView subclass.
|
Decorator that converts a function-based view into an APIView subclass.
|
||||||
Takes a list of allowed methods for the view as an argument.
|
Takes a list of allowed methods for the view as an argument.
|
||||||
|
@ -77,15 +77,8 @@ def api_view(http_method_names=None, exclude_from_schema=False):
|
||||||
WrappedAPIView.schema = getattr(func, 'schema',
|
WrappedAPIView.schema = getattr(func, 'schema',
|
||||||
APIView.schema)
|
APIView.schema)
|
||||||
|
|
||||||
if exclude_from_schema:
|
|
||||||
warnings.warn(
|
|
||||||
"The `exclude_from_schema` argument to `api_view` is deprecated. "
|
|
||||||
"Use the `schema` decorator instead, passing `None`.",
|
|
||||||
DeprecationWarning
|
|
||||||
)
|
|
||||||
WrappedAPIView.exclude_from_schema = exclude_from_schema
|
|
||||||
|
|
||||||
return WrappedAPIView.as_view()
|
return WrappedAPIView.as_view()
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,7 +124,7 @@ def schema(view_inspector):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def action(methods=None, detail=None, name=None, url_path=None, url_name=None, **kwargs):
|
def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Mark a ViewSet method as a routable action.
|
Mark a ViewSet method as a routable action.
|
||||||
|
|
||||||
|
@ -145,18 +138,22 @@ def action(methods=None, detail=None, name=None, url_path=None, url_name=None, *
|
||||||
"@action() missing required argument: 'detail'"
|
"@action() missing required argument: 'detail'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# name and suffix are mutually exclusive
|
||||||
|
if 'name' in kwargs and 'suffix' in kwargs:
|
||||||
|
raise TypeError("`name` and `suffix` are mutually exclusive arguments.")
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.mapping = MethodMapper(func, methods)
|
func.mapping = MethodMapper(func, methods)
|
||||||
|
|
||||||
func.detail = detail
|
func.detail = detail
|
||||||
func.name = name if name else pretty_name(func.__name__)
|
|
||||||
func.url_path = url_path if url_path else func.__name__
|
func.url_path = url_path if url_path else func.__name__
|
||||||
func.url_name = url_name if url_name else func.__name__.replace('_', '-')
|
func.url_name = url_name if url_name else func.__name__.replace('_', '-')
|
||||||
func.kwargs = kwargs
|
func.kwargs = kwargs
|
||||||
func.kwargs.update({
|
|
||||||
'name': func.name,
|
# Set descriptive arguments for viewsets
|
||||||
'description': func.__doc__ or None
|
if 'name' not in kwargs and 'suffix' not in kwargs:
|
||||||
})
|
func.kwargs['name'] = pretty_name(func.__name__)
|
||||||
|
func.kwargs['description'] = func.__doc__ or None
|
||||||
|
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
@ -226,9 +223,9 @@ def detail_route(methods=None, **kwargs):
|
||||||
Used to mark a method on a ViewSet that should be routed for detail requests.
|
Used to mark a method on a ViewSet that should be routed for detail requests.
|
||||||
"""
|
"""
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`detail_route` is pending deprecation and will be removed in 3.10 in favor of "
|
"`detail_route` is deprecated and will be removed in 3.10 in favor of "
|
||||||
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
|
"`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.",
|
||||||
PendingDeprecationWarning, stacklevel=2
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
@ -244,9 +241,9 @@ def list_route(methods=None, **kwargs):
|
||||||
Used to mark a method on a ViewSet that should be routed for list requests.
|
Used to mark a method on a ViewSet that should be routed for list requests.
|
||||||
"""
|
"""
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`list_route` is pending deprecation and will be removed in 3.10 in favor of "
|
"`list_route` is deprecated and will be removed in 3.10 in favor of "
|
||||||
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
|
"`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.",
|
||||||
PendingDeprecationWarning, stacklevel=2
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
|
|
@ -34,7 +34,8 @@ from pytz.exceptions import InvalidTimeError
|
||||||
from rest_framework import ISO_8601
|
from rest_framework import ISO_8601
|
||||||
from rest_framework.compat import (
|
from rest_framework.compat import (
|
||||||
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
|
||||||
MinValueValidator, unicode_repr, unicode_to_repr
|
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
|
||||||
|
unicode_to_repr
|
||||||
)
|
)
|
||||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
@ -674,10 +675,7 @@ class BooleanField(Field):
|
||||||
'0', 0, 0.0,
|
'0', 0, 0.0,
|
||||||
False
|
False
|
||||||
}
|
}
|
||||||
|
NULL_VALUES = {'null', 'Null', 'NULL', '', None}
|
||||||
def __init__(self, **kwargs):
|
|
||||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
|
|
||||||
super(BooleanField, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
try:
|
try:
|
||||||
|
@ -685,6 +683,8 @@ class BooleanField(Field):
|
||||||
return True
|
return True
|
||||||
elif data in self.FALSE_VALUES:
|
elif data in self.FALSE_VALUES:
|
||||||
return False
|
return False
|
||||||
|
elif data in self.NULL_VALUES and self.allow_null:
|
||||||
|
return None
|
||||||
except TypeError: # Input is an unhashable type
|
except TypeError: # Input is an unhashable type
|
||||||
pass
|
pass
|
||||||
self.fail('invalid', input=data)
|
self.fail('invalid', input=data)
|
||||||
|
@ -694,6 +694,8 @@ class BooleanField(Field):
|
||||||
return True
|
return True
|
||||||
elif value in self.FALSE_VALUES:
|
elif value in self.FALSE_VALUES:
|
||||||
return False
|
return False
|
||||||
|
if value in self.NULL_VALUES and self.allow_null:
|
||||||
|
return None
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -718,7 +720,7 @@ class NullBooleanField(Field):
|
||||||
'0', 0, 0.0,
|
'0', 0, 0.0,
|
||||||
False
|
False
|
||||||
}
|
}
|
||||||
NULL_VALUES = {'n', 'N', 'null', 'Null', 'NULL', '', None}
|
NULL_VALUES = {'null', 'Null', 'NULL', '', None}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
|
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
|
||||||
|
@ -754,7 +756,7 @@ class CharField(Field):
|
||||||
'invalid': _('Not a valid string.'),
|
'invalid': _('Not a valid string.'),
|
||||||
'blank': _('This field may not be blank.'),
|
'blank': _('This field may not be blank.'),
|
||||||
'max_length': _('Ensure this field has no more than {max_length} characters.'),
|
'max_length': _('Ensure this field has no more than {max_length} characters.'),
|
||||||
'min_length': _('Ensure this field has at least {min_length} characters.')
|
'min_length': _('Ensure this field has at least {min_length} characters.'),
|
||||||
}
|
}
|
||||||
initial = ''
|
initial = ''
|
||||||
|
|
||||||
|
@ -777,6 +779,10 @@ class CharField(Field):
|
||||||
self.validators.append(
|
self.validators.append(
|
||||||
MinLengthValidator(self.min_length, message=message))
|
MinLengthValidator(self.min_length, message=message))
|
||||||
|
|
||||||
|
# ProhibitNullCharactersValidator is None on Django < 2.0
|
||||||
|
if ProhibitNullCharactersValidator is not None:
|
||||||
|
self.validators.append(ProhibitNullCharactersValidator())
|
||||||
|
|
||||||
def run_validation(self, data=empty):
|
def run_validation(self, data=empty):
|
||||||
# Test for the empty string here so that it does not get validated,
|
# Test for the empty string here so that it does not get validated,
|
||||||
# and so that subclasses do not need to handle it explicitly
|
# and so that subclasses do not need to handle it explicitly
|
||||||
|
@ -1779,7 +1785,7 @@ class JSONField(Field):
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
try:
|
try:
|
||||||
if self.binary or getattr(data, 'is_json_string', False):
|
if self.binary or getattr(data, 'is_json_string', False):
|
||||||
if isinstance(data, six.binary_type):
|
if isinstance(data, bytes):
|
||||||
data = data.decode('utf-8')
|
data = data.decode('utf-8')
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,6 +5,7 @@ returned by list views.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
|
import warnings
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -284,6 +285,11 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||||
has read object level permissions.
|
has read object level permissions.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
warnings.warn(
|
||||||
|
"`DjangoObjectPermissionsFilter` has been deprecated and moved to "
|
||||||
|
"the 3rd-party django-rest-framework-guardian package.",
|
||||||
|
DeprecationWarning, stacklevel=2
|
||||||
|
)
|
||||||
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
|
assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
|
||||||
|
|
||||||
perm_format = '%(app_label)s.view_%(model_name)s'
|
perm_format = '%(app_label)s.view_%(model_name)s'
|
||||||
|
|
0
rest_framework/management/__init__.py
Normal file
0
rest_framework/management/__init__.py
Normal file
0
rest_framework/management/commands/__init__.py
Normal file
0
rest_framework/management/commands/__init__.py
Normal file
39
rest_framework/management/commands/generateschema.py
Normal file
39
rest_framework/management/commands/generateschema.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from rest_framework.compat import coreapi
|
||||||
|
from rest_framework.renderers import (
|
||||||
|
CoreJSONRenderer, JSONOpenAPIRenderer, OpenAPIRenderer
|
||||||
|
)
|
||||||
|
from rest_framework.schemas.generators import SchemaGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Generates configured API schema for project."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--title', dest="title", default=None, type=str)
|
||||||
|
parser.add_argument('--url', dest="url", default=None, type=str)
|
||||||
|
parser.add_argument('--description', dest="description", default=None, type=str)
|
||||||
|
parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json', 'corejson'], default='openapi', type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
assert coreapi is not None, 'coreapi must be installed.'
|
||||||
|
|
||||||
|
generator = SchemaGenerator(
|
||||||
|
url=options['url'],
|
||||||
|
title=options['title'],
|
||||||
|
description=options['description']
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = generator.get_schema(request=None, public=True)
|
||||||
|
|
||||||
|
renderer = self.get_renderer(options['format'])
|
||||||
|
output = renderer.render(schema, renderer_context={})
|
||||||
|
self.stdout.write(output.decode('utf-8'))
|
||||||
|
|
||||||
|
def get_renderer(self, format):
|
||||||
|
return {
|
||||||
|
'corejson': CoreJSONRenderer(),
|
||||||
|
'openapi': OpenAPIRenderer(),
|
||||||
|
'openapi-json': JSONOpenAPIRenderer()
|
||||||
|
}[format]
|
|
@ -1 +0,0 @@
|
||||||
# Just to keep things like ./manage.py test happy
|
|
|
@ -472,7 +472,7 @@ class CursorPagination(BasePagination):
|
||||||
"""
|
"""
|
||||||
The cursor pagination implementation is necessarily complex.
|
The cursor pagination implementation is necessarily complex.
|
||||||
For an overview of the position/offset style we use, see this post:
|
For an overview of the position/offset style we use, see this post:
|
||||||
http://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
|
https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
|
||||||
"""
|
"""
|
||||||
cursor_query_param = 'cursor'
|
cursor_query_param = 'cursor'
|
||||||
cursor_query_description = _('The pagination cursor value.')
|
cursor_query_description = _('The pagination cursor value.')
|
||||||
|
@ -544,12 +544,11 @@ class CursorPagination(BasePagination):
|
||||||
has_following_position = False
|
has_following_position = False
|
||||||
following_position = None
|
following_position = None
|
||||||
|
|
||||||
# If we have a reverse queryset, then the query ordering was in reverse
|
|
||||||
# so we need to reverse the items again before returning them to the user.
|
|
||||||
if reverse:
|
if reverse:
|
||||||
|
# If we have a reverse queryset, then the query ordering was in reverse
|
||||||
|
# so we need to reverse the items again before returning them to the user.
|
||||||
self.page = list(reversed(self.page))
|
self.page = list(reversed(self.page))
|
||||||
|
|
||||||
if reverse:
|
|
||||||
# Determine next and previous positions for reverse cursors.
|
# Determine next and previous positions for reverse cursors.
|
||||||
self.has_next = (current_position is not None) or (offset > 0)
|
self.has_next = (current_position is not None) or (offset > 0)
|
||||||
self.has_previous = has_following_position
|
self.has_previous = has_following_position
|
||||||
|
|
|
@ -4,12 +4,76 @@ Provides a set of pluggable permission policies.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
|
|
||||||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||||
|
|
||||||
|
|
||||||
|
class OperandHolder:
|
||||||
|
def __init__(self, operator_class, op1_class, op2_class):
|
||||||
|
self.operator_class = operator_class
|
||||||
|
self.op1_class = op1_class
|
||||||
|
self.op2_class = op2_class
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
op1 = self.op1_class(*args, **kwargs)
|
||||||
|
op2 = self.op2_class(*args, **kwargs)
|
||||||
|
return self.operator_class(op1, op2)
|
||||||
|
|
||||||
|
|
||||||
|
class AND:
|
||||||
|
def __init__(self, op1, op2):
|
||||||
|
self.op1 = op1
|
||||||
|
self.op2 = op2
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return (
|
||||||
|
self.op1.has_permission(request, view) &
|
||||||
|
self.op2.has_permission(request, view)
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return (
|
||||||
|
self.op1.has_object_permission(request, view, obj) &
|
||||||
|
self.op2.has_object_permission(request, view, obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OR:
|
||||||
|
def __init__(self, op1, op2):
|
||||||
|
self.op1 = op1
|
||||||
|
self.op2 = op2
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return (
|
||||||
|
self.op1.has_permission(request, view) |
|
||||||
|
self.op2.has_permission(request, view)
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return (
|
||||||
|
self.op1.has_object_permission(request, view, obj) |
|
||||||
|
self.op2.has_object_permission(request, view, obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePermissionMetaclass(type):
|
||||||
|
def __and__(cls, other):
|
||||||
|
return OperandHolder(AND, cls, other)
|
||||||
|
|
||||||
|
def __or__(cls, other):
|
||||||
|
return OperandHolder(OR, cls, other)
|
||||||
|
|
||||||
|
def __rand__(cls, other):
|
||||||
|
return OperandHolder(AND, other, cls)
|
||||||
|
|
||||||
|
def __ror__(cls, other):
|
||||||
|
return OperandHolder(OR, other, cls)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(BasePermissionMetaclass)
|
||||||
class BasePermission(object):
|
class BasePermission(object):
|
||||||
"""
|
"""
|
||||||
A base class from which all permission classes should inherit.
|
A base class from which all permission classes should inherit.
|
||||||
|
@ -46,7 +110,7 @@ class IsAuthenticated(BasePermission):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return request.user and request.user.is_authenticated
|
return bool(request.user and request.user.is_authenticated)
|
||||||
|
|
||||||
|
|
||||||
class IsAdminUser(BasePermission):
|
class IsAdminUser(BasePermission):
|
||||||
|
@ -55,7 +119,7 @@ class IsAdminUser(BasePermission):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return request.user and request.user.is_staff
|
return bool(request.user and request.user.is_staff)
|
||||||
|
|
||||||
|
|
||||||
class IsAuthenticatedOrReadOnly(BasePermission):
|
class IsAuthenticatedOrReadOnly(BasePermission):
|
||||||
|
@ -64,7 +128,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
return (
|
return bool(
|
||||||
request.method in SAFE_METHODS or
|
request.method in SAFE_METHODS or
|
||||||
request.user and
|
request.user and
|
||||||
request.user.is_authenticated
|
request.user.is_authenticated
|
||||||
|
|
|
@ -24,8 +24,8 @@ from django.utils.html import mark_safe
|
||||||
|
|
||||||
from rest_framework import VERSION, exceptions, serializers, status
|
from rest_framework import VERSION, exceptions, serializers, status
|
||||||
from rest_framework.compat import (
|
from rest_framework.compat import (
|
||||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi,
|
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
|
||||||
pygments_css
|
pygments_css, urlparse, yaml
|
||||||
)
|
)
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.request import is_form_media_type, override_method
|
from rest_framework.request import is_form_media_type, override_method
|
||||||
|
@ -932,3 +932,119 @@ class CoreJSONRenderer(BaseRenderer):
|
||||||
indent = bool(renderer_context.get('indent', 0))
|
indent = bool(renderer_context.get('indent', 0))
|
||||||
codec = coreapi.codecs.CoreJSONCodec()
|
codec = coreapi.codecs.CoreJSONCodec()
|
||||||
return codec.dump(data, indent=indent)
|
return codec.dump(data, indent=indent)
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseOpenAPIRenderer:
|
||||||
|
def get_schema(self, instance):
|
||||||
|
CLASS_TO_TYPENAME = {
|
||||||
|
coreschema.Object: 'object',
|
||||||
|
coreschema.Array: 'array',
|
||||||
|
coreschema.Number: 'number',
|
||||||
|
coreschema.Integer: 'integer',
|
||||||
|
coreschema.String: 'string',
|
||||||
|
coreschema.Boolean: 'boolean',
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = {}
|
||||||
|
if instance.__class__ in CLASS_TO_TYPENAME:
|
||||||
|
schema['type'] = CLASS_TO_TYPENAME[instance.__class__]
|
||||||
|
schema['title'] = instance.title,
|
||||||
|
schema['description'] = instance.description
|
||||||
|
if hasattr(instance, 'enum'):
|
||||||
|
schema['enum'] = instance.enum
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_parameters(self, link):
|
||||||
|
parameters = []
|
||||||
|
for field in link.fields:
|
||||||
|
if field.location not in ['path', 'query']:
|
||||||
|
continue
|
||||||
|
parameter = {
|
||||||
|
'name': field.name,
|
||||||
|
'in': field.location,
|
||||||
|
}
|
||||||
|
if field.required:
|
||||||
|
parameter['required'] = True
|
||||||
|
if field.description:
|
||||||
|
parameter['description'] = field.description
|
||||||
|
if field.schema:
|
||||||
|
parameter['schema'] = self.get_schema(field.schema)
|
||||||
|
parameters.append(parameter)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
def get_operation(self, link, name, tag):
|
||||||
|
operation_id = "%s_%s" % (tag, name) if tag else name
|
||||||
|
parameters = self.get_parameters(link)
|
||||||
|
|
||||||
|
operation = {
|
||||||
|
'operationId': operation_id,
|
||||||
|
}
|
||||||
|
if link.title:
|
||||||
|
operation['summary'] = link.title
|
||||||
|
if link.description:
|
||||||
|
operation['description'] = link.description
|
||||||
|
if parameters:
|
||||||
|
operation['parameters'] = parameters
|
||||||
|
if tag:
|
||||||
|
operation['tags'] = [tag]
|
||||||
|
return operation
|
||||||
|
|
||||||
|
def get_paths(self, document):
|
||||||
|
paths = {}
|
||||||
|
|
||||||
|
tag = None
|
||||||
|
for name, link in document.links.items():
|
||||||
|
path = urlparse.urlparse(link.url).path
|
||||||
|
method = link.action.lower()
|
||||||
|
paths.setdefault(path, {})
|
||||||
|
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||||
|
|
||||||
|
for tag, section in document.data.items():
|
||||||
|
for name, link in section.links.items():
|
||||||
|
path = urlparse.urlparse(link.url).path
|
||||||
|
method = link.action.lower()
|
||||||
|
paths.setdefault(path, {})
|
||||||
|
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
def get_structure(self, data):
|
||||||
|
return {
|
||||||
|
'openapi': '3.0.0',
|
||||||
|
'info': {
|
||||||
|
'version': '',
|
||||||
|
'title': data.title,
|
||||||
|
'description': data.description
|
||||||
|
},
|
||||||
|
'servers': [{
|
||||||
|
'url': data.url
|
||||||
|
}],
|
||||||
|
'paths': self.get_paths(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||||
|
media_type = 'application/vnd.oai.openapi'
|
||||||
|
charset = None
|
||||||
|
format = 'openapi'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
assert coreapi, 'Using OpenAPIRenderer, but `coreapi` is not installed.'
|
||||||
|
assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
|
||||||
|
|
||||||
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
structure = self.get_structure(data)
|
||||||
|
return yaml.dump(structure, default_flow_style=False).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class JSONOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||||
|
media_type = 'application/vnd.oai.openapi+json'
|
||||||
|
charset = None
|
||||||
|
format = 'openapi-json'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
assert coreapi, 'Using JSONOpenAPIRenderer, but `coreapi` is not installed.'
|
||||||
|
|
||||||
|
def render(self, data, media_type=None, renderer_context=None):
|
||||||
|
structure = self.get_structure(data)
|
||||||
|
return json.dumps(structure, indent=4).encode('utf-8')
|
||||||
|
|
|
@ -10,6 +10,7 @@ The wrapped request then offers a richer API, in particular :
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
@ -301,7 +302,7 @@ class Request(object):
|
||||||
elif not self._request._read_started:
|
elif not self._request._read_started:
|
||||||
self._stream = self._request
|
self._stream = self._request
|
||||||
else:
|
else:
|
||||||
self._stream = six.BytesIO(self.body)
|
self._stream = io.BytesIO(self.body)
|
||||||
|
|
||||||
def _supports_form_parsing(self):
|
def _supports_form_parsing(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -40,10 +40,10 @@ DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'
|
||||||
class DynamicDetailRoute(object):
|
class DynamicDetailRoute(object):
|
||||||
def __new__(cls, url, name, initkwargs):
|
def __new__(cls, url, name, initkwargs):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`DynamicDetailRoute` is pending deprecation and will be removed in 3.10 "
|
"`DynamicDetailRoute` is deprecated and will be removed in 3.10 "
|
||||||
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
"in favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||||
"`DynamicRoute(url, name, True, initkwargs)` instead.",
|
"`DynamicRoute(url, name, True, initkwargs)` instead.",
|
||||||
PendingDeprecationWarning, stacklevel=2
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
return DynamicRoute(url, name, True, initkwargs)
|
return DynamicRoute(url, name, True, initkwargs)
|
||||||
|
|
||||||
|
@ -51,10 +51,10 @@ class DynamicDetailRoute(object):
|
||||||
class DynamicListRoute(object):
|
class DynamicListRoute(object):
|
||||||
def __new__(cls, url, name, initkwargs):
|
def __new__(cls, url, name, initkwargs):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`DynamicListRoute` is pending deprecation and will be removed in 3.10 in "
|
"`DynamicListRoute` is deprecated and will be removed in 3.10 in "
|
||||||
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
"favor of `DynamicRoute`, which accepts a `detail` boolean. Use "
|
||||||
"`DynamicRoute(url, name, False, initkwargs)` instead.",
|
"`DynamicRoute(url, name, False, initkwargs)` instead.",
|
||||||
PendingDeprecationWarning, stacklevel=2
|
DeprecationWarning, stacklevel=2
|
||||||
)
|
)
|
||||||
return DynamicRoute(url, name, False, initkwargs)
|
return DynamicRoute(url, name, False, initkwargs)
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ def flatten(list_of_lists):
|
||||||
|
|
||||||
class RenameRouterMethods(RenameMethodsBase):
|
class RenameRouterMethods(RenameMethodsBase):
|
||||||
renamed_methods = (
|
renamed_methods = (
|
||||||
('get_default_base_name', 'get_default_basename', DeprecationWarning),
|
('get_default_base_name', 'get_default_basename', PendingDeprecationWarning),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,8 +87,8 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)):
|
||||||
|
|
||||||
def register(self, prefix, viewset, basename=None, base_name=None):
|
def register(self, prefix, viewset, basename=None, base_name=None):
|
||||||
if base_name is not None:
|
if base_name is not None:
|
||||||
msg = "The `base_name` argument has been deprecated in favor of `basename`."
|
msg = "The `base_name` argument is pending deprecation in favor of `basename`."
|
||||||
warnings.warn(msg, DeprecationWarning, 2)
|
warnings.warn(msg, PendingDeprecationWarning, 2)
|
||||||
|
|
||||||
assert not (basename and base_name), (
|
assert not (basename and base_name), (
|
||||||
"Do not provide both the `basename` and `base_name` arguments.")
|
"Do not provide both the `basename` and `base_name` arguments.")
|
||||||
|
|
|
@ -4,7 +4,6 @@ generators.py # Top-down schema generation
|
||||||
See schemas.__init__.py for package overview.
|
See schemas.__init__.py for package overview.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import warnings
|
|
||||||
from collections import Counter, OrderedDict
|
from collections import Counter, OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
|
@ -207,14 +206,6 @@ class EndpointEnumerator(object):
|
||||||
if not is_api_view(callback):
|
if not is_api_view(callback):
|
||||||
return False # Ignore anything except REST framework views.
|
return False # Ignore anything except REST framework views.
|
||||||
|
|
||||||
if hasattr(callback.cls, 'exclude_from_schema'):
|
|
||||||
fmt = ("The `{}.exclude_from_schema` attribute is deprecated. "
|
|
||||||
"Set `schema = None` instead.")
|
|
||||||
msg = fmt.format(callback.cls.__name__)
|
|
||||||
warnings.warn(msg, DeprecationWarning)
|
|
||||||
if getattr(callback.cls, 'exclude_from_schema', False):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if callback.cls.schema is None:
|
if callback.cls.schema is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -247,9 +247,11 @@ class AutoSchema(ViewInspector):
|
||||||
method_docstring = getattr(view, method_name, None).__doc__
|
method_docstring = getattr(view, method_name, None).__doc__
|
||||||
if method_docstring:
|
if method_docstring:
|
||||||
# An explicit docstring on the method or action.
|
# An explicit docstring on the method or action.
|
||||||
return formatting.dedent(smart_text(method_docstring))
|
return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring)))
|
||||||
|
else:
|
||||||
|
return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description())
|
||||||
|
|
||||||
description = view.get_view_description()
|
def _get_description_section(self, view, header, description):
|
||||||
lines = [line for line in description.splitlines()]
|
lines = [line for line in description.splitlines()]
|
||||||
current_section = ''
|
current_section = ''
|
||||||
sections = {'': ''}
|
sections = {'': ''}
|
||||||
|
@ -263,7 +265,6 @@ class AutoSchema(ViewInspector):
|
||||||
|
|
||||||
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
|
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
|
||||||
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
||||||
header = getattr(view, 'action', method.lower())
|
|
||||||
if header in sections:
|
if header in sections:
|
||||||
return sections[header].strip()
|
return sections[header].strip()
|
||||||
if header in coerce_method_names:
|
if header in coerce_method_names:
|
||||||
|
|
|
@ -19,13 +19,12 @@ class SchemaView(APIView):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SchemaView, self).__init__(*args, **kwargs)
|
super(SchemaView, self).__init__(*args, **kwargs)
|
||||||
if self.renderer_classes is None:
|
if self.renderer_classes is None:
|
||||||
|
self.renderer_classes = [
|
||||||
|
renderers.OpenAPIRenderer,
|
||||||
|
renderers.CoreJSONRenderer
|
||||||
|
]
|
||||||
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
|
||||||
self.renderer_classes = [
|
self.renderer_classes += [renderers.BrowsableAPIRenderer]
|
||||||
renderers.CoreJSONRenderer,
|
|
||||||
renderers.BrowsableAPIRenderer,
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
self.renderer_classes = [renderers.CoreJSONRenderer]
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
schema = self.schema_generator.get_schema(request, self.public)
|
schema = self.schema_generator.get_schema(request, self.public)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from __future__ import unicode_literals
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import traceback
|
import traceback
|
||||||
from collections import Mapping, OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
@ -27,7 +27,7 @@ from django.utils import six, timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.compat import postgres_fields, unicode_to_repr
|
from rest_framework.compat import Mapping, postgres_fields, unicode_to_repr
|
||||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||||
from rest_framework.fields import get_error_detail, set_value
|
from rest_framework.fields import get_error_detail, set_value
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
|
@ -232,7 +232,7 @@ class APISettings(object):
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def __check_user_settings(self, user_settings):
|
def __check_user_settings(self, user_settings):
|
||||||
SETTINGS_DOC = "http://www.django-rest-framework.org/api-guide/settings/"
|
SETTINGS_DOC = "https://www.django-rest-framework.org/api-guide/settings/"
|
||||||
for setting in REMOVED_SETTINGS:
|
for setting in REMOVED_SETTINGS:
|
||||||
if setting in user_settings:
|
if setting in user_settings:
|
||||||
raise RuntimeError("The '%s' setting has been removed. Please refer to '%s' for available settings." % (setting, SETTINGS_DOC))
|
raise RuntimeError("The '%s' setting has been removed. Please refer to '%s' for available settings." % (setting, SETTINGS_DOC))
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span>
|
<span>
|
||||||
{% block branding %}
|
{% block branding %}
|
||||||
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
|
<a class='navbar-brand' rel="nofollow" href='https://www.django-rest-framework.org/'>
|
||||||
Django REST framework
|
Django REST framework
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span>
|
<span>
|
||||||
{% block branding %}
|
{% block branding %}
|
||||||
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
|
<a class='navbar-brand' rel="nofollow" href='https://www.django-rest-framework.org/'>
|
||||||
Django REST framework
|
Django REST framework
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -314,7 +314,7 @@ def smart_urlquote_wrapper(matched_url):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter(needs_autoescape=True)
|
||||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||||
"""
|
"""
|
||||||
Converts any URLs in text into clickable links.
|
Converts any URLs in text into clickable links.
|
||||||
|
|
|
@ -47,7 +47,7 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
return six.text_type(obj)
|
return six.text_type(obj)
|
||||||
elif isinstance(obj, QuerySet):
|
elif isinstance(obj, QuerySet):
|
||||||
return tuple(obj)
|
return tuple(obj)
|
||||||
elif isinstance(obj, six.binary_type):
|
elif isinstance(obj, bytes):
|
||||||
# Best-effort for binary blobs. See #4187.
|
# Best-effort for binary blobs. See #4187.
|
||||||
return obj.decode('utf-8')
|
return obj.decode('utf-8')
|
||||||
elif hasattr(obj, 'tolist'):
|
elif hasattr(obj, 'tolist'):
|
||||||
|
|
|
@ -100,7 +100,8 @@ def get_field_kwargs(field_name, model_field):
|
||||||
if model_field.has_default() or model_field.blank or model_field.null:
|
if model_field.has_default() or model_field.blank or model_field.null:
|
||||||
kwargs['required'] = False
|
kwargs['required'] = False
|
||||||
|
|
||||||
if model_field.null and not isinstance(model_field, models.NullBooleanField):
|
is_null_boolean_field = isinstance(model_field, models.NullBooleanField)
|
||||||
|
if (model_field.null and not is_null_boolean_field) or (model_field.choices and is_null_boolean_field):
|
||||||
kwargs['allow_null'] = True
|
kwargs['allow_null'] = True
|
||||||
|
|
||||||
if model_field.blank and (isinstance(model_field, models.CharField) or
|
if model_field.blank and (isinstance(model_field, models.CharField) or
|
||||||
|
|
|
@ -23,7 +23,7 @@ from rest_framework.utils import formatting
|
||||||
|
|
||||||
def get_view_name(view):
|
def get_view_name(view):
|
||||||
"""
|
"""
|
||||||
Given a view class, return a textual name to represent the view.
|
Given a view instance, return a textual name to represent the view.
|
||||||
This name is used in the browsable API, and in OPTIONS responses.
|
This name is used in the browsable API, and in OPTIONS responses.
|
||||||
|
|
||||||
This function is the default for the `VIEW_NAME_FUNCTION` setting.
|
This function is the default for the `VIEW_NAME_FUNCTION` setting.
|
||||||
|
@ -48,7 +48,7 @@ def get_view_name(view):
|
||||||
|
|
||||||
def get_view_description(view, html=False):
|
def get_view_description(view, html=False):
|
||||||
"""
|
"""
|
||||||
Given a view class, return a textual description to represent the view.
|
Given a view instance, return a textual description to represent the view.
|
||||||
This name is used in the browsable API, and in OPTIONS responses.
|
This name is used in the browsable API, and in OPTIONS responses.
|
||||||
|
|
||||||
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
|
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
|
||||||
|
|
|
@ -183,7 +183,8 @@ class ViewSetMixin(object):
|
||||||
try:
|
try:
|
||||||
url_name = '%s-%s' % (self.basename, action.url_name)
|
url_name = '%s-%s' % (self.basename, action.url_name)
|
||||||
url = reverse(url_name, self.args, self.kwargs, request=self.request)
|
url = reverse(url_name, self.args, self.kwargs, request=self.request)
|
||||||
action_urls[action.name] = url
|
view = self.__class__(**action.kwargs)
|
||||||
|
action_urls[view.get_view_name()] = url
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
pass # URL requires additional arguments, ignore
|
pass # URL requires additional arguments, ignore
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -42,7 +42,7 @@ if sys.argv[-1] == 'publish':
|
||||||
setup(
|
setup(
|
||||||
name='djangorestframework',
|
name='djangorestframework',
|
||||||
version=version,
|
version=version,
|
||||||
url='http://www.django-rest-framework.org',
|
url='https://www.django-rest-framework.org/',
|
||||||
license='BSD',
|
license='BSD',
|
||||||
description='Web APIs for Django, made easy.',
|
description='Web APIs for Django, made easy.',
|
||||||
long_description=read('README.md'),
|
long_description=read('README.md'),
|
||||||
|
|
|
@ -179,7 +179,6 @@ class ActionDecoratorTestCase(TestCase):
|
||||||
|
|
||||||
assert test_action.mapping == {'get': 'test_action'}
|
assert test_action.mapping == {'get': 'test_action'}
|
||||||
assert test_action.detail is True
|
assert test_action.detail is True
|
||||||
assert test_action.name == 'Test action'
|
|
||||||
assert test_action.url_path == 'test_action'
|
assert test_action.url_path == 'test_action'
|
||||||
assert test_action.url_name == 'test-action'
|
assert test_action.url_name == 'test-action'
|
||||||
assert test_action.kwargs == {
|
assert test_action.kwargs == {
|
||||||
|
@ -213,6 +212,47 @@ class ActionDecoratorTestCase(TestCase):
|
||||||
for name in APIView.http_method_names:
|
for name in APIView.http_method_names:
|
||||||
assert test_action.mapping[name] == name
|
assert test_action.mapping[name] == name
|
||||||
|
|
||||||
|
def test_view_name_kwargs(self):
|
||||||
|
"""
|
||||||
|
'name' and 'suffix' are mutually exclusive kwargs used for generating
|
||||||
|
a view's display name.
|
||||||
|
"""
|
||||||
|
# by default, generate name from method
|
||||||
|
@action(detail=True)
|
||||||
|
def test_action(request):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
assert test_action.kwargs == {
|
||||||
|
'description': None,
|
||||||
|
'name': 'Test action',
|
||||||
|
}
|
||||||
|
|
||||||
|
# name kwarg supersedes name generation
|
||||||
|
@action(detail=True, name='test name')
|
||||||
|
def test_action(request):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
assert test_action.kwargs == {
|
||||||
|
'description': None,
|
||||||
|
'name': 'test name',
|
||||||
|
}
|
||||||
|
|
||||||
|
# suffix kwarg supersedes name generation
|
||||||
|
@action(detail=True, suffix='Suffix')
|
||||||
|
def test_action(request):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
assert test_action.kwargs == {
|
||||||
|
'description': None,
|
||||||
|
'suffix': 'Suffix',
|
||||||
|
}
|
||||||
|
|
||||||
|
# name + suffix is a conflict.
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
action(detail=True, name='test name', suffix='Suffix')
|
||||||
|
|
||||||
|
assert str(excinfo.value) == "`name` and `suffix` are mutually exclusive arguments."
|
||||||
|
|
||||||
def test_method_mapping(self):
|
def test_method_mapping(self):
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def test_action(request):
|
def test_action(request):
|
||||||
|
@ -223,7 +263,7 @@ class ActionDecoratorTestCase(TestCase):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# The secondary handler methods should not have the action attributes
|
# The secondary handler methods should not have the action attributes
|
||||||
for name in ['mapping', 'detail', 'name', 'url_path', 'url_name', 'kwargs']:
|
for name in ['mapping', 'detail', 'url_path', 'url_name', 'kwargs']:
|
||||||
assert hasattr(test_action, name) and not hasattr(test_action_post, name)
|
assert hasattr(test_action, name) and not hasattr(test_action_post, name)
|
||||||
|
|
||||||
def test_method_mapping_already_mapped(self):
|
def test_method_mapping_already_mapped(self):
|
||||||
|
@ -250,34 +290,34 @@ class ActionDecoratorTestCase(TestCase):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def test_detail_route_deprecation(self):
|
def test_detail_route_deprecation(self):
|
||||||
with pytest.warns(PendingDeprecationWarning) as record:
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
@detail_route()
|
@detail_route()
|
||||||
def view(request):
|
def view(request):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
assert len(record) == 1
|
assert len(record) == 1
|
||||||
assert str(record[0].message) == (
|
assert str(record[0].message) == (
|
||||||
"`detail_route` is pending deprecation and will be removed in "
|
"`detail_route` is deprecated and will be removed in "
|
||||||
"3.10 in favor of `action`, which accepts a `detail` bool. Use "
|
"3.10 in favor of `action`, which accepts a `detail` bool. Use "
|
||||||
"`@action(detail=True)` instead."
|
"`@action(detail=True)` instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_route_deprecation(self):
|
def test_list_route_deprecation(self):
|
||||||
with pytest.warns(PendingDeprecationWarning) as record:
|
with pytest.warns(DeprecationWarning) as record:
|
||||||
@list_route()
|
@list_route()
|
||||||
def view(request):
|
def view(request):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
assert len(record) == 1
|
assert len(record) == 1
|
||||||
assert str(record[0].message) == (
|
assert str(record[0].message) == (
|
||||||
"`list_route` is pending deprecation and will be removed in "
|
"`list_route` is deprecated and will be removed in "
|
||||||
"3.10 in favor of `action`, which accepts a `detail` bool. Use "
|
"3.10 in favor of `action`, which accepts a `detail` bool. Use "
|
||||||
"`@action(detail=False)` instead."
|
"`@action(detail=False)` instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_route_url_name_from_path(self):
|
def test_route_url_name_from_path(self):
|
||||||
# pre-3.8 behavior was to base the `url_name` off of the `url_path`
|
# pre-3.8 behavior was to base the `url_name` off of the `url_path`
|
||||||
with pytest.warns(PendingDeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
@list_route(url_path='foo_bar')
|
@list_route(url_path='foo_bar')
|
||||||
def view(request):
|
def view(request):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -15,6 +15,7 @@ from django.utils.timezone import activate, deactivate, override, utc
|
||||||
|
|
||||||
import rest_framework
|
import rest_framework
|
||||||
from rest_framework import exceptions, serializers
|
from rest_framework import exceptions, serializers
|
||||||
|
from rest_framework.compat import ProhibitNullCharactersValidator
|
||||||
from rest_framework.fields import DjangoImageField, is_simple_callable
|
from rest_framework.fields import DjangoImageField, is_simple_callable
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -657,7 +658,7 @@ class TestBooleanField(FieldValues):
|
||||||
|
|
||||||
class TestNullBooleanField(TestBooleanField):
|
class TestNullBooleanField(TestBooleanField):
|
||||||
"""
|
"""
|
||||||
Valid and invalid values for `BooleanField`.
|
Valid and invalid values for `NullBooleanField`.
|
||||||
"""
|
"""
|
||||||
valid_inputs = {
|
valid_inputs = {
|
||||||
'true': True,
|
'true': True,
|
||||||
|
@ -682,6 +683,16 @@ class TestNullBooleanField(TestBooleanField):
|
||||||
field = serializers.NullBooleanField()
|
field = serializers.NullBooleanField()
|
||||||
|
|
||||||
|
|
||||||
|
class TestNullableBooleanField(TestNullBooleanField):
|
||||||
|
"""
|
||||||
|
Valid and invalid values for `BooleanField` when `allow_null=True`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def field(self):
|
||||||
|
return serializers.BooleanField(allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
# String types...
|
# String types...
|
||||||
|
|
||||||
class TestCharField(FieldValues):
|
class TestCharField(FieldValues):
|
||||||
|
@ -718,6 +729,17 @@ class TestCharField(FieldValues):
|
||||||
field.run_validation(' ')
|
field.run_validation(' ')
|
||||||
assert exc_info.value.detail == ['This field may not be blank.']
|
assert exc_info.value.detail == ['This field may not be blank.']
|
||||||
|
|
||||||
|
@pytest.mark.skipif(ProhibitNullCharactersValidator is None, reason="Skipped on Django < 2.0")
|
||||||
|
def test_null_bytes(self):
|
||||||
|
field = serializers.CharField()
|
||||||
|
|
||||||
|
for value in ('\0', 'foo\0', '\0foo', 'foo\0foo'):
|
||||||
|
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||||
|
field.run_validation(value)
|
||||||
|
assert exc_info.value.detail == [
|
||||||
|
'Null characters are not allowed.'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestEmailField(FieldValues):
|
class TestEmailField(FieldValues):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,8 +9,10 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import django
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
|
@ -219,6 +221,25 @@ class TestRegularFieldMappings(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(unicode_repr(TestSerializer()), expected)
|
self.assertEqual(unicode_repr(TestSerializer()), expected)
|
||||||
|
|
||||||
|
# merge this into test_regular_fields / RegularFieldsModel when
|
||||||
|
# Django 2.1 is the minimum supported version
|
||||||
|
@pytest.mark.skipif(django.VERSION < (2, 1), reason='Django version < 2.1')
|
||||||
|
def test_nullable_boolean_field(self):
|
||||||
|
class NullableBooleanModel(models.Model):
|
||||||
|
field = models.BooleanField(null=True, default=False)
|
||||||
|
|
||||||
|
class NullableBooleanSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = NullableBooleanModel
|
||||||
|
fields = ['field']
|
||||||
|
|
||||||
|
expected = dedent("""
|
||||||
|
NullableBooleanSerializer():
|
||||||
|
field = BooleanField(allow_null=True, required=False)
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(unicode_repr(NullableBooleanSerializer()), expected)
|
||||||
|
|
||||||
def test_method_field(self):
|
def test_method_field(self):
|
||||||
"""
|
"""
|
||||||
Properties and methods on the model should be allowed as `Meta.fields`
|
Properties and methods on the model should be allowed as `Meta.fields`
|
||||||
|
@ -343,13 +364,14 @@ class TestRegularFieldMappings(TestCase):
|
||||||
ExampleSerializer()
|
ExampleSerializer()
|
||||||
|
|
||||||
def test_null_boolean_field_choices(self):
|
def test_null_boolean_field_choices(self):
|
||||||
CHECKLIST_OPTIONS = [
|
|
||||||
(None, 'none'),
|
|
||||||
(True, 'checked'),
|
|
||||||
(False, 'N/A'),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Trivial(models.Model):
|
class Trivial(models.Model):
|
||||||
|
CHECKLIST_OPTIONS = (
|
||||||
|
(None, 'none'),
|
||||||
|
(True, 'checked'),
|
||||||
|
(False, 'N/A'),
|
||||||
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
theoretically_nullable_field = models.NullBooleanField(choices=CHECKLIST_OPTIONS)
|
theoretically_nullable_field = models.NullBooleanField(choices=CHECKLIST_OPTIONS)
|
||||||
|
|
||||||
|
@ -359,9 +381,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
trivial_serialized = TrivialSerializer(data=dict(name='test', theoretically_nullable_field=None))
|
trivial_serialized = TrivialSerializer(data=dict(name='test', theoretically_nullable_field=None))
|
||||||
|
|
||||||
self.assertTrue(trivial_serialized.is_valid())
|
self.assertTrue(trivial_serialized.is_valid())
|
||||||
|
|
||||||
self.assertEqual(trivial_serialized.errors, {})
|
self.assertEqual(trivial_serialized.errors, {})
|
||||||
|
|
||||||
|
|
||||||
|
@ -403,6 +423,10 @@ class TestDurationFieldMapping(TestCase):
|
||||||
TestSerializer():
|
TestSerializer():
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField(label='ID', read_only=True)
|
||||||
duration_field = DurationField(max_value=datetime.timedelta(3), min_value=datetime.timedelta(1))
|
duration_field = DurationField(max_value=datetime.timedelta(3), min_value=datetime.timedelta(1))
|
||||||
|
""") if sys.version_info < (3, 7) else dedent("""
|
||||||
|
TestSerializer():
|
||||||
|
id = IntegerField(label='ID', read_only=True)
|
||||||
|
duration_field = DurationField(max_value=datetime.timedelta(days=3), min_value=datetime.timedelta(days=1))
|
||||||
""")
|
""")
|
||||||
self.assertEqual(unicode_repr(TestSerializer()), expected)
|
self.assertEqual(unicode_repr(TestSerializer()), expected)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
import math
|
import math
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -10,7 +11,7 @@ from django.core.files.uploadhandler import (
|
||||||
)
|
)
|
||||||
from django.http.request import RawPostDataException
|
from django.http.request import RawPostDataException
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.six import BytesIO, StringIO
|
from django.utils.six import StringIO
|
||||||
|
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.parsers import (
|
from rest_framework.parsers import (
|
||||||
|
@ -43,7 +44,7 @@ class TestFileUploadParser(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
class MockRequest(object):
|
class MockRequest(object):
|
||||||
pass
|
pass
|
||||||
self.stream = BytesIO(
|
self.stream = io.BytesIO(
|
||||||
"Test text file".encode('utf-8')
|
"Test text file".encode('utf-8')
|
||||||
)
|
)
|
||||||
request = MockRequest()
|
request = MockRequest()
|
||||||
|
@ -131,7 +132,7 @@ class TestFileUploadParser(TestCase):
|
||||||
|
|
||||||
class TestJSONParser(TestCase):
|
class TestJSONParser(TestCase):
|
||||||
def bytes(self, value):
|
def bytes(self, value):
|
||||||
return BytesIO(value.encode('utf-8'))
|
return io.BytesIO(value.encode('utf-8'))
|
||||||
|
|
||||||
def test_float_strictness(self):
|
def test_float_strictness(self):
|
||||||
parser = JSONParser()
|
parser = JSONParser()
|
||||||
|
|
|
@ -2,9 +2,10 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import unittest
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.contrib.auth.models import Group, Permission, User
|
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import ResolverMatch
|
from django.urls import ResolverMatch
|
||||||
|
@ -417,17 +418,34 @@ class ObjectPermissionsIntegrationTests(TestCase):
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
# Read list
|
# Read list
|
||||||
|
def test_django_object_permissions_filter_deprecated(self):
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
DjangoObjectPermissionsFilter()
|
||||||
|
|
||||||
|
message = ("`DjangoObjectPermissionsFilter` has been deprecated and moved "
|
||||||
|
"to the 3rd-party django-rest-framework-guardian package.")
|
||||||
|
self.assertEqual(len(w), 1)
|
||||||
|
self.assertIs(w[-1].category, DeprecationWarning)
|
||||||
|
self.assertEqual(str(w[-1].message), message)
|
||||||
|
|
||||||
def test_can_read_list_permissions(self):
|
def test_can_read_list_permissions(self):
|
||||||
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
||||||
object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
|
object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
|
||||||
response = object_permissions_list_view(request)
|
# TODO: remove in version 3.10
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
response = object_permissions_list_view(request)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data[0].get('id'), 1)
|
self.assertEqual(response.data[0].get('id'), 1)
|
||||||
|
|
||||||
def test_cannot_read_list_permissions(self):
|
def test_cannot_read_list_permissions(self):
|
||||||
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly'])
|
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly'])
|
||||||
object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
|
object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
|
||||||
response = object_permissions_list_view(request)
|
# TODO: remove in version 3.10
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
response = object_permissions_list_view(request)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertListEqual(response.data, [])
|
self.assertListEqual(response.data, [])
|
||||||
|
|
||||||
|
@ -522,3 +540,52 @@ class CustomPermissionsTests(TestCase):
|
||||||
detail = response.data.get('detail')
|
detail = response.data.get('detail')
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertEqual(detail, self.custom_message)
|
self.assertEqual(detail, self.custom_message)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsCompositionTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.username = 'john'
|
||||||
|
self.email = 'lennon@thebeatles.com'
|
||||||
|
self.password = 'password'
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
self.username,
|
||||||
|
self.email,
|
||||||
|
self.password
|
||||||
|
)
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
|
def test_and_false(self):
|
||||||
|
request = factory.get('/1', format='json')
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
||||||
|
assert composed_perm().has_permission(request, None) is False
|
||||||
|
|
||||||
|
def test_and_true(self):
|
||||||
|
request = factory.get('/1', format='json')
|
||||||
|
request.user = self.user
|
||||||
|
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
||||||
|
assert composed_perm().has_permission(request, None) is True
|
||||||
|
|
||||||
|
def test_or_false(self):
|
||||||
|
request = factory.get('/1', format='json')
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
||||||
|
assert composed_perm().has_permission(request, None) is True
|
||||||
|
|
||||||
|
def test_or_true(self):
|
||||||
|
request = factory.get('/1', format='json')
|
||||||
|
request.user = self.user
|
||||||
|
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
||||||
|
assert composed_perm().has_permission(request, None) is True
|
||||||
|
|
||||||
|
def test_several_levels(self):
|
||||||
|
request = factory.get('/1', format='json')
|
||||||
|
request.user = self.user
|
||||||
|
composed_perm = (
|
||||||
|
permissions.IsAuthenticated &
|
||||||
|
permissions.IsAuthenticated &
|
||||||
|
permissions.IsAuthenticated &
|
||||||
|
permissions.IsAuthenticated
|
||||||
|
)
|
||||||
|
assert composed_perm().has_permission(request, None) is True
|
||||||
|
|
|
@ -495,18 +495,18 @@ class TestBaseNameRename(TestCase):
|
||||||
warnings.simplefilter('always')
|
warnings.simplefilter('always')
|
||||||
router.register('mock', MockViewSet, 'mock', base_name='mock')
|
router.register('mock', MockViewSet, 'mock', base_name='mock')
|
||||||
|
|
||||||
msg = "The `base_name` argument has been deprecated in favor of `basename`."
|
msg = "The `base_name` argument is pending deprecation in favor of `basename`."
|
||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert str(w[0].message) == msg
|
assert str(w[0].message) == msg
|
||||||
|
|
||||||
def test_base_name_argument_deprecation(self):
|
def test_base_name_argument_deprecation(self):
|
||||||
router = SimpleRouter()
|
router = SimpleRouter()
|
||||||
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
with pytest.warns(PendingDeprecationWarning) as w:
|
||||||
warnings.simplefilter('always')
|
warnings.simplefilter('always')
|
||||||
router.register('mock', MockViewSet, base_name='mock')
|
router.register('mock', MockViewSet, base_name='mock')
|
||||||
|
|
||||||
msg = "The `base_name` argument has been deprecated in favor of `basename`."
|
msg = "The `base_name` argument is pending deprecation in favor of `basename`."
|
||||||
assert len(w) == 1
|
assert len(w) == 1
|
||||||
assert str(w[0].message) == msg
|
assert str(w[0].message) == msg
|
||||||
assert router.registry == [
|
assert router.registry == [
|
||||||
|
@ -529,7 +529,7 @@ class TestBaseNameRename(TestCase):
|
||||||
msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`."
|
msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`."
|
||||||
|
|
||||||
# Class definition should raise a warning
|
# Class definition should raise a warning
|
||||||
with warnings.catch_warnings(record=True) as w:
|
with pytest.warns(PendingDeprecationWarning) as w:
|
||||||
warnings.simplefilter('always')
|
warnings.simplefilter('always')
|
||||||
|
|
||||||
class CustomRouter(SimpleRouter):
|
class CustomRouter(SimpleRouter):
|
||||||
|
|
|
@ -114,6 +114,24 @@ class ExampleViewSet(ModelViewSet):
|
||||||
assert self.action
|
assert self.action
|
||||||
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
|
@action(methods=['get', 'post'], detail=False)
|
||||||
|
def documented_custom_action(self, request):
|
||||||
|
"""
|
||||||
|
get:
|
||||||
|
A description of the get method on the custom action.
|
||||||
|
|
||||||
|
post:
|
||||||
|
A description of the post method on the custom action.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@documented_custom_action.mapping.put
|
||||||
|
def put_documented_custom_action(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
A description of the put method on the custom action from mapping.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if coreapi:
|
if coreapi:
|
||||||
schema_view = get_schema_view(title='Example API')
|
schema_view = get_schema_view(title='Example API')
|
||||||
|
@ -161,6 +179,13 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
description='Custom description.',
|
description='Custom description.',
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'documented_custom_action': {
|
||||||
|
'read': coreapi.Link(
|
||||||
|
url='/example/documented_custom_action/',
|
||||||
|
action='get',
|
||||||
|
description='A description of the get method on the custom action.',
|
||||||
|
)
|
||||||
|
},
|
||||||
'read': coreapi.Link(
|
'read': coreapi.Link(
|
||||||
url='/example/{id}/',
|
url='/example/{id}/',
|
||||||
action='get',
|
action='get',
|
||||||
|
@ -263,6 +288,33 @@ class TestRouterGeneratedSchema(TestCase):
|
||||||
description='Deletion description.',
|
description='Deletion description.',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
'documented_custom_action': {
|
||||||
|
'read': coreapi.Link(
|
||||||
|
url='/example/documented_custom_action/',
|
||||||
|
action='get',
|
||||||
|
description='A description of the get method on the custom action.',
|
||||||
|
),
|
||||||
|
'create': coreapi.Link(
|
||||||
|
url='/example/documented_custom_action/',
|
||||||
|
action='post',
|
||||||
|
description='A description of the post method on the custom action.',
|
||||||
|
encoding='application/json',
|
||||||
|
fields=[
|
||||||
|
coreapi.Field('a', required=True, location='form', schema=coreschema.String(title='A', description='A field description')),
|
||||||
|
coreapi.Field('b', required=False, location='form', schema=coreschema.String(title='B'))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'update': coreapi.Link(
|
||||||
|
url='/example/documented_custom_action/',
|
||||||
|
action='put',
|
||||||
|
description='A description of the put method on the custom action from mapping.',
|
||||||
|
encoding='application/json',
|
||||||
|
fields=[
|
||||||
|
coreapi.Field('a', required=True, location='form', schema=coreschema.String(title='A', description='A field description')),
|
||||||
|
coreapi.Field('b', required=False, location='form', schema=coreschema.String(title='B'))
|
||||||
|
]
|
||||||
|
),
|
||||||
|
},
|
||||||
'update': coreapi.Link(
|
'update': coreapi.Link(
|
||||||
url='/example/{id}/',
|
url='/example/{id}/',
|
||||||
action='put',
|
action='put',
|
||||||
|
@ -548,6 +600,13 @@ class TestSchemaGeneratorWithMethodLimitedViewSets(TestCase):
|
||||||
description='Custom description.',
|
description='Custom description.',
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'documented_custom_action': {
|
||||||
|
'read': coreapi.Link(
|
||||||
|
url='/example1/documented_custom_action/',
|
||||||
|
action='get',
|
||||||
|
description='A description of the get method on the custom action.',
|
||||||
|
),
|
||||||
|
},
|
||||||
'read': coreapi.Link(
|
'read': coreapi.Link(
|
||||||
url='/example1/{id}/',
|
url='/example1/{id}/',
|
||||||
action='get',
|
action='get',
|
||||||
|
@ -973,38 +1032,6 @@ class SchemaGenerationExclusionTests(TestCase):
|
||||||
|
|
||||||
assert should_include == expected
|
assert should_include == expected
|
||||||
|
|
||||||
def test_deprecations(self):
|
|
||||||
with pytest.warns(DeprecationWarning) as record:
|
|
||||||
@api_view(["GET"], exclude_from_schema=True)
|
|
||||||
def view(request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert len(record) == 1
|
|
||||||
assert str(record[0].message) == (
|
|
||||||
"The `exclude_from_schema` argument to `api_view` is deprecated. "
|
|
||||||
"Use the `schema` decorator instead, passing `None`."
|
|
||||||
)
|
|
||||||
|
|
||||||
class OldFashionedExcludedView(APIView):
|
|
||||||
exclude_from_schema = True
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
patterns = [
|
|
||||||
url('^excluded-old-fashioned/$', OldFashionedExcludedView.as_view()),
|
|
||||||
]
|
|
||||||
|
|
||||||
inspector = EndpointEnumerator(patterns)
|
|
||||||
with pytest.warns(DeprecationWarning) as record:
|
|
||||||
inspector.get_api_endpoints()
|
|
||||||
|
|
||||||
assert len(record) == 1
|
|
||||||
assert str(record[0].message) == (
|
|
||||||
"The `OldFashionedExcludedView.exclude_from_schema` attribute is "
|
|
||||||
"deprecated. Set `schema = None` instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def simple_fbv(request):
|
def simple_fbv(request):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from django.template import Context, Template
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.compat import coreapi, coreschema
|
from rest_framework.compat import coreapi, coreschema
|
||||||
|
@ -304,6 +305,16 @@ class URLizerTests(TestCase):
|
||||||
'"foo_set": [\n "<a href="http://api/foos/1/">http://api/foos/1/</a>"\n], '
|
'"foo_set": [\n "<a href="http://api/foos/1/">http://api/foos/1/</a>"\n], '
|
||||||
self._urlize_dict_check(data)
|
self._urlize_dict_check(data)
|
||||||
|
|
||||||
|
def test_template_render_with_noautoescape(self):
|
||||||
|
"""
|
||||||
|
Test if the autoescape value is getting passed to urlize_quoted_links filter.
|
||||||
|
"""
|
||||||
|
template = Template("{% load rest_framework %}"
|
||||||
|
"{% autoescape off %}{{ content|urlize_quoted_links }}"
|
||||||
|
"{% endautoescape %}")
|
||||||
|
rendered = template.render(Context({'content': '"http://example.com"'}))
|
||||||
|
assert rendered == '"<a href="http://example.com" rel="nofollow">http://example.com</a>"'
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||||
class SchemaLinksTests(TestCase):
|
class SchemaLinksTests(TestCase):
|
||||||
|
|
|
@ -52,6 +52,14 @@ class ResourceViewSet(ModelViewSet):
|
||||||
def detail_action(self, request, *args, **kwargs):
|
def detail_action(self, request, *args, **kwargs):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@action(detail=True, name='Custom Name')
|
||||||
|
def named_action(self, request, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@action(detail=True, suffix='Custom Suffix')
|
||||||
|
def suffixed_action(self, request, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
router = SimpleRouter()
|
router = SimpleRouter()
|
||||||
router.register(r'resources', ResourceViewSet)
|
router.register(r'resources', ResourceViewSet)
|
||||||
|
@ -145,6 +153,24 @@ class BreadcrumbTests(TestCase):
|
||||||
('Detail action', '/resources/1/detail_action/'),
|
('Detail action', '/resources/1/detail_action/'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_modelviewset_action_name_kwarg(self):
|
||||||
|
url = '/resources/1/named_action/'
|
||||||
|
assert get_breadcrumbs(url) == [
|
||||||
|
('Root', '/'),
|
||||||
|
('Resource List', '/resources/'),
|
||||||
|
('Resource Instance', '/resources/1/'),
|
||||||
|
('Custom Name', '/resources/1/named_action/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_modelviewset_action_suffix_kwarg(self):
|
||||||
|
url = '/resources/1/suffixed_action/'
|
||||||
|
assert get_breadcrumbs(url) == [
|
||||||
|
('Root', '/'),
|
||||||
|
('Resource List', '/resources/'),
|
||||||
|
('Resource Instance', '/resources/1/'),
|
||||||
|
('Resource Custom Suffix', '/resources/1/suffixed_action/'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class JsonFloatTests(TestCase):
|
class JsonFloatTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -63,9 +63,28 @@ class ActionViewSet(GenericViewSet):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ActionNamesViewSet(GenericViewSet):
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
@action(detail=True)
|
||||||
|
def unnamed_action(self, request, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@action(detail=True, name='Custom Name')
|
||||||
|
def named_action(self, request, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@action(detail=True, suffix='Custom Suffix')
|
||||||
|
def suffixed_action(self, request, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
router = SimpleRouter()
|
router = SimpleRouter()
|
||||||
router.register(r'actions', ActionViewSet)
|
router.register(r'actions', ActionViewSet)
|
||||||
router.register(r'actions-alt', ActionViewSet, basename='actions-alt')
|
router.register(r'actions-alt', ActionViewSet, basename='actions-alt')
|
||||||
|
router.register(r'names', ActionNamesViewSet, basename='names')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -172,6 +191,19 @@ class GetExtraActionUrlMapTests(TestCase):
|
||||||
def test_uninitialized_view(self):
|
def test_uninitialized_view(self):
|
||||||
self.assertEqual(ActionViewSet().get_extra_action_url_map(), OrderedDict())
|
self.assertEqual(ActionViewSet().get_extra_action_url_map(), OrderedDict())
|
||||||
|
|
||||||
|
def test_action_names(self):
|
||||||
|
# Action 'name' and 'suffix' kwargs should be respected
|
||||||
|
response = self.client.get('/api/names/1/')
|
||||||
|
view = response.renderer_context['view']
|
||||||
|
|
||||||
|
expected = OrderedDict([
|
||||||
|
('Custom Name', 'http://testserver/api/names/1/named_action/'),
|
||||||
|
('Action Names Custom Suffix', 'http://testserver/api/names/1/suffixed_action/'),
|
||||||
|
('Unnamed action', 'http://testserver/api/names/1/unnamed_action/'),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual(view.get_extra_action_url_map(), expected)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='tests.test_viewsets')
|
@override_settings(ROOT_URLCONF='tests.test_viewsets')
|
||||||
class ReverseActionTests(TestCase):
|
class ReverseActionTests(TestCase):
|
||||||
|
|
6
tox.ini
6
tox.ini
|
@ -1,9 +1,9 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py27,py34,py35,py36}-django111,
|
{py27,py34,py35,py36}-django111,
|
||||||
{py34,py35,py36}-django20,
|
{py34,py35,py36,py37}-django20,
|
||||||
{py35,py36}-django21
|
{py35,py36,py37}-django21
|
||||||
{py35,py36}-djangomaster,
|
{py35,py36,py37}-djangomaster,
|
||||||
base,dist,lint,docs,
|
base,dist,lint,docs,
|
||||||
|
|
||||||
[travis:env]
|
[travis:env]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user