Merge branch 'master' into topic/openapi-operation-plural

This commit is contained in:
Shinya Ohyanagi 2022-07-18 10:56:25 +09:00 committed by GitHub
commit bb66cb3c9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 341 additions and 134 deletions

View File

@ -21,14 +21,14 @@ The initial aim is to provide a single full-time position on REST framework.
[![][sentry-img]][sentry-url] [![][sentry-img]][sentry-url]
[![][stream-img]][stream-url] [![][stream-img]][stream-url]
[![][rollbar-img]][rollbar-url] [![][spacinov-img]][spacinov-url]
[![][esg-img]][esg-url]
[![][retool-img]][retool-url] [![][retool-img]][retool-url]
[![][bitio-img]][bitio-url] [![][bitio-img]][bitio-url]
[![][posthog-img]][posthog-url] [![][posthog-img]][posthog-url]
[![][cryptapi-img]][cryptapi-url] [![][cryptapi-img]][cryptapi-url]
[![][fezto-img]][fezto-url]
Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [ESG][esg-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], and [CryptAPI][cryptapi-url]. Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Spacinov][spacinov-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], [CryptAPI][cryptapi-url], and [FEZTO][fezto-url].
--- ---
@ -55,7 +55,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements # Requirements
* Python (3.6, 3.7, 3.8, 3.9, 3.10) * Python (3.6, 3.7, 3.8, 3.9, 3.10)
* Django (2.2, 3.0, 3.1, 3.2, 4.0) * Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1)
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
each Python and Django series. each Python and Django series.
@ -194,21 +194,21 @@ Please see the [security policy][security-policy].
[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
[rollbar-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png [spacinov-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/spacinov-readme.png
[esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png
[retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/retool-readme.png [retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/retool-readme.png
[bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/bitio-readme.png [bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/bitio-readme.png
[posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/posthog-readme.png [posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/posthog-readme.png
[cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png [cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png
[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png
[sentry-url]: https://getsentry.com/welcome/ [sentry-url]: https://getsentry.com/welcome/
[stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage [stream-url]: https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage
[rollbar-url]: https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial [spacinov-url]: https://www.spacinov.com/
[esg-url]: https://software.esg-usa.com/
[retool-url]: https://retool.com/?utm_source=djangorest&utm_medium=sponsorship [retool-url]: https://retool.com/?utm_source=djangorest&utm_medium=sponsorship
[bitio-url]: https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship [bitio-url]: https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship
[posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship [posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship
[cryptapi-url]: https://cryptapi.io [cryptapi-url]: https://cryptapi.io
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
[oauth1-section]: https://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]: https://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

View File

@ -120,6 +120,14 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
## TokenAuthentication ## TokenAuthentication
---
**Note:** The token authentication provided by Django REST framework is a fairly simple implementation.
For an implementation which allows more than one token per user, has some tighter security implementation details, and supports token expiry, please see the [Django REST Knox][django-rest-knox] third party package.
---
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
@ -129,11 +137,9 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica
'rest_framework.authtoken' 'rest_framework.authtoken'
] ]
--- Make sure to run `manage.py migrate` after changing your settings.
**Note:** Make sure to run `manage.py migrate` after changing your settings. The `rest_framework.authtoken` app provides Django database migrations. The `rest_framework.authtoken` app provides Django database migrations.
---
You'll also need to create tokens for your users. You'll also need to create tokens for your users.
@ -146,7 +152,7 @@ For clients to authenticate, the token key should be included in the `Authorizat
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
**Note:** If you want to use a different keyword in the header, such as `Bearer`, simply subclass `TokenAuthentication` and set the `keyword` class variable. *If you want to use a different keyword in the header, such as `Bearer`, simply subclass `TokenAuthentication` and set the `keyword` class variable.*
If successfully authenticated, `TokenAuthentication` provides the following credentials. If successfully authenticated, `TokenAuthentication` provides the following credentials.
@ -355,6 +361,10 @@ The following example will authenticate any incoming request as the user given b
The following third-party packages are also available. The following third-party packages are also available.
## django-rest-knox
[Django-rest-knox][django-rest-knox] library provides models and views to handle token-based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into).
## Django OAuth Toolkit ## Django OAuth Toolkit
The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
@ -424,10 +434,6 @@ There are currently two forks of this project.
[Drf-social-oauth2][drf-social-oauth2] is a framework that helps you authenticate with major social oauth2 vendors, such as Facebook, Google, Twitter, Orcid, etc. It generates tokens in a JWTed way with an easy setup. [Drf-social-oauth2][drf-social-oauth2] is a framework that helps you authenticate with major social oauth2 vendors, such as Facebook, Google, Twitter, Orcid, etc. It generates tokens in a JWTed way with an easy setup.
## django-rest-knox
[Django-rest-knox][django-rest-knox] library provides models and views to handle token-based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into).
## drfpasswordless ## drfpasswordless
[drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number. [drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number.

View File

@ -260,6 +260,15 @@ Set as `handler400`:
handler400 = 'rest_framework.exceptions.bad_request' handler400 = 'rest_framework.exceptions.bad_request'
# Third party packages
The following third-party packages are also available.
## DRF Standardized Errors
The [drf-standardized-errors][drf-standardized-errors] package provides an exception handler that generates the same format for all 4xx and 5xx responses. It is a drop-in replacement for the default exception handler and allows customizing the error response format without rewriting the whole exception handler. The standardized error response format is easier to document and easier to handle by API consumers.
[cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/ [cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/
[authentication]: authentication.md [authentication]: authentication.md
[django-custom-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views [django-custom-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors

View File

@ -42,7 +42,7 @@ Set to false if this field is not required to be present during deserialization.
Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation. Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation.
Defaults to `True`. Defaults to `True`. If you're using [Model Serializer](https://www.django-rest-framework.org/api-guide/serializers/#modelserializer) default value will be `False` if you have specified `blank=True` or `default` or `null=True` at your field in your `Model`.
### `default` ### `default`

View File

@ -338,5 +338,5 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
[drf-extensions-nested-viewsets]: https://chibisov.github.io/drf-extensions/docs/#nested-routes [drf-extensions-nested-viewsets]: https://chibisov.github.io/drf-extensions/docs/#nested-routes
[drf-extensions-collection-level-controllers]: https://chibisov.github.io/drf-extensions/docs/#collection-level-controllers [drf-extensions-collection-level-controllers]: https://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
[drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name [drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
[url-namespace-docs]: https://docs.djangoproject.com/en/1.11/topics/http/urls/#url-namespaces [url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces
[include-api-reference]: https://docs.djangoproject.com/en/2.0/ref/urls/#include [include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include

View File

@ -165,7 +165,7 @@ In order to customize the top-level schema, subclass
as an argument to the `generateschema` command or `get_schema_view()` helper as an argument to the `generateschema` command or `get_schema_view()` helper
function. function.
### get_schema(self, request) ### get_schema(self, request=None, public=False)
Returns a dictionary that represents the OpenAPI schema: Returns a dictionary that represents the OpenAPI schema:
@ -313,6 +313,11 @@ Computes the component's name from the serializer.
You may see warnings if your API has duplicate component names. If so you can override `get_component_name()` or pass the `component_name` `__init__()` kwarg (see below) to provide different names. You may see warnings if your API has duplicate component names. If so you can override `get_component_name()` or pass the `component_name` `__init__()` kwarg (see below) to provide different names.
#### `get_reference()`
Returns a reference to the serializer component. This may be useful if you override `get_schema()`.
#### `map_serializer()` #### `map_serializer()`
Maps serializers to their OpenAPI representations. Maps serializers to their OpenAPI representations.

View File

@ -137,7 +137,7 @@ REST framework continues to be open-source and permissively licensed, but we fir
## What future funding will enable ## What future funding will enable
* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries. * Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries.
* Better authentication defaults, possibly bringing JWT & CORs support into the core package. * Better authentication defaults, possibly bringing JWT & CORS support into the core package.
* Securing the community & operations manager position long-term. * Securing the community & operations manager position long-term.
* Opening up and securing a part-time position to focus on ticket triage and resolution. * Opening up and securing a part-time position to focus on ticket triage and resolution.
* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation. * Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation.

View File

@ -11,7 +11,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
* [https://djangojobs.net/jobs/][django-jobs-net] * [https://djangojobs.net/jobs/][django-jobs-net]
* [https://findwork.dev/django-rest-framework-jobs][findwork-dev] * [https://findwork.dev/django-rest-framework-jobs][findwork-dev]
* [https://www.indeed.com/q-Django-jobs.html][indeed-com] * [https://www.indeed.com/q-Django-jobs.html][indeed-com]
* [https://stackoverflow.com/jobs/developer-jobs-using-django][stackoverflow-com] * [https://stackoverflow.com/jobs/companies?tl=django][stackoverflow-com]
* [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com] * [https://www.upwork.com/o/jobs/browse/skill/django-framework/][upwork-com]
* [https://www.technojobs.co.uk/django-jobs][technobjobs-co-uk] * [https://www.technojobs.co.uk/django-jobs][technobjobs-co-uk]
* [https://remoteok.io/remote-django-jobs][remoteok-io] * [https://remoteok.io/remote-django-jobs][remoteok-io]
@ -29,7 +29,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
[django-jobs-net]: https://djangojobs.net/jobs/ [django-jobs-net]: https://djangojobs.net/jobs/
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs [findwork-dev]: https://findwork.dev/django-rest-framework-jobs
[indeed-com]: https://www.indeed.com/q-Django-jobs.html [indeed-com]: https://www.indeed.com/q-Django-jobs.html
[stackoverflow-com]: https://stackoverflow.com/jobs/developer-jobs-using-django [stackoverflow-com]: https://stackoverflow.com/jobs/companies?tl=django
[upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/ [upwork-com]: https://www.upwork.com/o/jobs/browse/skill/django-framework/
[technobjobs-co-uk]: https://www.technojobs.co.uk/django-jobs [technobjobs-co-uk]: https://www.technojobs.co.uk/django-jobs
[remoteok-io]: https://remoteok.io/remote-django-jobs [remoteok-io]: https://remoteok.io/remote-django-jobs

View File

@ -148,6 +148,8 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons. * [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons.
* [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models.. * [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models..
* [fast-drf] - A model based library for making API development faster and easier. * [fast-drf] - A model based library for making API development faster and easier.
* [django-requestlogs] - Providing middleware and other helpers for audit logging for REST framework.
* [drf-standardized-errors][drf-standardized-errors] - DRF exception handler to standardize error responses for all API endpoints.
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
@ -237,3 +239,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[graphwrap]: https://github.com/PaulGilmartin/graph_wrap [graphwrap]: https://github.com/PaulGilmartin/graph_wrap
[rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions [rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions
[fast-drf]: https://github.com/iashraful/fast-drf [fast-drf]: https://github.com/iashraful/fast-drf
[django-requestlogs]: https://github.com/Raekkeri/django-requestlogs
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -68,16 +68,16 @@ continued development by **[signing up for a paid plan][funding]**.
<ul class="premium-promo promo"> <ul class="premium-promo promo">
<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/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li> <li><a href="https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://software.esg-usa.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/esg-new-logo.png)">ESG</a></li> <li><a href="https://www.spacinov.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/spacinov.png)">Spacinov</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://retool.com/?utm_source=djangorest&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/retool-sidebar.png)">Retool</a></li> <li><a href="https://retool.com/?utm_source=djangorest&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/retool-sidebar.png)">Retool</a></li>
<li><a href="https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/bitio_logo_gold_background.png)">bit.io</a></li> <li><a href="https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/bitio_logo_gold_background.png)">bit.io</a></li>
<li><a href="https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/135996800-d49fe024-32d9-441a-98d9-4c7596287a67.png)">PostHog</a></li> <li><a href="https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/135996800-d49fe024-32d9-441a-98d9-4c7596287a67.png)">PostHog</a></li>
<li><a href="https://cryptapi.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cryptapi.png)">CryptAPI</a></li> <li><a href="https://cryptapi.io" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/cryptapi.png)">CryptAPI</a></li>
<li><a href="https://www.fezto.xyz/?utm_source=DjangoRESTFramework" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/fezto.png)">FEZTO</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, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), and [CryptAPI](https://cryptapi.io).* *Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), and [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework).*
--- ---
@ -86,7 +86,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Python (3.6, 3.7, 3.8, 3.9, 3.10) * Python (3.6, 3.7, 3.8, 3.9, 3.10)
* Django (2.2, 3.0, 3.1, 3.2, 4.0) * Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1)
We **highly recommend** and only officially support the latest patch release of We **highly recommend** and only officially support the latest patch release of
each Python and Django series. each Python and Django series.

View File

@ -112,8 +112,8 @@ Here's our re-wired `snippets/urls.py` file.
# Create a router and register our viewsets with it. # Create a router and register our viewsets with it.
router = DefaultRouter() router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet,basename="snippets") router.register(r'snippets', views.SnippetViewSet,basename="snippet")
router.register(r'users', views.UserViewSet,basename="users") router.register(r'users', views.UserViewSet,basename="user")
# The API URLs are now determined automatically by the router. # The API URLs are now determined automatically by the router.
urlpatterns = [ urlpatterns = [

View File

@ -1,2 +1,3 @@
# MkDocs to build our documentation. # MkDocs to build our documentation.
mkdocs>=1.1.2,<1.2 mkdocs>=1.1.2,<1.2
jinja2>=2.10,<3.1.0 # contextfilter has been renamed

View File

@ -4,8 +4,7 @@ coreschema==0.0.4
django-filter>=2.4.0,<3.0 django-filter>=2.4.0,<3.0
django-guardian>=2.4.0,<2.5 django-guardian>=2.4.0,<2.5
inflection==0.5.1 inflection==0.5.1
markdown==3.3;python_version>="3.6" markdown==3.3
markdown==3.2.2;python_version=="3.5"
psycopg2-binary>=2.8.5,<2.9 psycopg2-binary>=2.8.5,<2.9
pygments>=2.7.1,<2.8 pygments==2.12
pyyaml>=5.3.1,<5.4 pyyaml>=5.3.1,<5.4

View File

@ -2,6 +2,7 @@
The `compat` module provides support for backwards compatibility with older The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages. versions of Django/Python, and compatibility wrappers around optional packages.
""" """
import django
from django.conf import settings from django.conf import settings
from django.views.generic import View from django.views.generic import View
@ -152,6 +153,30 @@ else:
return False return False
if django.VERSION >= (4, 2):
# Django 4.2+: use the stock parse_header_parameters function
# Note: Django 4.1 also has an implementation of parse_header_parameters
# which is slightly different from the one in 4.2, it needs
# the compatibility shim as well.
from django.utils.http import parse_header_parameters
else:
# Django <= 4.1: create a compatibility shim for parse_header_parameters
from django.http.multipartparser import parse_header
def parse_header_parameters(line):
# parse_header works with bytes, but parse_header_parameters
# works with strings. Call encode to convert the line to bytes.
main_value_pair, params = parse_header(line.encode())
return main_value_pair, {
# parse_header will convert *some* values to string.
# parse_header_parameters converts *all* values to string.
# Make sure all values are converted by calling decode on
# any remaining non-string values.
k: v if isinstance(v, str) else v.decode()
for k, v in params.items()
}
# `separators` argument to `json.dumps()` differs between 2.x and 3.x # `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767 # See: https://bugs.python.org/issue22767
SHORT_SEPARATORS = (',', ':') SHORT_SEPARATORS = (',', ':')

View File

@ -27,7 +27,6 @@ from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_str from django.utils.encoding import is_protected_type, smart_str
from django.utils.formats import localize_input, sanitize_separators from django.utils.formats import localize_input, sanitize_separators
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.timezone import utc
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pytz.exceptions import InvalidTimeError from pytz.exceptions import InvalidTimeError
@ -63,6 +62,9 @@ def is_simple_callable(obj):
""" """
True if the object is a callable that takes no arguments. True if the object is a callable that takes no arguments.
""" """
if not callable(obj):
return False
# Bail early since we cannot inspect built-in function signatures. # Bail early since we cannot inspect built-in function signatures.
if inspect.isbuiltin(obj): if inspect.isbuiltin(obj):
raise BuiltinSignatureError( raise BuiltinSignatureError(
@ -1177,7 +1179,7 @@ class DateTimeField(Field):
When `self.default_timezone` is `None`, always return naive datetimes. When `self.default_timezone` is `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware datetimes. When `self.default_timezone` is not `None`, always return aware datetimes.
""" """
field_timezone = getattr(self, 'timezone', self.default_timezone()) field_timezone = self.timezone if hasattr(self, 'timezone') else self.default_timezone()
if field_timezone is not None: if field_timezone is not None:
if timezone.is_aware(value): if timezone.is_aware(value):
@ -1190,7 +1192,7 @@ class DateTimeField(Field):
except InvalidTimeError: except InvalidTimeError:
self.fail('make_aware', timezone=field_timezone) self.fail('make_aware', timezone=field_timezone)
elif (field_timezone is None) and timezone.is_aware(value): elif (field_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, utc) return timezone.make_naive(value, datetime.timezone.utc)
return value return value
def default_timezone(self): def default_timezone(self):

View File

@ -4,7 +4,7 @@ incoming request. Typically this will be based on the request's Accept header.
""" """
from django.http import Http404 from django.http import Http404
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import exceptions
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import ( from rest_framework.utils.mediatypes import (
_MediaType, media_type_matches, order_by_precedence _MediaType, media_type_matches, order_by_precedence
@ -64,9 +64,11 @@ class DefaultContentNegotiation(BaseContentNegotiation):
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
full_media_type = ';'.join( full_media_type = ';'.join(
(renderer.media_type,) + (renderer.media_type,) +
tuple('{}={}'.format( tuple(
key, value.decode(HTTP_HEADER_ENCODING)) '{}={}'.format(key, value)
for key, value in media_type_wrapper.params.items())) for key, value in media_type_wrapper.params.items()
)
)
return renderer, full_media_type return renderer, full_media_type
else: else:
# Eg client requests 'application/json; indent=8' # Eg client requests 'application/json; indent=8'

View File

@ -5,7 +5,6 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data. on the request, such as form content or json encoded data.
""" """
import codecs import codecs
from urllib import parse
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
@ -13,10 +12,10 @@ from django.http import QueryDict
from django.http.multipartparser import ChunkIter from django.http.multipartparser import ChunkIter
from django.http.multipartparser import \ from django.http.multipartparser import \
MultiPartParser as DjangoMultiPartParser MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header from django.http.multipartparser import MultiPartParserError
from django.utils.encoding import force_str
from rest_framework import renderers from rest_framework import renderers
from rest_framework.compat import parse_header_parameters
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils import json from rest_framework.utils import json
@ -201,23 +200,10 @@ class FileUploadParser(BaseParser):
try: try:
meta = parser_context['request'].META meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode()) disposition, params = parse_header_parameters(meta['HTTP_CONTENT_DISPOSITION'])
filename_parm = disposition[1] if 'filename*' in params:
if 'filename*' in filename_parm: return params['filename*']
return self.get_encoded_filename(filename_parm) else:
return force_str(filename_parm['filename']) return params['filename']
except (AttributeError, KeyError, ValueError): except (AttributeError, KeyError, ValueError):
pass pass
def get_encoded_filename(self, filename_parm):
"""
Handle encoded filenames per RFC6266. See also:
https://tools.ietf.org/html/rfc2231#section-4
"""
encoded_filename = force_str(filename_parm['filename*'])
try:
charset, lang, filename = encoded_filename.split('\'', 2)
filename = parse.unquote(filename)
except (ValueError, LookupError):
filename = force_str(filename_parm['filename'])
return filename

View File

@ -10,7 +10,7 @@ from django.utils.encoding import smart_str, uri_to_iri
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.fields import ( from rest_framework.fields import (
Field, empty, get_attribute, is_simple_callable, iter_options Field, SkipField, empty, get_attribute, is_simple_callable, iter_options
) )
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -535,7 +535,30 @@ class ManyRelatedField(Field):
if hasattr(instance, 'pk') and instance.pk is None: if hasattr(instance, 'pk') and instance.pk is None:
return [] return []
try:
relationship = get_attribute(instance, self.source_attrs) relationship = get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc:
if self.default is not empty:
return self.get_default()
if self.allow_null:
return None
if not self.required:
raise SkipField()
msg = (
'Got {exc_type} when attempting to get a value for field '
'`{field}` on serializer `{serializer}`.\nThe serializer '
'field might be named incorrectly and not match '
'any attribute or key on the `{instance}` instance.\n'
'Original exception text was: {exc}.'.format(
exc_type=type(exc).__name__,
field=self.field_name,
serializer=self.parent.__class__.__name__,
instance=instance.__class__.__name__,
exc=exc
)
)
raise type(exc)(msg)
return relationship.all() if hasattr(relationship, 'all') else relationship return relationship.all() if hasattr(relationship, 'all') else relationship
def to_representation(self, iterable): def to_representation(self, iterable):

View File

@ -14,7 +14,6 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Page from django.core.paginator import Page
from django.http.multipartparser import parse_header
from django.template import engines, loader from django.template import engines, loader
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
from django.utils.html import mark_safe from django.utils.html import mark_safe
@ -22,7 +21,7 @@ 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, coreschema, INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
pygments_css, yaml parse_header_parameters, pygments_css, 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
@ -72,7 +71,7 @@ class JSONRenderer(BaseRenderer):
# If the media type looks like 'application/json; indent=4', # If the media type looks like 'application/json; indent=4',
# then pretty print the result. # then pretty print the result.
# Note that we coerce `indent=0` into `indent=None`. # Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii')) base_media_type, params = parse_header_parameters(accepted_media_type)
try: try:
return zero_as_none(max(min(int(params['indent']), 8), 0)) return zero_as_none(max(min(int(params['indent']), 8), 0))
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):

View File

@ -14,11 +14,11 @@ from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.http.multipartparser import parse_header
from django.http.request import RawPostDataException from django.http.request import RawPostDataException
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework import exceptions
from rest_framework.compat import parse_header_parameters
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
@ -26,7 +26,7 @@ def is_form_media_type(media_type):
""" """
Return True if the media type is a valid form media type. Return True if the media type is a valid form media type.
""" """
base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) base_media_type, params = parse_header_parameters(media_type)
return (base_media_type == 'application/x-www-form-urlencoded' or return (base_media_type == 'application/x-www-form-urlencoded' or
base_media_type == 'multipart/form-data') base_media_type == 'multipart/form-data')

View File

@ -637,7 +637,7 @@ class AutoSchema(ViewInspector):
""" """
return self.get_serializer(path, method) return self.get_serializer(path, method)
def _get_reference(self, serializer): def get_reference(self, serializer):
return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))} return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
def get_request_body(self, path, method): def get_request_body(self, path, method):
@ -651,7 +651,7 @@ class AutoSchema(ViewInspector):
if not isinstance(serializer, serializers.Serializer): if not isinstance(serializer, serializers.Serializer):
item_schema = {} item_schema = {}
else: else:
item_schema = self._get_reference(serializer) item_schema = self.get_reference(serializer)
return { return {
'content': { 'content': {
@ -675,7 +675,7 @@ class AutoSchema(ViewInspector):
if not isinstance(serializer, serializers.Serializer): if not isinstance(serializer, serializers.Serializer):
item_schema = {} item_schema = {}
else: else:
item_schema = self._get_reference(serializer) item_schema = self.get_reference(serializer)
if is_list_view(path, method, self.view): if is_list_view(path, method, self.view):
response_schema = { response_schema = {
@ -809,3 +809,11 @@ class AutoSchema(ViewInspector):
RemovedInDRF314Warning, stacklevel=2 RemovedInDRF314Warning, stacklevel=2
) )
return self.allows_filters(path, method) return self.allows_filters(path, method)
def _get_reference(self, serializer):
warnings.warn(
"Method `_get_reference()` has been renamed to `get_reference()`. "
"The old name will be removed in DRF v3.14.",
RemovedInDRF314Warning, stacklevel=2
)
return self.get_reference(serializer)

View File

@ -218,7 +218,7 @@ def format_value(value):
return template.render(context) return template.render(context)
elif isinstance(value, str): elif isinstance(value, str):
if ( if (
(value.startswith('http:') or value.startswith('https:')) and not (value.startswith('http:') or value.startswith('https:') or value.startswith('/')) and not
re.search(r'\s', value) re.search(r'\s', value)
): ):
return mark_safe('<a href="{value}">{value}</a>'.format(value=escape(value))) return mark_safe('<a href="{value}">{value}</a>'.format(value=escape(value)))

View File

@ -171,7 +171,7 @@ class AnonRateThrottle(SimpleRateThrottle):
scope = 'anon' scope = 'anon'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
return None # Only throttle unauthenticated requests. return None # Only throttle unauthenticated requests.
return self.cache_format % { return self.cache_format % {
@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle):
scope = 'user' scope = 'user'
def get_cache_key(self, request, view): def get_cache_key(self, request, view):
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)
@ -237,9 +237,9 @@ class ScopedRateThrottle(SimpleRateThrottle):
If `view.throttle_scope` is not set, don't apply this throttle. If `view.throttle_scope` is not set, don't apply this throttle.
Otherwise generate the unique cache key by concatenating the user id Otherwise generate the unique cache key by concatenating the user id
with the '.throttle_scope` property of the view. with the `.throttle_scope` property of the view.
""" """
if request.user.is_authenticated: if request.user and request.user.is_authenticated:
ident = request.user.pk ident = request.user.pk
else: else:
ident = self.get_ident(request) ident = self.get_ident(request)

View File

@ -95,6 +95,9 @@ def get_field_kwargs(field_name, model_field):
(hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)): (hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField)):
kwargs['style'] = {'base_template': 'textarea.html'} kwargs['style'] = {'base_template': 'textarea.html'}
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field, models.AutoField) or not model_field.editable: if isinstance(model_field, models.AutoField) or not model_field.editable:
# If this field is read-only, then return early. # If this field is read-only, then return early.
# Further keyword arguments are not valid. # Further keyword arguments are not valid.
@ -104,9 +107,6 @@ 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:
kwargs['allow_null'] = True
if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))): if model_field.blank and (isinstance(model_field, (models.CharField, models.TextField))):
kwargs['allow_blank'] = True kwargs['allow_blank'] = True
@ -217,15 +217,9 @@ def get_field_kwargs(field_name, model_field):
] ]
if getattr(model_field, 'unique', False): if getattr(model_field, 'unique', False):
unique_error_message = model_field.error_messages.get('unique', None)
if unique_error_message:
unique_error_message = unique_error_message % {
'model_name': model_field.model._meta.verbose_name,
'field_label': model_field.verbose_name
}
validator = UniqueValidator( validator = UniqueValidator(
queryset=model_field.model._default_manager, queryset=model_field.model._default_manager,
message=unique_error_message) message=get_unique_error_message(model_field))
validator_kwarg.append(validator) validator_kwarg.append(validator)
if validator_kwarg: if validator_kwarg:
@ -269,6 +263,8 @@ def get_relation_kwargs(field_name, relation_info):
if not model_field.editable: if not model_field.editable:
kwargs['read_only'] = True kwargs['read_only'] = True
kwargs.pop('queryset', None) kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if kwargs.get('read_only', False): if kwargs.get('read_only', False):
# If this field is read-only, then return early. # If this field is read-only, then return early.
# No further keyword arguments are valid. # No further keyword arguments are valid.
@ -276,12 +272,12 @@ def get_relation_kwargs(field_name, relation_info):
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:
kwargs['allow_null'] = True
if model_field.validators: if model_field.validators:
kwargs['validators'] = model_field.validators kwargs['validators'] = model_field.validators
if getattr(model_field, 'unique', False): if getattr(model_field, 'unique', False):
validator = UniqueValidator(queryset=model_field.model._default_manager) validator = UniqueValidator(
queryset=model_field.model._default_manager,
message=get_unique_error_message(model_field))
kwargs['validators'] = kwargs.get('validators', []) + [validator] kwargs['validators'] = kwargs.get('validators', []) + [validator]
if to_many and not model_field.blank: if to_many and not model_field.blank:
kwargs['allow_empty'] = False kwargs['allow_empty'] = False
@ -300,3 +296,13 @@ def get_url_kwargs(model_field):
return { return {
'view_name': get_detail_view_name(model_field) 'view_name': get_detail_view_name(model_field)
} }
def get_unique_error_message(model_field):
unique_error_message = model_field.error_messages.get('unique', None)
if unique_error_message:
unique_error_message = unique_error_message % {
'model_name': model_field.model._meta.verbose_name,
'field_label': model_field.verbose_name
}
return unique_error_message

View File

@ -3,9 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
""" """
from django.http.multipartparser import parse_header from rest_framework.compat import parse_header_parameters
from rest_framework import HTTP_HEADER_ENCODING
def media_type_matches(lhs, rhs): def media_type_matches(lhs, rhs):
@ -46,7 +44,7 @@ def order_by_precedence(media_type_lst):
class _MediaType: class _MediaType:
def __init__(self, media_type_str): def __init__(self, media_type_str):
self.orig = '' if (media_type_str is None) else media_type_str self.orig = '' if (media_type_str is None) else media_type_str
self.full_type, self.params = parse_header(self.orig.encode(HTTP_HEADER_ENCODING)) self.full_type, self.params = parse_header_parameters(self.orig)
self.main_type, sep, self.sub_type = self.full_type.partition('/') self.main_type, sep, self.sub_type = self.full_type.partition('/')
def match(self, other): def match(self, other):
@ -79,5 +77,5 @@ class _MediaType:
def __str__(self): def __str__(self):
ret = "%s/%s" % (self.main_type, self.sub_type) ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items(): for key, val in self.params.items():
ret += "; %s=%s" % (key, val.decode('ascii')) ret += "; %s=%s" % (key, val)
return ret return ret

View File

@ -94,6 +94,7 @@ setup(
'Framework :: Django :: 3.1', 'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2', 'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0', 'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',

View File

@ -1,7 +1,8 @@
from django.urls import path from django.urls import path
from .views import MockView from .views import BasicModelWithUsersViewSet, MockView
urlpatterns = [ urlpatterns = [
path('', MockView.as_view()), path('', MockView.as_view()),
path('basicviewset', BasicModelWithUsersViewSet.as_view({'get': 'list'})),
] ]

View File

@ -0,0 +1,8 @@
from rest_framework.serializers import ModelSerializer
from tests.models import BasicModelWithUsers
class BasicSerializer(ModelSerializer):
class Meta:
model = BasicModelWithUsers
fields = '__all__'

View File

@ -1,8 +1,35 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from rest_framework.permissions import IsAuthenticated
from rest_framework.test import APIClient from rest_framework.test import APIClient
from .views import BasicModelWithUsersViewSet, OrganizationPermissions
@override_settings(ROOT_URLCONF='tests.browsable_api.no_auth_urls')
class AnonymousUserTests(TestCase):
"""Tests correct handling of anonymous user request on endpoints with IsAuthenticated permission class."""
def setUp(self):
self.client = APIClient(enforce_csrf_checks=True)
def tearDown(self):
self.client.logout()
def test_get_raises_typeerror_when_anonymous_user_in_queryset_filter(self):
with self.assertRaises(TypeError):
self.client.get('/basicviewset')
def test_get_returns_http_forbidden_when_anonymous_user(self):
old_permissions = BasicModelWithUsersViewSet.permission_classes
BasicModelWithUsersViewSet.permission_classes = [IsAuthenticated, OrganizationPermissions]
response = self.client.get('/basicviewset')
BasicModelWithUsersViewSet.permission_classes = old_permissions
self.assertEqual(response.status_code, 403)
@override_settings(ROOT_URLCONF='tests.browsable_api.auth_urls') @override_settings(ROOT_URLCONF='tests.browsable_api.auth_urls')
class DropdownWithAuthTests(TestCase): class DropdownWithAuthTests(TestCase):

View File

@ -1,6 +1,16 @@
from rest_framework import authentication, renderers from rest_framework import authentication, renderers
from rest_framework.permissions import BasePermission
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from ..models import BasicModelWithUsers
from .serializers import BasicSerializer
class OrganizationPermissions(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_staff or (request.user == obj.owner.organization_user.user)
class MockView(APIView): class MockView(APIView):
@ -9,3 +19,15 @@ class MockView(APIView):
def get(self, request): def get(self, request):
return Response({'a': 1, 'b': 2, 'c': 3}) return Response({'a': 1, 'b': 2, 'c': 3})
class BasicModelWithUsersViewSet(ModelViewSet):
queryset = BasicModelWithUsers.objects.all()
serializer_class = BasicSerializer
permission_classes = [OrganizationPermissions]
# permission_classes = [IsAuthenticated, OrganizationPermissions]
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_queryset(self):
qs = super().get_queryset().filter(users=self.request.user)
return qs

View File

@ -1,5 +1,6 @@
import uuid import uuid
from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -33,6 +34,10 @@ class ManyToManySource(RESTFrameworkModel):
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources') targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
class BasicModelWithUsers(RESTFrameworkModel):
users = models.ManyToManyField(User)
# ForeignKey # ForeignKey
class ForeignKeyTarget(RESTFrameworkModel): class ForeignKeyTarget(RESTFrameworkModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

View File

@ -1,5 +1,3 @@
import sys
import pytest import pytest
from django.test import TestCase from django.test import TestCase
@ -33,7 +31,7 @@ indented
# If markdown is installed we also test it's working # If markdown is installed we also test it's working
# (and that our wrapped forces '=' to h2 and '-' to h3) # (and that our wrapped forces '=' to h2 and '-' to h3)
MARKDOWN_BASE = """<h2 id="an-example-docstring">an example docstring</h2> MARKDOWN_DOCSTRING = """<h2 id="an-example-docstring">an example docstring</h2>
<ul> <ul>
<li>list</li> <li>list</li>
<li>list</li> <li>list</li>
@ -42,25 +40,8 @@ MARKDOWN_BASE = """<h2 id="an-example-docstring">an example docstring</h2>
<pre><code>code block <pre><code>code block
</code></pre> </code></pre>
<p>indented</p> <p>indented</p>
<h2 id="hash-style-header">hash style header</h2>%s""" <h2 id="hash-style-header">hash style header</h2>
<div class="highlight"><pre><span></span><span class="p">[{</span><span class="w"></span><br /><span class="w"> </span><span class="nt">&quot;alpha&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span><br /><span class="w"> </span><span class="nt">&quot;beta&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;this is a string&quot;</span><span class="w"></span><br /><span class="p">}]</span><span class="w"></span><br /></pre></div>
MARKDOWN_gte_33 = """
<div class="highlight"><pre><span></span><span class="p">[{</span><br />\
<span class="nt">&quot;alpha&quot;</span><span class="p">:</span>\
<span class="mi">1</span><span class="p">,</span><br />\
<span class="nt">&quot;beta&quot;</span><span class="p">:</span>\
<span class="s2">&quot;this is a string&quot;</span><br />\
<span class="p">}]</span><br /></pre></div>
<p><br /></p>"""
MARKDOWN_lt_33 = """
<div class="highlight"><pre><span></span><span class="p">[{</span><br />\
<span class="nt">&quot;alpha&quot;</span><span class="p">:</span>\
<span class="mi">1</span><span class="p">,</span><br />\
<span class="nt">&quot;beta&quot;</span><span class="p">:</span>\
<span class="s2">&quot;this is a string&quot;</span><br />\
<span class="p">}]</span><br /></pre></div>
<p><br /></p>""" <p><br /></p>"""
@ -163,11 +144,7 @@ class TestViewNamesAndDescriptions(TestCase):
""" """
Ensure markdown to HTML works as expected. Ensure markdown to HTML works as expected.
""" """
# Markdown 3.3 is only supported on Python 3.6 and higher assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
if sys.version_info >= (3, 6):
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33
else:
assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33
def test_dedent_tabs(): def test_dedent_tabs():

View File

@ -1,15 +1,16 @@
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import utc
from rest_framework.compat import coreapi from rest_framework.compat import coreapi
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from rest_framework.utils.serializer_helpers import ReturnList from rest_framework.utils.serializer_helpers import ReturnList
utc = timezone.utc
class MockList: class MockList:
def tolist(self): def tolist(self):

View File

@ -9,7 +9,7 @@ import pytz
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.http import QueryDict from django.http import QueryDict
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.timezone import activate, deactivate, override, utc from django.utils.timezone import activate, deactivate, override
import rest_framework import rest_framework
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
@ -17,6 +17,8 @@ from rest_framework.fields import (
BuiltinSignatureError, DjangoImageField, is_simple_callable BuiltinSignatureError, DjangoImageField, is_simple_callable
) )
utc = datetime.timezone.utc
# Tests for helper functions. # Tests for helper functions.
# --------------------------- # ---------------------------
@ -73,6 +75,10 @@ class TestIsSimpleCallable:
assert is_simple_callable(valid_vargs_kwargs) assert is_simple_callable(valid_vargs_kwargs)
assert not is_simple_callable(invalid) assert not is_simple_callable(invalid)
@pytest.mark.parametrize('obj', (True, None, "str", b'bytes', 123, 1.23))
def test_not_callable(self, obj):
assert not is_simple_callable(obj)
def test_4602_regression(self): def test_4602_regression(self):
from django.db import models from django.db import models
@ -1859,9 +1865,9 @@ class TestMultipleChoiceField(FieldValues):
def test_against_partial_and_full_updates(self): def test_against_partial_and_full_updates(self):
field = serializers.MultipleChoiceField(choices=(('a', 'a'), ('b', 'b'))) field = serializers.MultipleChoiceField(choices=(('a', 'a'), ('b', 'b')))
field.partial = False field.partial = False
assert field.get_value(QueryDict({})) == [] assert field.get_value(QueryDict('')) == []
field.partial = True field.partial = True
assert field.get_value(QueryDict({})) == rest_framework.fields.empty assert field.get_value(QueryDict('')) == rest_framework.fields.empty
class TestEmptyMultipleChoiceField(FieldValues): class TestEmptyMultipleChoiceField(FieldValues):

View File

@ -12,6 +12,7 @@ import sys
import tempfile import tempfile
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.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@ -452,11 +453,14 @@ class TestPosgresFieldsMapping(TestCase):
model = ArrayFieldModel model = ArrayFieldModel
fields = ['array_field', 'array_field_with_blank'] fields = ['array_field', 'array_field_with_blank']
validators = ""
if django.VERSION < (4, 1):
validators = ", validators=[<django.core.validators.MaxLengthValidator object>]"
expected = dedent(""" expected = dedent("""
TestSerializer(): TestSerializer():
array_field = ListField(allow_empty=False, child=CharField(label='Array field', validators=[<django.core.validators.MaxLengthValidator object>])) array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s))
array_field_with_blank = ListField(child=CharField(label='Array field with blank', validators=[<django.core.validators.MaxLengthValidator object>]), required=False) array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False)
""") """ % (validators, validators))
self.assertEqual(repr(TestSerializer()), expected) self.assertEqual(repr(TestSerializer()), expected)
@pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField') @pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField')
@ -1021,6 +1025,73 @@ class Issue2704TestCase(TestCase):
assert serializer.data == expected assert serializer.data == expected
class Issue7550FooModel(models.Model):
text = models.CharField(max_length=100)
bar = models.ForeignKey(
'Issue7550BarModel', null=True, blank=True, on_delete=models.SET_NULL,
related_name='foos', related_query_name='foo')
class Issue7550BarModel(models.Model):
pass
class Issue7550TestCase(TestCase):
def test_dotted_source(self):
class _FooSerializer(serializers.ModelSerializer):
class Meta:
model = Issue7550FooModel
fields = ('id', 'text')
class FooSerializer(serializers.ModelSerializer):
other_foos = _FooSerializer(source='bar.foos', many=True)
class Meta:
model = Issue7550BarModel
fields = ('id', 'other_foos')
bar = Issue7550BarModel.objects.create()
foo_a = Issue7550FooModel.objects.create(bar=bar, text='abc')
foo_b = Issue7550FooModel.objects.create(bar=bar, text='123')
assert FooSerializer(foo_a).data == {
'id': foo_a.id,
'other_foos': [
{
'id': foo_a.id,
'text': foo_a.text,
},
{
'id': foo_b.id,
'text': foo_b.text,
},
],
}
def test_dotted_source_with_default(self):
class _FooSerializer(serializers.ModelSerializer):
class Meta:
model = Issue7550FooModel
fields = ('id', 'text')
class FooSerializer(serializers.ModelSerializer):
other_foos = _FooSerializer(source='bar.foos', default=[], many=True)
class Meta:
model = Issue7550FooModel
fields = ('id', 'other_foos')
foo = Issue7550FooModel.objects.create(bar=None, text='abc')
assert FooSerializer(foo).data == {
'id': foo.id,
'other_foos': [],
}
class DecimalFieldModel(models.Model): class DecimalFieldModel(models.Model):
decimal_field = models.DecimalField( decimal_field = models.DecimalField(
max_digits=3, max_digits=3,

View File

@ -42,6 +42,12 @@ class RelatedModelSerializer(serializers.ModelSerializer):
fields = ('username', 'email') fields = ('username', 'email')
class RelatedModelUserSerializer(serializers.ModelSerializer):
class Meta:
model = RelatedModel
fields = ('user',)
class AnotherUniquenessModel(models.Model): class AnotherUniquenessModel(models.Model):
code = models.IntegerField(unique=True) code = models.IntegerField(unique=True)
@ -83,6 +89,13 @@ class TestUniquenessValidation(TestCase):
assert not serializer.is_valid() assert not serializer.is_valid()
assert serializer.errors == {'username': ['uniqueness model with this username already exists.']} assert serializer.errors == {'username': ['uniqueness model with this username already exists.']}
def test_relation_is_not_unique(self):
RelatedModel.objects.create(user=self.instance)
data = {'user': self.instance.pk}
serializer = RelatedModelUserSerializer(data=data)
assert not serializer.is_valid()
assert serializer.errors == {'user': ['related model with this user already exists.']}
def test_is_unique(self): def test_is_unique(self):
data = {'username': 'other'} data = {'username': 'other'}
serializer = UniquenessSerializer(data=data) serializer = UniquenessSerializer(data=data)

View File

@ -3,7 +3,7 @@ envlist =
{py36,py37,py38,py39}-django22, {py36,py37,py38,py39}-django22,
{py36,py37,py38,py39}-django31, {py36,py37,py38,py39}-django31,
{py36,py37,py38,py39,py310}-django32, {py36,py37,py38,py39,py310}-django32,
{py38,py39,py310}-{django40,djangomain}, {py38,py39,py310}-{django40,django41,djangomain},
base,dist,docs, base,dist,docs,
[travis:env] [travis:env]
@ -12,6 +12,7 @@ DJANGO =
3.1: django31 3.1: django31
3.2: django32 3.2: django32
4.0: django40 4.0: django40
4.1: django41
main: djangomain main: djangomain
[testenv] [testenv]
@ -24,7 +25,8 @@ deps =
django22: Django>=2.2,<3.0 django22: Django>=2.2,<3.0
django31: Django>=3.1,<3.2 django31: Django>=3.1,<3.2
django32: Django>=3.2,<4.0 django32: Django>=3.2,<4.0
django40: Django>=4.0,<5.0 django40: Django>=4.0,<4.1
django41: Django>=4.1a1,<4.2
djangomain: https://github.com/django/django/archive/main.tar.gz djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt