mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-30 21:44:04 +03:00
Merge branch 'master' into migrate_setuppy_to_pryoject.toml
This commit is contained in:
commit
6f8da971d6
9
.github/ISSUE_TEMPLATE/1-issue.md
vendored
9
.github/ISSUE_TEMPLATE/1-issue.md
vendored
|
@ -5,6 +5,13 @@ about: Please only raise an issue if you've been advised to do so after discussi
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note: REST framework is considered feature-complete. New functionality should be implemented outside the core REST framework. For details, please check the docs: https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages
|
||||||
|
-->
|
||||||
|
|
||||||
- [ ] Raised initially as discussion #...
|
- [ ] Raised initially as discussion #...
|
||||||
- [ ] 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/community/third-party-packages/#about-third-party-packages) where possible.)
|
- [ ] This is not a feature request suitable for implementation outside this project. Please elaborate what it is:
|
||||||
|
- [ ] compatibility fix for new Django/Python version ...
|
||||||
|
- [ ] other type of bug fix
|
||||||
|
- [ ] other type of improvement that does not touch existing code or change existing behavior (e.g. wrapper for new Django field)
|
||||||
- [ ] I have reduced the issue to the simplest possible case.
|
- [ ] I have reduced the issue to the simplest possible case.
|
||||||
|
|
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
|
@ -14,13 +14,12 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- '3.6'
|
|
||||||
- '3.7'
|
|
||||||
- '3.8'
|
- '3.8'
|
||||||
- '3.9'
|
- '3.9'
|
||||||
- '3.10'
|
- '3.10'
|
||||||
- '3.11'
|
- '3.11'
|
||||||
- '3.12'
|
- '3.12'
|
||||||
|
- '3.13'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -37,17 +36,8 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python -m pip install --upgrade codecov tox
|
run: python -m pip install --upgrade codecov tox
|
||||||
|
|
||||||
- name: Install tox-py
|
|
||||||
if: ${{ matrix.python-version == '3.6' }}
|
|
||||||
run: python -m pip install --upgrade tox-py
|
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
- name: Run tox targets for ${{ matrix.python-version }}
|
||||||
if: ${{ matrix.python-version != '3.6' }}
|
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-')
|
||||||
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
|
||||||
if: ${{ matrix.python-version == '3.6' }}
|
|
||||||
run: tox --py current
|
|
||||||
|
|
||||||
- name: Run extra tox targets
|
- name: Run extra tox targets
|
||||||
if: ${{ matrix.python-version == '3.9' }}
|
if: ${{ matrix.python-version == '3.9' }}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
*Note*: Before submitting this pull request, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests).
|
*Note*: Before submitting a code change, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests).
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
|
15
README.md
15
README.md
|
@ -28,8 +28,9 @@ The initial aim is to provide a single full-time position on REST framework.
|
||||||
[![][cryptapi-img]][cryptapi-url]
|
[![][cryptapi-img]][cryptapi-url]
|
||||||
[![][fezto-img]][fezto-url]
|
[![][fezto-img]][fezto-url]
|
||||||
[![][svix-img]][svix-url]
|
[![][svix-img]][svix-url]
|
||||||
|
[![][zuplo-img]][zuplo-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], [FEZTO][fezto-url], and [Svix][svix-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], [FEZTO][fezto-url], [Svix][svix-url], and [Zuplo][zuplo-url].
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -45,8 +46,6 @@ Some reasons you might want to use REST framework:
|
||||||
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
|
||||||
* [Extensive documentation][docs], and [great community support][group].
|
* [Extensive documentation][docs], and [great community support][group].
|
||||||
|
|
||||||
There is a live example API for testing purposes, [available here][sandbox].
|
|
||||||
|
|
||||||
**Below**: *Screenshot from the browsable API*
|
**Below**: *Screenshot from the browsable API*
|
||||||
|
|
||||||
![Screenshot][image]
|
![Screenshot][image]
|
||||||
|
@ -55,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox].
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python 3.6+
|
* Python 3.8+
|
||||||
* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
|
* Django 4.2, 5.0, 5.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.
|
||||||
|
@ -174,8 +173,6 @@ Full documentation for the project is available at [https://www.django-rest-fram
|
||||||
|
|
||||||
For questions and support, use the [REST framework discussion group][group], or `#restframework` on libera.chat IRC.
|
For questions and support, use the [REST framework discussion group][group], or `#restframework` on libera.chat IRC.
|
||||||
|
|
||||||
You may also want to [follow the author on Twitter][twitter].
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
|
|
||||||
Please see the [security policy][security-policy].
|
Please see the [security policy][security-policy].
|
||||||
|
@ -186,9 +183,7 @@ Please see the [security policy][security-policy].
|
||||||
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
|
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
|
||||||
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
|
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
|
||||||
[pypi]: https://pypi.org/project/djangorestframework/
|
[pypi]: https://pypi.org/project/djangorestframework/
|
||||||
[twitter]: https://twitter.com/starletdreaming
|
|
||||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
[sandbox]: https://restframework.herokuapp.com/
|
|
||||||
|
|
||||||
[funding]: https://fund.django-rest-framework.org/topics/funding/
|
[funding]: https://fund.django-rest-framework.org/topics/funding/
|
||||||
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
||||||
|
@ -202,6 +197,7 @@ Please see the [security policy][security-policy].
|
||||||
[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
|
[fezto-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/fezto-readme.png
|
||||||
[svix-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/svix-premium.png
|
[svix-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/svix-premium.png
|
||||||
|
[zuplo-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/zuplo-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
|
||||||
|
@ -212,6 +208,7 @@ Please see the [security policy][security-policy].
|
||||||
[cryptapi-url]: https://cryptapi.io
|
[cryptapi-url]: https://cryptapi.io
|
||||||
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
|
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
|
||||||
[svix-url]: https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship
|
[svix-url]: https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship
|
||||||
|
[zuplo-url]: https://zuplo.link/django-gh
|
||||||
|
|
||||||
[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
|
||||||
|
|
|
@ -90,6 +90,12 @@ The kind of response that will be used depends on the authentication scheme. Al
|
||||||
|
|
||||||
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
|
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.
|
||||||
|
|
||||||
|
## Django 5.1+ `LoginRequiredMiddleware`
|
||||||
|
|
||||||
|
If you're running Django 5.1+ and use the [`LoginRequiredMiddleware`][login-required-middleware], please note that all views from DRF are opted-out of this middleware. This is because the authentication in DRF is based authentication and permissions classes, which may be determined after the middleware has been applied. Additionally, when the request is not authenticated, the middleware redirects the user to the login page, which is not suitable for API requests, where it's preferable to return a 401 status code.
|
||||||
|
|
||||||
|
REST framework offers an equivalent mechanism for DRF views via the global settings, `DEFAULT_AUTHENTICATION_CLASSES` and `DEFAULT_PERMISSION_CLASSES`. They should be changed accordingly if you need to enforce that API requests are logged in.
|
||||||
|
|
||||||
## Apache mod_wsgi specific configuration
|
## Apache mod_wsgi specific configuration
|
||||||
|
|
||||||
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the authorization header is not passed through to a WSGI application by default, as it is assumed that authentication will be handled by Apache, rather than at an application level.
|
||||||
|
@ -484,3 +490,4 @@ More information can be found in the [Documentation](https://django-rest-durin.r
|
||||||
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
|
||||||
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
|
||||||
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
|
||||||
|
[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware
|
|
@ -59,6 +59,29 @@ class PostView(APIView):
|
||||||
return Response(content)
|
return Response(content)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Using cache with @api_view decorator
|
||||||
|
|
||||||
|
When using @api_view decorator, the Django-provided method-based cache decorators such as [`cache_page`][page],
|
||||||
|
[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers] can be called directly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
from django.views.decorators.vary import vary_on_cookie
|
||||||
|
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
@cache_page(60 * 15)
|
||||||
|
@vary_on_cookie
|
||||||
|
@api_view(["GET"])
|
||||||
|
def get_user_list(request):
|
||||||
|
content = {"user_feed": request.user.get_user_feed()}
|
||||||
|
return Response(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**NOTE:** The [`cache_page`][page] decorator only caches the
|
**NOTE:** The [`cache_page`][page] decorator only caches the
|
||||||
`GET` and `HEAD` responses with status 200.
|
`GET` and `HEAD` responses with status 200.
|
||||||
|
|
||||||
|
|
|
@ -68,14 +68,6 @@ When serializing the instance, default will be used if the object attribute or d
|
||||||
|
|
||||||
Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error.
|
Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error.
|
||||||
|
|
||||||
Notes regarding default value propagation from model to serializer:
|
|
||||||
|
|
||||||
All the default values from model will pass as default to the serializer and the options method.
|
|
||||||
|
|
||||||
If the default is callable then it will be propagated to & evaluated every time in the serializer but not in options method.
|
|
||||||
|
|
||||||
If the value for given field is not given then default value will be present in the serializer and available in serializer's methods. Specified validation on given field will be evaluated on default value as that field will be present in the serializer.
|
|
||||||
|
|
||||||
### `allow_null`
|
### `allow_null`
|
||||||
|
|
||||||
Normally an error will be raised if `None` is passed to a serializer field. Set this keyword argument to `True` if `None` should be considered a valid value.
|
Normally an error will be raised if `None` is passed to a serializer field. Set this keyword argument to `True` if `None` should be considered a valid value.
|
||||||
|
@ -299,8 +291,8 @@ Corresponds to `django.db.models.fields.DecimalField`.
|
||||||
* `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`.
|
* `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`.
|
||||||
* `decimal_places` The number of decimal places to store with the number.
|
* `decimal_places` The number of decimal places to store with the number.
|
||||||
* `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`.
|
* `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`.
|
||||||
* `max_value` Validate that the number provided is no greater than this value.
|
* `max_value` Validate that the number provided is no greater than this value. Should be an integer or `Decimal` object.
|
||||||
* `min_value` Validate that the number provided is no less than this value.
|
* `min_value` Validate that the number provided is no less than this value. Should be an integer or `Decimal` object.
|
||||||
* `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
|
* `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
|
||||||
* `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
|
* `rounding` Sets the rounding mode used when quantizing to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
|
||||||
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`.
|
* `normalize_output` Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without losing data. Defaults to `False`.
|
||||||
|
|
|
@ -173,12 +173,11 @@ This permission is suitable if you want to your API to allow read permissions to
|
||||||
|
|
||||||
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. The appropriate model is determined by checking `get_queryset().model` or `queryset.model`.
|
This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. The appropriate model is determined by checking `get_queryset().model` or `queryset.model`.
|
||||||
|
|
||||||
* `GET` requests require the user to have the `view` or `change` permission on the model
|
|
||||||
* `POST` requests require the user to have the `add` permission on the model.
|
* `POST` requests require the user to have the `add` permission on the model.
|
||||||
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
|
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
|
||||||
* `DELETE` requests require the user to have the `delete` permission on the model.
|
* `DELETE` requests require the user to have the `delete` permission on the model.
|
||||||
|
|
||||||
The default behaviour can also be overridden to support custom model permissions.
|
The default behavior can also be overridden to support custom model permissions. For example, you might want to include a `view` model permission for `GET` requests.
|
||||||
|
|
||||||
To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
|
To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
|
||||||
|
|
||||||
|
|
|
@ -283,7 +283,7 @@ By default this will include the following keys: `view`, `request`, `response`,
|
||||||
|
|
||||||
The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response.
|
The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response.
|
||||||
|
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_str
|
||||||
from rest_framework import renderers
|
from rest_framework import renderers
|
||||||
|
|
||||||
|
|
||||||
|
@ -292,7 +292,7 @@ The following is an example plaintext renderer that will return a response with
|
||||||
format = 'txt'
|
format = 'txt'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
return smart_text(data, encoding=self.charset)
|
return smart_str(data, encoding=self.charset)
|
||||||
|
|
||||||
## Setting the character set
|
## Setting the character set
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,24 @@ The above example would now generate the following URL pattern:
|
||||||
* URL path: `^users/{pk}/change-password/$`
|
* URL path: `^users/{pk}/change-password/$`
|
||||||
* URL name: `'user-change_password'`
|
* URL name: `'user-change_password'`
|
||||||
|
|
||||||
|
### Using Django `path()` with routers
|
||||||
|
|
||||||
|
By default, the URLs created by routers use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example:
|
||||||
|
|
||||||
|
router = SimpleRouter(use_regex_path=False)
|
||||||
|
|
||||||
|
The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs:
|
||||||
|
|
||||||
|
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
lookup_field = 'my_model_id'
|
||||||
|
lookup_value_regex = '[0-9a-f]{32}'
|
||||||
|
|
||||||
|
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
lookup_field = 'my_model_uuid'
|
||||||
|
lookup_value_converter = 'uuid'
|
||||||
|
|
||||||
|
Note that path converters will be used on all URLs registered in the router, including viewset actions.
|
||||||
|
|
||||||
# API Guide
|
# API Guide
|
||||||
|
|
||||||
## SimpleRouter
|
## SimpleRouter
|
||||||
|
@ -160,30 +178,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
|
||||||
<tr><td>{prefix}/{lookup}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
|
<tr><td>{prefix}/{lookup}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
|
By default, the URLs created by `SimpleRouter` are appended with a trailing slash.
|
||||||
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
|
||||||
|
|
||||||
router = SimpleRouter(trailing_slash=False)
|
router = SimpleRouter(trailing_slash=False)
|
||||||
|
|
||||||
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
|
||||||
|
|
||||||
By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router, in this case [path converters][path-converters-topic-reference] are used. For example:
|
|
||||||
|
|
||||||
router = SimpleRouter(use_regex_path=False)
|
|
||||||
|
|
||||||
**Note**: `use_regex_path=False` only works with Django 2.x or above, since this feature was introduced in 2.0.0. See [release note][simplified-routing-release-note]
|
|
||||||
|
|
||||||
|
|
||||||
The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset or `lookup_value_converter` if using path converters. For example, you can limit the lookup to valid UUIDs:
|
|
||||||
|
|
||||||
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
||||||
lookup_field = 'my_model_id'
|
|
||||||
lookup_value_regex = '[0-9a-f]{32}'
|
|
||||||
|
|
||||||
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
||||||
lookup_field = 'my_model_uuid'
|
|
||||||
lookup_value_converter = 'uuid'
|
|
||||||
|
|
||||||
## DefaultRouter
|
## DefaultRouter
|
||||||
|
|
||||||
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
|
||||||
|
@ -351,5 +352,4 @@ The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions
|
||||||
[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/4.0/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/4.0/ref/urls/#include
|
[include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include
|
||||||
[simplified-routing-release-note]: https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax
|
|
||||||
[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters
|
[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters
|
||||||
|
|
|
@ -845,8 +845,6 @@ Here's an example of how you might choose to implement multiple updates:
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = BookListSerializer
|
list_serializer_class = BookListSerializer
|
||||||
|
|
||||||
It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2.
|
|
||||||
|
|
||||||
#### Customizing ListSerializer initialization
|
#### Customizing ListSerializer initialization
|
||||||
|
|
||||||
When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
|
When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
|
||||||
|
|
|
@ -31,10 +31,6 @@ The current minimum versions of Django still is 3.0 and Python 3.6.
|
||||||
|
|
||||||
`ModelSerializer` generates validators for [UniqueConstraint](https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint) (both UniqueValidator and UniqueTogetherValidator)
|
`ModelSerializer` generates validators for [UniqueConstraint](https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint) (both UniqueValidator and UniqueTogetherValidator)
|
||||||
|
|
||||||
## ValidationErrors improvements
|
|
||||||
|
|
||||||
The `ValidationError` has been aligned with Django's, currently supporting the same style (signature) and nesting.
|
|
||||||
|
|
||||||
## SimpleRouter non-regex matching support
|
## SimpleRouter non-regex matching support
|
||||||
|
|
||||||
By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router.
|
By default the URLs created by `SimpleRouter` use regular expressions. This behavior can be modified by setting the `use_regex_path` argument to `False` when instantiating the router.
|
||||||
|
@ -47,10 +43,6 @@ Dependency on pytz has been removed and deprecation warnings have been added, Dj
|
||||||
|
|
||||||
Searches now may contain _quoted phrases_ with spaces, each phrase is considered as a single search term, and it will raise a validation error if any null-character is provided in search. See the [Filtering API guide](../api-guide/filtering.md) for more information.
|
Searches now may contain _quoted phrases_ with spaces, each phrase is considered as a single search term, and it will raise a validation error if any null-character is provided in search. See the [Filtering API guide](../api-guide/filtering.md) for more information.
|
||||||
|
|
||||||
## Default values propagation
|
|
||||||
|
|
||||||
Model fields' default values are now propagated to serializer fields, for more information see the [Serializer fields API guide](../api-guide/fields.md#default).
|
|
||||||
|
|
||||||
## Other fixes and improvements
|
## Other fixes and improvements
|
||||||
|
|
||||||
There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour.
|
There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour.
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
|
|
||||||
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
|
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
|
||||||
|
|
||||||
---
|
!!! note
|
||||||
|
|
||||||
**Note**: At this point in it's lifespan we consider Django REST framework to be essentially feature-complete. We may accept pull requests that track the continued development of Django versions, but would prefer not to accept new features or code formatting changes.
|
At this point in its lifespan we consider Django REST framework to be feature-complete. We focus on pull requests that track the continued development of Django versions, and generally do not accept new features or code formatting changes.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
@ -36,10 +34,9 @@ Our contribution process is that the [GitHub discussions page](https://github.co
|
||||||
|
|
||||||
Some tips on good potential issue reporting:
|
Some tips on good potential issue reporting:
|
||||||
|
|
||||||
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
* Django REST framework is considered feature-complete. Please do not file requests to change behavior, unless it is required for security reasons or to maintain compatibility with upcoming Django or Python versions.
|
||||||
* Search the GitHub project page for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
* Search the GitHub project page for related items, and make sure you're running the latest version of REST framework before reporting an issue.
|
||||||
* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. At this point in it's lifespan we consider Django REST framework to be essentially feature-complete.
|
* Feature requests will typically be closed with a recommendation that they be implemented outside the core REST framework library (e.g. as third-party libraries). This approach allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability and great documentation.
|
||||||
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
|
|
||||||
|
|
||||||
## Triaging issues
|
## Triaging issues
|
||||||
|
|
||||||
|
@ -48,8 +45,8 @@ Getting involved in triaging incoming issues is a good way to start contributing
|
||||||
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
|
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
|
||||||
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
|
* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
|
||||||
* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
|
* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
|
||||||
* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
|
* If the ticket is a feature request, could the feature request instead be implemented as a third party package?
|
||||||
* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
|
* If a ticket hasn't had much activity and addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
|
||||||
|
|
||||||
* [https://www.djangoproject.com/community/jobs/][djangoproject-website]
|
* [https://www.djangoproject.com/community/jobs/][djangoproject-website]
|
||||||
* [https://www.python.org/jobs/][python-org-jobs]
|
* [https://www.python.org/jobs/][python-org-jobs]
|
||||||
|
* [https://django.on-remote.com][django-on-remote]
|
||||||
* [https://djangogigs.com][django-gigs-com]
|
* [https://djangogigs.com][django-gigs-com]
|
||||||
* [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]
|
||||||
|
@ -26,6 +27,7 @@ Wonder how else you can help? One of the best ways you can help Django REST Fram
|
||||||
|
|
||||||
[djangoproject-website]: https://www.djangoproject.com/community/jobs/
|
[djangoproject-website]: https://www.djangoproject.com/community/jobs/
|
||||||
[python-org-jobs]: https://www.python.org/jobs/
|
[python-org-jobs]: https://www.python.org/jobs/
|
||||||
|
[django-on-remote]: https://django.on-remote.com/
|
||||||
[django-gigs-com]: https://djangogigs.com
|
[django-gigs-com]: https://djangogigs.com
|
||||||
[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
|
||||||
|
|
|
@ -13,55 +13,13 @@ The aim is to ensure that the project has a high
|
||||||
|
|
||||||
## Maintenance team
|
## Maintenance team
|
||||||
|
|
||||||
We have a quarterly maintenance cycle where new members may join the maintenance team. We currently cap the size of the team at 5 members, and may encourage folks to step out of the team for a cycle to allow new members to participate.
|
[Participating actively in the REST framework project](contributing.md) **does not require being part of the maintenance team**. Almost every important part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository.
|
||||||
|
|
||||||
#### Current team
|
#### Composition
|
||||||
|
|
||||||
The [maintenance team for Q4 2015](https://github.com/encode/django-rest-framework/issues/2190):
|
The composition of the maintenance team is handled by [@tomchristie](https://github.com/encode/). Team members will be added as collaborators to the repository.
|
||||||
|
|
||||||
* [@tomchristie](https://github.com/encode/)
|
#### Responsibilities
|
||||||
* [@xordoquy](https://github.com/xordoquy/) (Release manager.)
|
|
||||||
* [@carltongibson](https://github.com/carltongibson/)
|
|
||||||
* [@kevin-brown](https://github.com/kevin-brown/)
|
|
||||||
* [@jpadilla](https://github.com/jpadilla/)
|
|
||||||
|
|
||||||
#### Maintenance cycles
|
|
||||||
|
|
||||||
Each maintenance cycle is initiated by an issue being opened with the `Process` label.
|
|
||||||
|
|
||||||
* To be considered for a maintainer role simply comment against the issue.
|
|
||||||
* Existing members must explicitly opt-in to the next cycle by check-marking their name.
|
|
||||||
* The final decision on the incoming team will be made by `@tomchristie`.
|
|
||||||
|
|
||||||
Members of the maintenance team will be added as collaborators to the repository.
|
|
||||||
|
|
||||||
The following template should be used for the description of the issue, and serves as the formal process for selecting the team.
|
|
||||||
|
|
||||||
This issue is for determining the maintenance team for the *** period.
|
|
||||||
|
|
||||||
Please see the [Project management](https://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Renewing existing members.
|
|
||||||
|
|
||||||
The following people are the current maintenance team. Please checkmark your name if you wish to continue to have write permission on the repository for the *** period.
|
|
||||||
|
|
||||||
- [ ] @***
|
|
||||||
- [ ] @***
|
|
||||||
- [ ] @***
|
|
||||||
- [ ] @***
|
|
||||||
- [ ] @***
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### New members.
|
|
||||||
|
|
||||||
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](https://www.django-rest-framework.org/topics/project-management/) documentation.
|
|
||||||
|
|
||||||
#### Responsibilities of team members
|
|
||||||
|
|
||||||
Team members have the following responsibilities.
|
Team members have the following responsibilities.
|
||||||
|
|
||||||
|
@ -78,16 +36,12 @@ Further notes for maintainers:
|
||||||
* Each issue/pull request should have exactly one label once triaged.
|
* Each issue/pull request should have exactly one label once triaged.
|
||||||
* Search for un-triaged issues with [is:open no:label][un-triaged].
|
* Search for un-triaged issues with [is:open no:label][un-triaged].
|
||||||
|
|
||||||
It should be noted that participating actively in the REST framework project clearly **does not require being part of the maintenance team**. Almost every import part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Release process
|
## Release process
|
||||||
|
|
||||||
The release manager is selected on every quarterly maintenance cycle.
|
* The release manager is selected by `@tomchristie`.
|
||||||
|
* The release manager will then have the maintainer role added to PyPI package.
|
||||||
* The manager should be selected by `@tomchristie`.
|
|
||||||
* The manager will then have the maintainer role added to PyPI package.
|
|
||||||
* The previous manager will then have the maintainer role removed from the PyPI package.
|
* The previous manager will then have the maintainer role removed from the PyPI package.
|
||||||
|
|
||||||
Our PyPI releases will be handled by either the current release manager, or by `@tomchristie`. Every release should have an open issue tagged with the `Release` label and marked against the appropriate milestone.
|
Our PyPI releases will be handled by either the current release manager, or by `@tomchristie`. Every release should have an open issue tagged with the `Release` label and marked against the appropriate milestone.
|
||||||
|
@ -198,8 +152,7 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons
|
||||||
|
|
||||||
The following issues still need to be addressed:
|
The following issues still need to be addressed:
|
||||||
|
|
||||||
* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.
|
* Ensure `@j4mie` has back-up access to the `django-rest-framework.org` domain setup and admin.
|
||||||
* Document ownership of the [live example][sandbox] API.
|
|
||||||
* Document ownership of the [mailing list][mailing-list] and IRC channel.
|
* Document ownership of the [mailing list][mailing-list] and IRC channel.
|
||||||
* Document ownership and management of the security mailing list.
|
* Document ownership and management of the security mailing list.
|
||||||
|
|
||||||
|
@ -208,5 +161,4 @@ The following issues still need to be addressed:
|
||||||
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
||||||
[transifex-client]: https://pypi.org/project/transifex-client/
|
[transifex-client]: https://pypi.org/project/transifex-client/
|
||||||
[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
|
[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
|
||||||
[sandbox]: https://restframework.herokuapp.com/
|
|
||||||
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
|
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
|
- **Minor** version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
|
||||||
|
|
||||||
Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
|
- **Medium** version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
|
||||||
|
|
||||||
Major version numbers (x.0.0) are reserved for substantial project milestones.
|
- **Major** version numbers (x.0.0) are reserved for substantial project milestones.
|
||||||
|
|
||||||
|
As REST Framework is considered feature-complete, most releases are expected to be minor releases.
|
||||||
|
|
||||||
## Deprecation policy
|
## Deprecation policy
|
||||||
|
|
||||||
|
@ -36,6 +38,23 @@ You can determine your currently installed version using `pip show`:
|
||||||
|
|
||||||
## 3.15.x series
|
## 3.15.x series
|
||||||
|
|
||||||
|
### 3.15.2
|
||||||
|
|
||||||
|
**Date**: 14th June 2024
|
||||||
|
|
||||||
|
* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9435)
|
||||||
|
* Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381)
|
||||||
|
* Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367)
|
||||||
|
* Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393)
|
||||||
|
* Django < 4.2 and Python < 3.8 no longer supported. [#9393](https://github.com/encode/django-rest-framework/pull/9393)
|
||||||
|
|
||||||
|
### 3.15.1
|
||||||
|
|
||||||
|
Date: 22nd March 2024
|
||||||
|
|
||||||
|
* Fix `SearchFilter` handling of quoted and comma separated strings, when `.get_search_terms` is being called into by a custom class. See [[#9338](https://github.com/encode/django-rest-framework/issues/9338)]
|
||||||
|
* Revert number of 3.15.0 issues which included unintended side-effects. See [[#9331](https://github.com/encode/django-rest-framework/issues/9331)]
|
||||||
|
|
||||||
### 3.15.0
|
### 3.15.0
|
||||||
|
|
||||||
Date: 15th March 2024
|
Date: 15th March 2024
|
||||||
|
|
|
@ -46,6 +46,10 @@ Check out a grid detailing all the packages and ecosystem around Django REST Fra
|
||||||
|
|
||||||
To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr].
|
To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr].
|
||||||
|
|
||||||
|
## Async Support
|
||||||
|
|
||||||
|
* [adrf](https://github.com/em1208/adrf) - Async support, provides async Views, ViewSets, and Serializers.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
* [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support.
|
* [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support.
|
||||||
|
@ -125,6 +129,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
|
|
||||||
|
* [drf-sendables][drf-sendables] - User messages for Django REST Framework
|
||||||
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
|
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
|
||||||
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serializer that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
|
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serializer that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
|
||||||
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
|
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
|
||||||
|
@ -157,6 +162,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
|
||||||
* [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5.
|
* [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5.
|
||||||
* [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design.
|
* [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design.
|
||||||
|
|
||||||
|
[drf-sendables]: https://github.com/amikrop/drf-sendables
|
||||||
[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
|
||||||
[new-repo]: https://github.com/new
|
[new-repo]: https://github.com/new
|
||||||
|
|
|
@ -39,6 +39,8 @@ There are a wide range of resources available for learning and using Django REST
|
||||||
* [Check Credentials Using Django REST Framework][check-credentials-using-django-rest-framework]
|
* [Check Credentials Using Django REST Framework][check-credentials-using-django-rest-framework]
|
||||||
* [Creating a Production Ready API with Python and Django REST Framework – Part 1][creating-a-production-ready-api-with-python-and-drf-part1]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 1][creating-a-production-ready-api-with-python-and-drf-part1]
|
||||||
* [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2]
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2]
|
||||||
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 3][creating-a-production-ready-api-with-python-and-drf-part3]
|
||||||
|
* [Creating a Production Ready API with Python and Django REST Framework – Part 4][creating-a-production-ready-api-with-python-and-drf-part4]
|
||||||
* [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog]
|
* [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog]
|
||||||
* [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list]
|
* [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list]
|
||||||
* [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog]
|
* [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog]
|
||||||
|
@ -115,8 +117,10 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
|
||||||
[chatbot-using-drf-part1]: https://chatbotslife.com/chatbot-using-django-rest-framework-api-ai-slack-part-1-3-69c7e38b7b1e#.g2aceuncf
|
[chatbot-using-drf-part1]: https://chatbotslife.com/chatbot-using-django-rest-framework-api-ai-slack-part-1-3-69c7e38b7b1e#.g2aceuncf
|
||||||
[new-django-admin-with-drf-and-emberjs]: https://blog.levit.be/new-django-admin-with-emberjs-what-are-the-news/
|
[new-django-admin-with-drf-and-emberjs]: https://blog.levit.be/new-django-admin-with-emberjs-what-are-the-news/
|
||||||
[drf-schema]: https://drf-schema-adapter.readthedocs.io/en/latest/
|
[drf-schema]: https://drf-schema-adapter.readthedocs.io/en/latest/
|
||||||
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/2016/09/28/creating-production-ready-api-python-django-rest-framework-part-1/
|
[creating-a-production-ready-api-with-python-and-drf-part1]: https://www.andreagrandi.it/posts/creating-production-ready-api-python-django-rest-framework-part-1/
|
||||||
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
|
[creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
|
||||||
|
[creating-a-production-ready-api-with-python-and-drf-part3]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-3/
|
||||||
|
[creating-a-production-ready-api-with-python-and-drf-part4]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-4/
|
||||||
[django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/
|
[django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/
|
||||||
[django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/
|
[django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/
|
||||||
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
|
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
|
||||||
|
|
BIN
docs/img/premium/zuplo-readme.png
Normal file
BIN
docs/img/premium/zuplo-readme.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -75,10 +75,11 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
<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>
|
<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>
|
||||||
<li><a href="https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/svix.png)">Svix</a></li>
|
<li><a href="https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/svix.png)">Svix</a></li>
|
||||||
|
<li><a href="https://zuplo.link/django-web" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/zuplo.png)">Zuplo</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), [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), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), and [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship).*
|
*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), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship), , and [Zuplo](https://zuplo.link/django-web).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -86,8 +87,8 @@ 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, 3.11)
|
* Django (4.2, 5.0, 5.1)
|
||||||
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0)
|
* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13)
|
||||||
|
|
||||||
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.
|
||||||
|
@ -95,8 +96,8 @@ each Python and Django series.
|
||||||
The following packages are optional:
|
The following packages are optional:
|
||||||
|
|
||||||
* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
|
* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
|
||||||
* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
|
* [Markdown][markdown] (3.3.0+) - Markdown support for the browsable API.
|
||||||
* [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing.
|
* [Pygments][pygments] (2.7.0+) - Add syntax highlighting to Markdown processing.
|
||||||
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
||||||
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
||||||
|
|
||||||
|
@ -184,7 +185,7 @@ Can't wait to get started? The [quickstart guide][quickstart] is the fastest way
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See the [Contribution guidelines][contributing] for information on how to clone
|
See the [Contribution guidelines][contributing] for information on how to clone
|
||||||
the repository, run the test suite and contribute changes back to REST
|
the repository, run the test suite and help maintain the code base of REST
|
||||||
Framework.
|
Framework.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
@ -247,7 +248,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[serializer-section]: api-guide/serializers#serializers
|
[serializer-section]: api-guide/serializers#serializers
|
||||||
[modelserializer-section]: api-guide/serializers#modelserializer
|
[modelserializer-section]: api-guide/serializers#modelserializer
|
||||||
[functionview-section]: api-guide/views#function-based-views
|
[functionview-section]: api-guide/views#function-based-views
|
||||||
[sandbox]: https://restframework.herokuapp.com/
|
|
||||||
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
|
||||||
|
|
||||||
[quickstart]: tutorial/quickstart.md
|
[quickstart]: tutorial/quickstart.md
|
||||||
|
|
|
@ -20,9 +20,11 @@ By default, the API will return the format specified by the headers, which in th
|
||||||
To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf:
|
To quickly add authentication to the browesable api, add a routes named `"login"` and `"logout"` under the namespace `"rest_framework"`. DRF provides default routes for this which you can add to your urlconf:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# ...
|
# ...
|
||||||
url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
|
path("api-auth/", include("rest_framework.urls", namespace="rest_framework"))
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ For API clients the most appropriate of these will typically be to use the `Acce
|
||||||
[cite]: https://youtu.be/Wa0VfS2q94Y
|
[cite]: https://youtu.be/Wa0VfS2q94Y
|
||||||
[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation
|
[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation
|
||||||
[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
|
[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
|
||||||
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
|
[transifex-project]: https://explore.transifex.com/django-rest-framework-1/django-rest-framework/
|
||||||
[django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
[django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
|
||||||
[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference
|
[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference
|
||||||
[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS
|
[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS
|
||||||
|
|
|
@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
|
**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. Feel free to clone the repository and see the code in action.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes.
|
||||||
|
|
||||||
content = JSONRenderer().render(serializer.data)
|
content = JSONRenderer().render(serializer.data)
|
||||||
content
|
content
|
||||||
# b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'
|
# b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'
|
||||||
|
|
||||||
Deserialization is similar. First we parse a stream into Python native datatypes...
|
Deserialization is similar. First we parse a stream into Python native datatypes...
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
# True
|
# True
|
||||||
serializer.validated_data
|
serializer.validated_data
|
||||||
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
|
# {'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}
|
||||||
serializer.save()
|
serializer.save()
|
||||||
# <Snippet: Snippet object>
|
# <Snippet: Snippet object>
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply
|
||||||
|
|
||||||
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
|
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
|
||||||
serializer.data
|
serializer.data
|
||||||
# [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
|
# [{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}]
|
||||||
|
|
||||||
## Using ModelSerializers
|
## Using ModelSerializers
|
||||||
|
|
||||||
|
@ -307,7 +307,7 @@ Quit out of the shell...
|
||||||
Validating models...
|
Validating models...
|
||||||
|
|
||||||
0 errors found
|
0 errors found
|
||||||
Django version 4.0, using settings 'tutorial.settings'
|
Django version 5.0, using settings 'tutorial.settings'
|
||||||
Starting Development server at http://127.0.0.1:8000/
|
Starting Development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
@ -321,42 +321,50 @@ You can install httpie using pip:
|
||||||
|
|
||||||
Finally, we can get a list of all of the snippets:
|
Finally, we can get a list of all of the snippets:
|
||||||
|
|
||||||
http http://127.0.0.1:8000/snippets/
|
http GET http://127.0.0.1:8000/snippets/ --unsorted
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
...
|
...
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "",
|
"title": "",
|
||||||
"code": "foo = \"bar\"\n",
|
"code": "foo = \"bar\"\n",
|
||||||
"linenos": false,
|
"linenos": false,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"style": "friendly"
|
"style": "friendly"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "",
|
||||||
|
"code": "print(\"hello, world\")\n",
|
||||||
|
"linenos": false,
|
||||||
|
"language": "python",
|
||||||
|
"style": "friendly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "",
|
||||||
|
"code": "print(\"hello, world\")",
|
||||||
|
"linenos": false,
|
||||||
|
"language": "python",
|
||||||
|
"style": "friendly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Or we can get a particular snippet by referencing its id:
|
||||||
|
|
||||||
|
http GET http://127.0.0.1:8000/snippets/2/ --unsorted
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
...
|
||||||
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"title": "",
|
"title": "",
|
||||||
"code": "print(\"hello, world\")\n",
|
"code": "print(\"hello, world\")\n",
|
||||||
"linenos": false,
|
"linenos": false,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"style": "friendly"
|
"style": "friendly"
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Or we can get a particular snippet by referencing its id:
|
|
||||||
|
|
||||||
http http://127.0.0.1:8000/snippets/2/
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
...
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "",
|
|
||||||
"code": "print(\"hello, world\")\n",
|
|
||||||
"linenos": false,
|
|
||||||
"language": "python",
|
|
||||||
"style": "friendly"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
|
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
|
||||||
|
@ -371,7 +379,6 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
||||||
|
|
||||||
[quickstart]: quickstart.md
|
[quickstart]: quickstart.md
|
||||||
[repo]: https://github.com/encode/rest-framework-tutorial
|
[repo]: https://github.com/encode/rest-framework-tutorial
|
||||||
[sandbox]: https://restframework.herokuapp.com/
|
|
||||||
[venv]: https://docs.python.org/3/library/venv.html
|
[venv]: https://docs.python.org/3/library/venv.html
|
||||||
[tut-2]: 2-requests-and-responses.md
|
[tut-2]: 2-requests-and-responses.md
|
||||||
[httpie]: https://github.com/httpie/httpie#installation
|
[httpie]: https://github.com/httpie/httpie#installation
|
||||||
|
|
|
@ -15,7 +15,6 @@ Create a new Django project named `tutorial`, then start a new app called `quick
|
||||||
source env/bin/activate # On Windows use `env\Scripts\activate`
|
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||||
|
|
||||||
# Install Django and Django REST framework into the virtual environment
|
# Install Django and Django REST framework into the virtual environment
|
||||||
pip install django
|
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
|
|
||||||
# Set up a new project with a single application
|
# Set up a new project with a single application
|
||||||
|
|
|
@ -439,3 +439,17 @@ ul.sponsor {
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* admonition */
|
||||||
|
.admonition {
|
||||||
|
border: .075rem solid #448aff;
|
||||||
|
border-radius: .2rem;
|
||||||
|
margin: 1.5625em 0;
|
||||||
|
padding: 0 .6rem;
|
||||||
|
}
|
||||||
|
.admonition-title {
|
||||||
|
background: #448aff1a;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 -.6rem 1em;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ theme:
|
||||||
custom_dir: docs_theme
|
custom_dir: docs_theme
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
- toc:
|
- toc:
|
||||||
anchorlink: True
|
anchorlink: True
|
||||||
|
|
||||||
|
|
|
@ -12,31 +12,26 @@ classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Framework :: Django :: 3.0",
|
|
||||||
"Framework :: Django :: 3.1",
|
|
||||||
"Framework :: Django :: 3.2",
|
|
||||||
"Framework :: Django :: 4.0",
|
|
||||||
"Framework :: Django :: 4.1",
|
|
||||||
"Framework :: Django :: 4.2",
|
"Framework :: Django :: 4.2",
|
||||||
"Framework :: Django :: 5.0",
|
"Framework :: Django :: 5.0",
|
||||||
|
"Framework :: Django :: 5.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",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.6"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=3.0",
|
"django>=4.2",
|
||||||
'backports.zoneinfo;python_version<"3.9"',
|
'backports.zoneinfo;python_version<"3.9"',
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
@ -50,5 +45,51 @@ Funding = "https://fund.django-rest-framework.org/topics/funding/"
|
||||||
Source = "https://github.com/encode/django-rest-framework"
|
Source = "https://github.com/encode/django-rest-framework"
|
||||||
Changelog = "https://www.django-rest-framework.org/community/release-notes/"
|
Changelog = "https://www.django-rest-framework.org/community/release-notes/"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
zip-safe = false
|
||||||
|
include-package-data = true
|
||||||
|
license-files = ["LICENSE.md"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
namespaces = false
|
namespaces = false
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "--tb=short --strict-markers -ra"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
filterwarnings = ["ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning"]
|
||||||
|
|
||||||
|
[tool.flake8]
|
||||||
|
ignore = "E501,W503,W504"
|
||||||
|
banned-modules = "json = use from rest_framework.utils import json!"
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
skip = [".tox"]
|
||||||
|
atomic = true
|
||||||
|
multi_line_output = 5
|
||||||
|
extra_standard_library = ["types"]
|
||||||
|
known_third_party = ["pytest", "_pytest", "django", "pytz", "uritemplate"]
|
||||||
|
known_first_party = ["rest_framework", "tests"]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
# NOTE: source is ignored with pytest-cov (but uses the same).
|
||||||
|
source = ["."]
|
||||||
|
include = ["rest_framework/*", "tests/*"]
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
include = ["rest_framework/*", "tests/*"]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.codespell]
|
||||||
|
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||||
|
skip = "*/kickstarter-announcement.md,*.js,*.map,*.po"
|
||||||
|
ignore-words-list = "fo,malcom,ser"
|
||||||
|
|
||||||
|
# (*) Please direct queries to the discussion group, rather than to me directly
|
||||||
|
# Doing so helps ensure your question is helpful to other users.
|
||||||
|
# Queries directly to my email are likely to receive a canned response.
|
||||||
|
#
|
||||||
|
# Many thanks for your understanding.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# MkDocs to build our documentation.
|
# MkDocs to build our documentation.
|
||||||
mkdocs==1.2.4
|
mkdocs==1.6.0
|
||||||
jinja2>=2.10,<3.1.0 # contextfilter has been renamed
|
|
||||||
|
|
||||||
# pylinkvalidator to check for broken links in documentation.
|
# pylinkvalidator to check for broken links in documentation.
|
||||||
pylinkvalidator==0.3
|
pylinkvalidator==0.3
|
||||||
|
|
|
@ -6,5 +6,5 @@ django-guardian>=2.4.0,<2.5
|
||||||
inflection==0.5.1
|
inflection==0.5.1
|
||||||
markdown>=3.3.7
|
markdown>=3.3.7
|
||||||
psycopg2-binary>=2.9.5,<2.10
|
psycopg2-binary>=2.9.5,<2.10
|
||||||
pygments>=2.12.0,<2.14.0
|
pygments~=2.17.0
|
||||||
pyyaml>=5.3.1,<5.4
|
pyyaml>=5.3.1,<5.4
|
||||||
|
|
|
@ -7,10 +7,8 @@ ______ _____ _____ _____ __
|
||||||
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import django
|
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.15.0'
|
__version__ = '3.15.2'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 3-Clause'
|
__license__ = 'BSD 3-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
||||||
|
@ -25,11 +23,7 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||||
ISO_8601 = 'iso-8601'
|
ISO_8601 = 'iso-8601'
|
||||||
|
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
class RemovedInDRF316Warning(DeprecationWarning):
|
||||||
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
|
|
||||||
|
|
||||||
|
|
||||||
class RemovedInDRF315Warning(DeprecationWarning):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import django
|
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
|
||||||
default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig'
|
|
|
@ -28,7 +28,6 @@ class TokenAdmin(admin.ModelAdmin):
|
||||||
search_help_text = _('Username')
|
search_help_text = _('Username')
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
actions = None # Actions not compatible with mapped IDs.
|
actions = None # Actions not compatible with mapped IDs.
|
||||||
autocomplete_fields = ("user",)
|
|
||||||
|
|
||||||
def get_changelist(self, request, **kwargs):
|
def get_changelist(self, request, **kwargs):
|
||||||
return TokenChangeList
|
return TokenChangeList
|
||||||
|
|
|
@ -151,30 +151,6 @@ 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if django.VERSION >= (5, 1):
|
if django.VERSION >= (5, 1):
|
||||||
# Django 5.1+: use the stock ip_address_validators function
|
# Django 5.1+: use the stock ip_address_validators function
|
||||||
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
|
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
|
||||||
|
|
|
@ -144,30 +144,17 @@ class ValidationError(APIException):
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
default_detail = _('Invalid input.')
|
default_detail = _('Invalid input.')
|
||||||
default_code = 'invalid'
|
default_code = 'invalid'
|
||||||
default_params = {}
|
|
||||||
|
|
||||||
def __init__(self, detail=None, code=None, params=None):
|
def __init__(self, detail=None, code=None):
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = self.default_detail
|
detail = self.default_detail
|
||||||
if code is None:
|
if code is None:
|
||||||
code = self.default_code
|
code = self.default_code
|
||||||
if params is None:
|
|
||||||
params = self.default_params
|
|
||||||
|
|
||||||
# For validation failures, we may collect many errors together,
|
# For validation failures, we may collect many errors together,
|
||||||
# so the details should always be coerced to a list if not already.
|
# so the details should always be coerced to a list if not already.
|
||||||
if isinstance(detail, str):
|
if isinstance(detail, tuple):
|
||||||
detail = [detail % params]
|
detail = list(detail)
|
||||||
elif isinstance(detail, ValidationError):
|
|
||||||
detail = detail.detail
|
|
||||||
elif isinstance(detail, (list, tuple)):
|
|
||||||
final_detail = []
|
|
||||||
for detail_item in detail:
|
|
||||||
if isinstance(detail_item, ValidationError):
|
|
||||||
final_detail += detail_item.detail
|
|
||||||
else:
|
|
||||||
final_detail += [detail_item % params if isinstance(detail_item, str) else detail_item]
|
|
||||||
detail = final_detail
|
|
||||||
elif not isinstance(detail, dict) and not isinstance(detail, list):
|
elif not isinstance(detail, dict) and not isinstance(detail, list):
|
||||||
detail = [detail]
|
detail = [detail]
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
@ -44,8 +44,6 @@ from rest_framework.utils.formatting import lazy_format
|
||||||
from rest_framework.utils.timezone import valid_datetime
|
from rest_framework.utils.timezone import valid_datetime
|
||||||
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
||||||
|
|
||||||
logger = logging.getLogger("rest_framework.fields")
|
|
||||||
|
|
||||||
|
|
||||||
class empty:
|
class empty:
|
||||||
"""
|
"""
|
||||||
|
@ -988,10 +986,10 @@ class DecimalField(Field):
|
||||||
self.max_value = max_value
|
self.max_value = max_value
|
||||||
self.min_value = min_value
|
self.min_value = min_value
|
||||||
|
|
||||||
if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
|
if self.max_value is not None and not isinstance(self.max_value, (int, decimal.Decimal)):
|
||||||
logger.warning("max_value in DecimalField should be Decimal type.")
|
warnings.warn("max_value should be an integer or Decimal instance.")
|
||||||
if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal):
|
if self.min_value is not None and not isinstance(self.min_value, (int, decimal.Decimal)):
|
||||||
logger.warning("min_value in DecimalField should be Decimal type.")
|
warnings.warn("min_value should be an integer or Decimal instance.")
|
||||||
|
|
||||||
if self.max_digits is not None and self.decimal_places is not None:
|
if self.max_digits is not None and self.decimal_places is not None:
|
||||||
self.max_whole_digits = self.max_digits - self.decimal_places
|
self.max_whole_digits = self.max_digits - self.decimal_places
|
||||||
|
|
|
@ -21,18 +21,20 @@ from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
def search_smart_split(search_terms):
|
def search_smart_split(search_terms):
|
||||||
"""generator that first splits string by spaces, leaving quoted phrases together,
|
"""Returns sanitized search terms as a list."""
|
||||||
then it splits non-quoted phrases by commas.
|
split_terms = []
|
||||||
"""
|
|
||||||
for term in smart_split(search_terms):
|
for term in smart_split(search_terms):
|
||||||
# trim commas to avoid bad matching for quoted phrases
|
# trim commas to avoid bad matching for quoted phrases
|
||||||
term = term.strip(',')
|
term = term.strip(',')
|
||||||
if term.startswith(('"', "'")) and term[0] == term[-1]:
|
if term.startswith(('"', "'")) and term[0] == term[-1]:
|
||||||
# quoted phrases are kept together without any other split
|
# quoted phrases are kept together without any other split
|
||||||
yield unescape_string_literal(term)
|
split_terms.append(unescape_string_literal(term))
|
||||||
else:
|
else:
|
||||||
# non-quoted tokens are split by comma, keeping only non-empty ones
|
# non-quoted tokens are split by comma, keeping only non-empty ones
|
||||||
yield from (sub_term.strip() for sub_term in term.split(',') if sub_term)
|
for sub_term in term.split(','):
|
||||||
|
if sub_term:
|
||||||
|
split_terms.append(sub_term.strip())
|
||||||
|
return split_terms
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterBackend:
|
class BaseFilterBackend:
|
||||||
|
@ -85,7 +87,8 @@ class SearchFilter(BaseFilterBackend):
|
||||||
"""
|
"""
|
||||||
value = request.query_params.get(self.search_param, '')
|
value = request.query_params.get(self.search_param, '')
|
||||||
field = CharField(trim_whitespace=False, allow_blank=True)
|
field = CharField(trim_whitespace=False, allow_blank=True)
|
||||||
return field.run_validation(value)
|
cleaned_value = field.run_validation(value)
|
||||||
|
return search_smart_split(cleaned_value)
|
||||||
|
|
||||||
def construct_search(self, field_name, queryset):
|
def construct_search(self, field_name, queryset):
|
||||||
lookup = self.lookup_prefixes.get(field_name[0])
|
lookup = self.lookup_prefixes.get(field_name[0])
|
||||||
|
@ -111,10 +114,6 @@ class SearchFilter(BaseFilterBackend):
|
||||||
if hasattr(field, "path_infos"):
|
if hasattr(field, "path_infos"):
|
||||||
# Update opts to follow the relation.
|
# Update opts to follow the relation.
|
||||||
opts = field.path_infos[-1].to_opts
|
opts = field.path_infos[-1].to_opts
|
||||||
# django < 4.1
|
|
||||||
elif hasattr(field, 'get_path_info'):
|
|
||||||
# Update opts to follow the relation.
|
|
||||||
opts = field.get_path_info()[-1].to_opts
|
|
||||||
# Otherwise, use the field with icontains.
|
# Otherwise, use the field with icontains.
|
||||||
lookup = 'icontains'
|
lookup = 'icontains'
|
||||||
return LOOKUP_SEP.join([field_name, lookup])
|
return LOOKUP_SEP.join([field_name, lookup])
|
||||||
|
@ -163,7 +162,7 @@ class SearchFilter(BaseFilterBackend):
|
||||||
reduce(
|
reduce(
|
||||||
operator.or_,
|
operator.or_,
|
||||||
(models.Q(**{orm_lookup: term}) for orm_lookup in orm_lookups)
|
(models.Q(**{orm_lookup: term}) for orm_lookup in orm_lookups)
|
||||||
) for term in search_smart_split(search_terms)
|
) for term in search_terms
|
||||||
)
|
)
|
||||||
queryset = queryset.filter(reduce(operator.and_, conditions))
|
queryset = queryset.filter(reduce(operator.and_, conditions))
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
||||||
# Omid Zarin <zarinpy@gmail.com>, 2019
|
# Omid Zarin <zarinpy@gmail.com>, 2019
|
||||||
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
||||||
|
# Sina Amini <general.sina.amini.20@gmail.com>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
|
||||||
|
|
||||||
#: fields.py:816
|
#: fields.py:816
|
||||||
msgid "Enter a valid email address."
|
msgid "Enter a valid email address."
|
||||||
msgstr "پست الکترونیکی صحبح وارد کنید."
|
msgstr "پست الکترونیکی صحیح وارد کنید."
|
||||||
|
|
||||||
#: fields.py:827
|
#: fields.py:827
|
||||||
msgid "This value does not match the required pattern."
|
msgid "This value does not match the required pattern."
|
||||||
|
|
Binary file not shown.
|
@ -7,6 +7,7 @@
|
||||||
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
# Aryan Baghi <ar.baghi.ce@gmail.com>, 2020
|
||||||
# Omid Zarin <zarinpy@gmail.com>, 2019
|
# Omid Zarin <zarinpy@gmail.com>, 2019
|
||||||
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
# Xavier Ordoquy <xordoquy@linovia.com>, 2020
|
||||||
|
# Sina Amini <general.sina.amini.20@gmail.com>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
|
||||||
|
|
||||||
#: fields.py:816
|
#: fields.py:816
|
||||||
msgid "Enter a valid email address."
|
msgid "Enter a valid email address."
|
||||||
msgstr "پست الکترونیکی صحبح وارد کنید."
|
msgstr "پست الکترونیکی صحیح وارد کنید."
|
||||||
|
|
||||||
#: fields.py:827
|
#: fields.py:827
|
||||||
msgid "This value does not match the required pattern."
|
msgid "This value does not match the required pattern."
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 2015
|
# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 2015
|
||||||
# Hugo Leonardo Chalhoub Mendonça <hugoleonardocm@live.com>, 2015
|
# Hugo Leonardo Chalhoub Mendonça <hugoleonardocm@live.com>, 2015
|
||||||
# Jonatas Baldin <jonatas.baldin@gmail.com>, 2017
|
# Jonatas Baldin <jonatas.baldin@gmail.com>, 2017
|
||||||
|
# Gabriel Mitelman Tkacz <gmtkacz@proton.me>, 2024
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django REST framework\n"
|
"Project-Id-Version: Django REST framework\n"
|
||||||
|
@ -106,11 +107,11 @@ msgstr "Ocorreu um erro de servidor."
|
||||||
|
|
||||||
#: exceptions.py:142
|
#: exceptions.py:142
|
||||||
msgid "Invalid input."
|
msgid "Invalid input."
|
||||||
msgstr ""
|
msgstr "Entrada inválida"
|
||||||
|
|
||||||
#: exceptions.py:161
|
#: exceptions.py:161
|
||||||
msgid "Malformed request."
|
msgid "Malformed request."
|
||||||
msgstr "Pedido malformado."
|
msgstr "Requisição malformada."
|
||||||
|
|
||||||
#: exceptions.py:167
|
#: exceptions.py:167
|
||||||
msgid "Incorrect authentication credentials."
|
msgid "Incorrect authentication credentials."
|
||||||
|
@ -149,12 +150,12 @@ msgstr "Pedido foi limitado."
|
||||||
#: exceptions.py:224
|
#: exceptions.py:224
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected available in {wait} second."
|
msgid "Expected available in {wait} second."
|
||||||
msgstr ""
|
msgstr "Disponível em {wait} segundo."
|
||||||
|
|
||||||
#: exceptions.py:225
|
#: exceptions.py:225
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected available in {wait} seconds."
|
msgid "Expected available in {wait} seconds."
|
||||||
msgstr ""
|
msgstr "Disponível em {wait} segundos."
|
||||||
|
|
||||||
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
|
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
|
||||||
#: validators.py:183
|
#: validators.py:183
|
||||||
|
@ -167,15 +168,15 @@ msgstr "Este campo não pode ser nulo."
|
||||||
|
|
||||||
#: fields.py:701
|
#: fields.py:701
|
||||||
msgid "Must be a valid boolean."
|
msgid "Must be a valid boolean."
|
||||||
msgstr ""
|
msgstr "Deve ser um valor booleano válido."
|
||||||
|
|
||||||
#: fields.py:766
|
#: fields.py:766
|
||||||
msgid "Not a valid string."
|
msgid "Not a valid string."
|
||||||
msgstr ""
|
msgstr "Não é uma string válida."
|
||||||
|
|
||||||
#: fields.py:767
|
#: fields.py:767
|
||||||
msgid "This field may not be blank."
|
msgid "This field may not be blank."
|
||||||
msgstr "Este campo não pode ser em branco."
|
msgstr "Este campo não pode estar em branco."
|
||||||
|
|
||||||
#: fields.py:768 fields.py:1881
|
#: fields.py:768 fields.py:1881
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -205,7 +206,7 @@ msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
|
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
|
||||||
"or hyphens."
|
"or hyphens."
|
||||||
msgstr ""
|
msgstr "Digite um \"slug\" válido que consista de letras Unicode, números, sublinhados ou hífens."
|
||||||
|
|
||||||
#: fields.py:854
|
#: fields.py:854
|
||||||
msgid "Enter a valid URL."
|
msgid "Enter a valid URL."
|
||||||
|
@ -213,7 +214,7 @@ msgstr "Entrar um URL válido."
|
||||||
|
|
||||||
#: fields.py:867
|
#: fields.py:867
|
||||||
msgid "Must be a valid UUID."
|
msgid "Must be a valid UUID."
|
||||||
msgstr ""
|
msgstr "Deve ser um UUID válido."
|
||||||
|
|
||||||
#: fields.py:903
|
#: fields.py:903
|
||||||
msgid "Enter a valid IPv4 or IPv6 address."
|
msgid "Enter a valid IPv4 or IPv6 address."
|
||||||
|
@ -271,11 +272,11 @@ msgstr "Necessário uma data e hora mas recebeu uma data."
|
||||||
#: fields.py:1150
|
#: fields.py:1150
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid datetime for the timezone \"{timezone}\"."
|
msgid "Invalid datetime for the timezone \"{timezone}\"."
|
||||||
msgstr ""
|
msgstr "Data e hora inválidas para o fuso horário \"{timezone}\"."
|
||||||
|
|
||||||
#: fields.py:1151
|
#: fields.py:1151
|
||||||
msgid "Datetime value out of range."
|
msgid "Datetime value out of range."
|
||||||
msgstr ""
|
msgstr "Valor de data e hora fora do intervalo."
|
||||||
|
|
||||||
#: fields.py:1236
|
#: fields.py:1236
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -299,7 +300,7 @@ msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format}
|
||||||
#: fields.py:1399 fields.py:1456
|
#: fields.py:1399 fields.py:1456
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "\"{input}\" is not a valid choice."
|
msgid "\"{input}\" is not a valid choice."
|
||||||
msgstr "\"{input}\" não é um escolha válido."
|
msgstr "\"{input}\" não é um escolha válida."
|
||||||
|
|
||||||
#: fields.py:1402
|
#: fields.py:1402
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -309,7 +310,7 @@ msgstr "Mais de {count} itens..."
|
||||||
#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
|
#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected a list of items but got type \"{input_type}\"."
|
msgid "Expected a list of items but got type \"{input_type}\"."
|
||||||
msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"."
|
msgstr "Esperava uma lista de itens, mas recebeu tipo \"{input_type}\"."
|
||||||
|
|
||||||
#: fields.py:1458
|
#: fields.py:1458
|
||||||
msgid "This selection may not be empty."
|
msgid "This selection may not be empty."
|
||||||
|
@ -356,21 +357,21 @@ msgstr "Esta lista não pode estar vazia."
|
||||||
#: fields.py:1605
|
#: fields.py:1605
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has at least {min_length} elements."
|
msgid "Ensure this field has at least {min_length} elements."
|
||||||
msgstr ""
|
msgstr "Certifique-se de que este campo tenha pelo menos {min_length} elementos."
|
||||||
|
|
||||||
#: fields.py:1606
|
#: fields.py:1606
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has no more than {max_length} elements."
|
msgid "Ensure this field has no more than {max_length} elements."
|
||||||
msgstr ""
|
msgstr "Certifique-se de que este campo não tenha mais que {max_length} elementos."
|
||||||
|
|
||||||
#: fields.py:1682
|
#: fields.py:1682
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Expected a dictionary of items but got type \"{input_type}\"."
|
msgid "Expected a dictionary of items but got type \"{input_type}\"."
|
||||||
msgstr "Esperado um dicionário de itens mas recebeu tipo \"{input_type}\"."
|
msgstr "Esperava um dicionário de itens mas recebeu tipo \"{input_type}\"."
|
||||||
|
|
||||||
#: fields.py:1683
|
#: fields.py:1683
|
||||||
msgid "This dictionary may not be empty."
|
msgid "This dictionary may not be empty."
|
||||||
msgstr ""
|
msgstr "Este dicionário não pode estar vazio."
|
||||||
|
|
||||||
#: fields.py:1755
|
#: fields.py:1755
|
||||||
msgid "Value must be valid JSON."
|
msgid "Value must be valid JSON."
|
||||||
|
@ -382,7 +383,7 @@ msgstr "Buscar"
|
||||||
|
|
||||||
#: filters.py:50
|
#: filters.py:50
|
||||||
msgid "A search term."
|
msgid "A search term."
|
||||||
msgstr ""
|
msgstr "Um termo de busca."
|
||||||
|
|
||||||
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
|
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
|
||||||
msgid "Ordering"
|
msgid "Ordering"
|
||||||
|
@ -390,7 +391,7 @@ msgstr "Ordenando"
|
||||||
|
|
||||||
#: filters.py:181
|
#: filters.py:181
|
||||||
msgid "Which field to use when ordering the results."
|
msgid "Which field to use when ordering the results."
|
||||||
msgstr ""
|
msgstr "Qual campo usar ao ordenar os resultados."
|
||||||
|
|
||||||
#: filters.py:287
|
#: filters.py:287
|
||||||
msgid "ascending"
|
msgid "ascending"
|
||||||
|
@ -402,11 +403,11 @@ msgstr "descendente"
|
||||||
|
|
||||||
#: pagination.py:174
|
#: pagination.py:174
|
||||||
msgid "A page number within the paginated result set."
|
msgid "A page number within the paginated result set."
|
||||||
msgstr ""
|
msgstr "Um número de página dentro do conjunto de resultados paginado."
|
||||||
|
|
||||||
#: pagination.py:179 pagination.py:372 pagination.py:590
|
#: pagination.py:179 pagination.py:372 pagination.py:590
|
||||||
msgid "Number of results to return per page."
|
msgid "Number of results to return per page."
|
||||||
msgstr ""
|
msgstr "Número de resultados a serem retornados por página."
|
||||||
|
|
||||||
#: pagination.py:189
|
#: pagination.py:189
|
||||||
msgid "Invalid page."
|
msgid "Invalid page."
|
||||||
|
@ -414,11 +415,11 @@ msgstr "Página inválida."
|
||||||
|
|
||||||
#: pagination.py:374
|
#: pagination.py:374
|
||||||
msgid "The initial index from which to return the results."
|
msgid "The initial index from which to return the results."
|
||||||
msgstr ""
|
msgstr "O índice inicial a partir do qual retornar os resultados."
|
||||||
|
|
||||||
#: pagination.py:581
|
#: pagination.py:581
|
||||||
msgid "The pagination cursor value."
|
msgid "The pagination cursor value."
|
||||||
msgstr ""
|
msgstr "O valor do cursor de paginação."
|
||||||
|
|
||||||
#: pagination.py:583
|
#: pagination.py:583
|
||||||
msgid "Invalid cursor"
|
msgid "Invalid cursor"
|
||||||
|
@ -432,7 +433,7 @@ msgstr "Pk inválido \"{pk_value}\" - objeto não existe."
|
||||||
#: relations.py:247
|
#: relations.py:247
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Incorrect type. Expected pk value, received {data_type}."
|
msgid "Incorrect type. Expected pk value, received {data_type}."
|
||||||
msgstr "Tipo incorreto. Esperado valor pk, recebeu {data_type}."
|
msgstr "Tipo incorreto. Esperava valor pk, recebeu {data_type}."
|
||||||
|
|
||||||
#: relations.py:280
|
#: relations.py:280
|
||||||
msgid "Invalid hyperlink - No URL match."
|
msgid "Invalid hyperlink - No URL match."
|
||||||
|
@ -462,20 +463,20 @@ msgstr "Valor inválido."
|
||||||
|
|
||||||
#: schemas/utils.py:32
|
#: schemas/utils.py:32
|
||||||
msgid "unique integer value"
|
msgid "unique integer value"
|
||||||
msgstr ""
|
msgstr "valor inteiro único"
|
||||||
|
|
||||||
#: schemas/utils.py:34
|
#: schemas/utils.py:34
|
||||||
msgid "UUID string"
|
msgid "UUID string"
|
||||||
msgstr ""
|
msgstr "string UUID"
|
||||||
|
|
||||||
#: schemas/utils.py:36
|
#: schemas/utils.py:36
|
||||||
msgid "unique value"
|
msgid "unique value"
|
||||||
msgstr ""
|
msgstr "valor único"
|
||||||
|
|
||||||
#: schemas/utils.py:38
|
#: schemas/utils.py:38
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "A {value_type} identifying this {name}."
|
msgid "A {value_type} identifying this {name}."
|
||||||
msgstr ""
|
msgstr "Um {value_type} que identifica este {name}."
|
||||||
|
|
||||||
#: serializers.py:337
|
#: serializers.py:337
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -485,7 +486,7 @@ msgstr "Dado inválido. Necessário um dicionário mas recebeu {datatype}."
|
||||||
#: templates/rest_framework/admin.html:116
|
#: templates/rest_framework/admin.html:116
|
||||||
#: templates/rest_framework/base.html:136
|
#: templates/rest_framework/base.html:136
|
||||||
msgid "Extra Actions"
|
msgid "Extra Actions"
|
||||||
msgstr ""
|
msgstr "Ações Extras"
|
||||||
|
|
||||||
#: templates/rest_framework/admin.html:130
|
#: templates/rest_framework/admin.html:130
|
||||||
#: templates/rest_framework/base.html:150
|
#: templates/rest_framework/base.html:150
|
||||||
|
@ -540,7 +541,7 @@ msgstr "Os campos {field_names} devem criar um set único."
|
||||||
#: validators.py:171
|
#: validators.py:171
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Surrogate characters are not allowed: U+{code_point:X}."
|
msgid "Surrogate characters are not allowed: U+{code_point:X}."
|
||||||
msgstr ""
|
msgstr "Caracteres substitutos não são permitidos: U+{code_point:X}."
|
||||||
|
|
||||||
#: validators.py:243
|
#: validators.py:243
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
|
|
@ -353,12 +353,12 @@ msgstr "列表字段不能为空值。"
|
||||||
#: fields.py:1605
|
#: fields.py:1605
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has at least {min_length} elements."
|
msgid "Ensure this field has at least {min_length} elements."
|
||||||
msgstr ""
|
msgstr "请确保这个字段至少包含 {min_length} 个元素。"
|
||||||
|
|
||||||
#: fields.py:1606
|
#: fields.py:1606
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Ensure this field has no more than {max_length} elements."
|
msgid "Ensure this field has no more than {max_length} elements."
|
||||||
msgstr ""
|
msgstr "请确保这个字段不能超过 {max_length} 个元素。"
|
||||||
|
|
||||||
#: fields.py:1682
|
#: fields.py:1682
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
|
@ -367,7 +367,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。"
|
||||||
|
|
||||||
#: fields.py:1683
|
#: fields.py:1683
|
||||||
msgid "This dictionary may not be empty."
|
msgid "This dictionary may not be empty."
|
||||||
msgstr ""
|
msgstr "这个字典可能不是空的。"
|
||||||
|
|
||||||
#: fields.py:1755
|
#: fields.py:1755
|
||||||
msgid "Value must be valid JSON."
|
msgid "Value must be valid JSON."
|
||||||
|
|
|
@ -11,7 +11,6 @@ from django.http import Http404
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from rest_framework import exceptions, serializers
|
from rest_framework import exceptions, serializers
|
||||||
from rest_framework.fields import empty
|
|
||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||||
|
|
||||||
|
@ -150,7 +149,4 @@ class SimpleMetadata(BaseMetadata):
|
||||||
for choice_value, choice_name in field.choices.items()
|
for choice_value, choice_name in field.choices.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
if getattr(field, 'default', None) and field.default != empty and not callable(field.default):
|
|
||||||
field_info['default'] = field.default
|
|
||||||
|
|
||||||
return field_info
|
return field_info
|
||||||
|
|
|
@ -4,8 +4,6 @@ Basic building blocks for generic class based views.
|
||||||
We don't bind behaviour to http method handlers yet,
|
We don't bind behaviour to http method handlers yet,
|
||||||
which allows mixin classes to be composed in interesting ways.
|
which allows mixin classes to be composed in interesting ways.
|
||||||
"""
|
"""
|
||||||
from django.db.models.query import prefetch_related_objects
|
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
@ -69,13 +67,10 @@ class UpdateModelMixin:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_update(serializer)
|
self.perform_update(serializer)
|
||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
if getattr(instance, '_prefetched_objects_cache', None):
|
||||||
if queryset._prefetch_related_lookups:
|
|
||||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
# If 'prefetch_related' has been applied to a queryset, we need to
|
||||||
# forcibly invalidate the prefetch cache on the instance,
|
# forcibly invalidate the prefetch cache on the instance.
|
||||||
# and then re-prefetch related objects
|
|
||||||
instance._prefetched_objects_cache = {}
|
instance._prefetched_objects_cache = {}
|
||||||
prefetch_related_objects([instance], *queryset._prefetch_related_lookups)
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ from urllib import parse
|
||||||
|
|
||||||
from django.core.paginator import InvalidPage
|
from django.core.paginator import InvalidPage
|
||||||
from django.core.paginator import Paginator as DjangoPaginator
|
from django.core.paginator import Paginator as DjangoPaginator
|
||||||
from django.db.models import Q
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -631,7 +630,7 @@ class CursorPagination(BasePagination):
|
||||||
queryset = queryset.order_by(*self.ordering)
|
queryset = queryset.order_by(*self.ordering)
|
||||||
|
|
||||||
# If we have a cursor with a fixed position then filter by that.
|
# If we have a cursor with a fixed position then filter by that.
|
||||||
if str(current_position) != 'None':
|
if current_position is not None:
|
||||||
order = self.ordering[0]
|
order = self.ordering[0]
|
||||||
is_reversed = order.startswith('-')
|
is_reversed = order.startswith('-')
|
||||||
order_attr = order.lstrip('-')
|
order_attr = order.lstrip('-')
|
||||||
|
@ -642,12 +641,7 @@ class CursorPagination(BasePagination):
|
||||||
else:
|
else:
|
||||||
kwargs = {order_attr + '__gt': current_position}
|
kwargs = {order_attr + '__gt': current_position}
|
||||||
|
|
||||||
filter_query = Q(**kwargs)
|
queryset = queryset.filter(**kwargs)
|
||||||
# If some records contain a null for the ordering field, don't lose them.
|
|
||||||
# When reverse ordering, nulls will come last and need to be included.
|
|
||||||
if (reverse and not is_reversed) or is_reversed:
|
|
||||||
filter_query |= Q(**{order_attr + '__isnull': True})
|
|
||||||
queryset = queryset.filter(filter_query)
|
|
||||||
|
|
||||||
# If we have an offset cursor then offset the entire page by that amount.
|
# If we have an offset cursor then offset the entire page by that amount.
|
||||||
# We also always fetch an extra item in order to determine if there is a
|
# We also always fetch an extra item in order to determine if there is a
|
||||||
|
@ -720,7 +714,7 @@ class CursorPagination(BasePagination):
|
||||||
# The item in this position and the item following it
|
# The item in this position and the item following it
|
||||||
# have different positions. We can use this position as
|
# have different positions. We can use this position as
|
||||||
# our marker.
|
# our marker.
|
||||||
has_item_with_unique_position = position is not None
|
has_item_with_unique_position = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# The item in this position has the same position as the item
|
# The item in this position has the same position as the item
|
||||||
|
@ -773,7 +767,7 @@ class CursorPagination(BasePagination):
|
||||||
# The item in this position and the item following it
|
# The item in this position and the item following it
|
||||||
# have different positions. We can use this position as
|
# have different positions. We can use this position as
|
||||||
# our marker.
|
# our marker.
|
||||||
has_item_with_unique_position = position is not None
|
has_item_with_unique_position = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# The item in this position has the same position as the item
|
# The item in this position has the same position as the item
|
||||||
|
@ -896,7 +890,7 @@ class CursorPagination(BasePagination):
|
||||||
attr = instance[field_name]
|
attr = instance[field_name]
|
||||||
else:
|
else:
|
||||||
attr = getattr(instance, field_name)
|
attr = getattr(instance, field_name)
|
||||||
return None if attr is None else str(attr)
|
return str(attr)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
return Response({
|
return Response({
|
||||||
|
|
|
@ -15,9 +15,9 @@ 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
|
from django.http.multipartparser import MultiPartParserError
|
||||||
|
from django.utils.http import parse_header_parameters
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -54,6 +54,9 @@ class OperandHolder(OperationHolderMixin):
|
||||||
self.op2_class == other.op2_class
|
self.op2_class == other.op2_class
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.operator_class, self.op1_class, self.op2_class))
|
||||||
|
|
||||||
|
|
||||||
class AND:
|
class AND:
|
||||||
def __init__(self, op1, op2):
|
def __init__(self, op1, op2):
|
||||||
|
@ -186,9 +189,9 @@ class DjangoModelPermissions(BasePermission):
|
||||||
# Override this if you need to also provide 'view' permissions,
|
# Override this if you need to also provide 'view' permissions,
|
||||||
# or if you want to provide custom permission codes.
|
# or if you want to provide custom permission codes.
|
||||||
perms_map = {
|
perms_map = {
|
||||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
'GET': [],
|
||||||
'OPTIONS': [],
|
'OPTIONS': [],
|
||||||
'HEAD': ['%(app_label)s.view_%(model_name)s'],
|
'HEAD': [],
|
||||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||||
|
@ -239,13 +242,8 @@ class DjangoModelPermissions(BasePermission):
|
||||||
|
|
||||||
queryset = self._queryset(view)
|
queryset = self._queryset(view)
|
||||||
perms = self.get_required_permissions(request.method, queryset.model)
|
perms = self.get_required_permissions(request.method, queryset.model)
|
||||||
change_perm = self.get_required_permissions('PUT', queryset.model)
|
|
||||||
|
|
||||||
user = request.user
|
return request.user.has_perms(perms)
|
||||||
if request.method == 'GET':
|
|
||||||
return user.has_perms(perms) or user.has_perms(change_perm)
|
|
||||||
|
|
||||||
return user.has_perms(perms)
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
|
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
|
||||||
|
|
|
@ -19,12 +19,13 @@ from django.core.paginator import Page
|
||||||
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
|
||||||
|
from django.utils.http import parse_header_parameters
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
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,
|
||||||
parse_header_parameters, pygments_css, yaml
|
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
|
||||||
|
@ -170,6 +171,10 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
|
|
||||||
def get_template_context(self, data, renderer_context):
|
def get_template_context(self, data, renderer_context):
|
||||||
response = renderer_context['response']
|
response = renderer_context['response']
|
||||||
|
# in case a ValidationError is caught the data parameter may be a list
|
||||||
|
# see rest_framework.views.exception_handler
|
||||||
|
if isinstance(data, list):
|
||||||
|
return {'details': data, 'status_code': response.status_code}
|
||||||
if response.exception:
|
if response.exception:
|
||||||
data['status_code'] = response.status_code
|
data['status_code'] = response.status_code
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -16,9 +16,9 @@ from django.conf import settings
|
||||||
from django.http import HttpRequest, QueryDict
|
from django.http import HttpRequest, QueryDict
|
||||||
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 django.utils.http import parse_header_parameters
|
||||||
|
|
||||||
from rest_framework import 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,7 +217,8 @@ class Request:
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
if not _hasattr(self, '_full_data'):
|
if not _hasattr(self, '_full_data'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
return self._full_data
|
return self._full_data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -420,20 +421,14 @@ class Request:
|
||||||
_request = self.__getattribute__("_request")
|
_request = self.__getattribute__("_request")
|
||||||
return getattr(_request, attr)
|
return getattr(_request, attr)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return self.__getattribute__(attr)
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
|
||||||
|
|
||||||
@property
|
|
||||||
def DATA(self):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'`request.DATA` has been deprecated in favor of `request.data` '
|
|
||||||
'since version 3.0, and has been fully removed as of version 3.2.'
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def POST(self):
|
def POST(self):
|
||||||
# Ensure that request.POST uses our request parsing.
|
# Ensure that request.POST uses our request parsing.
|
||||||
if not _hasattr(self, '_data'):
|
if not _hasattr(self, '_data'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
if is_form_media_type(self.content_type):
|
if is_form_media_type(self.content_type):
|
||||||
return self._data
|
return self._data
|
||||||
return QueryDict('', encoding=self._request._encoding)
|
return QueryDict('', encoding=self._request._encoding)
|
||||||
|
@ -444,16 +439,10 @@ class Request:
|
||||||
# Different from the other two cases, which are not valid property
|
# Different from the other two cases, which are not valid property
|
||||||
# names on the WSGIRequest class.
|
# names on the WSGIRequest class.
|
||||||
if not _hasattr(self, '_files'):
|
if not _hasattr(self, '_files'):
|
||||||
self._load_data_and_files()
|
with wrap_attributeerrors():
|
||||||
|
self._load_data_and_files()
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
@property
|
|
||||||
def QUERY_PARAMS(self):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'`request.QUERY_PARAMS` has been deprecated in favor of `request.query_params` '
|
|
||||||
'since version 3.0, and has been fully removed as of version 3.2.'
|
|
||||||
)
|
|
||||||
|
|
||||||
def force_plaintext_errors(self, value):
|
def force_plaintext_errors(self, value):
|
||||||
# Hack to allow our exception handler to force choice of
|
# Hack to allow our exception handler to force choice of
|
||||||
# plaintext or html error responses.
|
# plaintext or html error responses.
|
||||||
|
|
|
@ -79,8 +79,9 @@ class ViewInspector:
|
||||||
view = self.view
|
view = self.view
|
||||||
|
|
||||||
method_name = getattr(view, 'action', method.lower())
|
method_name = getattr(view, 'action', method.lower())
|
||||||
method_docstring = getattr(view, method_name, None).__doc__
|
method_func = getattr(view, method_name, None)
|
||||||
if method_docstring:
|
method_docstring = method_func.__doc__
|
||||||
|
if method_func and method_docstring:
|
||||||
# An explicit docstring on the method or action.
|
# An explicit docstring on the method or action.
|
||||||
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
|
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.db import models
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
RemovedInDRF315Warning, exceptions, renderers, serializers
|
RemovedInDRF316Warning, exceptions, renderers, serializers
|
||||||
)
|
)
|
||||||
from rest_framework.compat import inflection, uritemplate
|
from rest_framework.compat import inflection, uritemplate
|
||||||
from rest_framework.fields import _UnvalidatedField, empty
|
from rest_framework.fields import _UnvalidatedField, empty
|
||||||
|
@ -725,7 +725,7 @@ class AutoSchema(ViewInspector):
|
||||||
def _get_reference(self, serializer):
|
def _get_reference(self, serializer):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Method `_get_reference()` has been renamed to `get_reference()`. "
|
"Method `_get_reference()` has been renamed to `get_reference()`. "
|
||||||
"The old name will be removed in DRF v3.15.",
|
"The old name will be removed in DRF v3.16.",
|
||||||
RemovedInDRF315Warning, stacklevel=2
|
RemovedInDRF316Warning, stacklevel=2
|
||||||
)
|
)
|
||||||
return self.get_reference(serializer)
|
return self.get_reference(serializer)
|
||||||
|
|
|
@ -322,5 +322,5 @@ def break_long_headers(header):
|
||||||
when possible (are comma separated)
|
when possible (are comma separated)
|
||||||
"""
|
"""
|
||||||
if len(header) > 160 and ',' in header:
|
if len(header) > 160 and ',' in header:
|
||||||
header = mark_safe('<br> ' + ', <br>'.join(header.split(',')))
|
header = mark_safe('<br> ' + ', <br>'.join(escape(header).split(',')))
|
||||||
return header
|
return header
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import io
|
import io
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import django
|
|
||||||
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.handlers.wsgi import WSGIHandler
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
|
@ -394,19 +393,7 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
|
||||||
|
|
||||||
cls._override.enable()
|
cls._override.enable()
|
||||||
|
|
||||||
if django.VERSION > (4, 0):
|
cls.addClassCleanup(cls._override.disable)
|
||||||
cls.addClassCleanup(cls._override.disable)
|
cls.addClassCleanup(cleanup_url_patterns, cls)
|
||||||
cls.addClassCleanup(cleanup_url_patterns, cls)
|
|
||||||
|
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
if django.VERSION < (4, 0):
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
super().tearDownClass()
|
|
||||||
cls._override.disable()
|
|
||||||
|
|
||||||
if hasattr(cls, '_module_urlpatterns'):
|
|
||||||
cls._module.urlpatterns = cls._module_urlpatterns
|
|
||||||
else:
|
|
||||||
del cls._module.urlpatterns
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django.urls import URLResolver, include, path, re_path, register_converter
|
from django.urls import URLResolver, include, path, re_path, register_converter
|
||||||
|
from django.urls.converters import get_converters
|
||||||
from django.urls.resolvers import RoutePattern
|
from django.urls.resolvers import RoutePattern
|
||||||
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
def _get_format_path_converter(suffix_kwarg, allowed):
|
def _get_format_path_converter(allowed):
|
||||||
if allowed:
|
if allowed:
|
||||||
if len(allowed) == 1:
|
if len(allowed) == 1:
|
||||||
allowed_pattern = allowed[0]
|
allowed_pattern = allowed[0]
|
||||||
|
@ -23,11 +24,14 @@ def _get_format_path_converter(suffix_kwarg, allowed):
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return '.' + value + '/'
|
return '.' + value + '/'
|
||||||
|
|
||||||
|
return FormatSuffixConverter
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_converter_name(allowed):
|
||||||
converter_name = 'drf_format_suffix'
|
converter_name = 'drf_format_suffix'
|
||||||
if allowed:
|
if allowed:
|
||||||
converter_name += '_' + '_'.join(allowed)
|
converter_name += '_' + '_'.join(allowed)
|
||||||
|
return converter_name
|
||||||
return converter_name, FormatSuffixConverter
|
|
||||||
|
|
||||||
|
|
||||||
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None):
|
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required, suffix_route=None):
|
||||||
|
@ -104,8 +108,10 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||||
else:
|
else:
|
||||||
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
|
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
|
||||||
|
|
||||||
converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed)
|
converter_name = _generate_converter_name(allowed)
|
||||||
register_converter(suffix_converter, converter_name)
|
if converter_name not in get_converters():
|
||||||
|
suffix_converter = _get_format_path_converter(allowed)
|
||||||
|
register_converter(suffix_converter, converter_name)
|
||||||
|
|
||||||
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
|
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ from django.db import models
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
|
||||||
from rest_framework.compat import postgres_fields
|
from rest_framework.compat import postgres_fields
|
||||||
from rest_framework.fields import empty
|
|
||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
|
|
||||||
NUMERIC_FIELD_TYPES = (
|
NUMERIC_FIELD_TYPES = (
|
||||||
|
@ -128,9 +127,6 @@ def get_field_kwargs(field_name, model_field):
|
||||||
kwargs['read_only'] = True
|
kwargs['read_only'] = True
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
if model_field.default is not None and model_field.default != empty and not callable(model_field.default):
|
|
||||||
kwargs['default'] = model_field.default
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -3,7 +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 rest_framework.compat import parse_header_parameters
|
from django.utils.http import parse_header_parameters
|
||||||
|
|
||||||
|
|
||||||
def media_type_matches(lhs, rhs):
|
def media_type_matches(lhs, rhs):
|
||||||
|
|
|
@ -159,17 +159,18 @@ class UniqueTogetherValidator:
|
||||||
queryset = self.filter_queryset(attrs, queryset, serializer)
|
queryset = self.filter_queryset(attrs, queryset, serializer)
|
||||||
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
|
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
|
||||||
|
|
||||||
|
checked_names = [
|
||||||
|
serializer.fields[field_name].source for field_name in self.fields
|
||||||
|
]
|
||||||
# Ignore validation if any field is None
|
# Ignore validation if any field is None
|
||||||
if serializer.instance is None:
|
if serializer.instance is None:
|
||||||
checked_values = [
|
checked_values = [attrs[field_name] for field_name in checked_names]
|
||||||
value for field, value in attrs.items() if field in self.fields
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
# Ignore validation if all field values are unchanged
|
# Ignore validation if all field values are unchanged
|
||||||
checked_values = [
|
checked_values = [
|
||||||
value
|
attrs[field_name]
|
||||||
for field, value in attrs.items()
|
for field_name in checked_names
|
||||||
if field in self.fields and value != getattr(serializer.instance, field)
|
if attrs[field_name] != getattr(serializer.instance, field_name)
|
||||||
]
|
]
|
||||||
|
|
||||||
if checked_values and None not in checked_values and qs_exists(queryset):
|
if checked_values and None not in checked_values and qs_exists(queryset):
|
||||||
|
|
|
@ -119,16 +119,15 @@ class NamespaceVersioning(BaseVersioning):
|
||||||
|
|
||||||
def determine_version(self, request, *args, **kwargs):
|
def determine_version(self, request, *args, **kwargs):
|
||||||
resolver_match = getattr(request, 'resolver_match', None)
|
resolver_match = getattr(request, 'resolver_match', None)
|
||||||
if resolver_match is not None and resolver_match.namespace:
|
if resolver_match is None or not resolver_match.namespace:
|
||||||
# Allow for possibly nested namespaces.
|
return self.default_version
|
||||||
possible_versions = resolver_match.namespace.split(':')
|
|
||||||
for version in possible_versions:
|
|
||||||
if self.is_allowed_version(version):
|
|
||||||
return version
|
|
||||||
|
|
||||||
if not self.is_allowed_version(self.default_version):
|
# Allow for possibly nested namespaces.
|
||||||
raise exceptions.NotFound(self.invalid_version_message)
|
possible_versions = resolver_match.namespace.split(':')
|
||||||
return self.default_version
|
for version in possible_versions:
|
||||||
|
if self.is_allowed_version(version):
|
||||||
|
return version
|
||||||
|
raise exceptions.NotFound(self.invalid_version_message)
|
||||||
|
|
||||||
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||||
if request.version is not None:
|
if request.version is not None:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Provides an APIView class that is the base of all views in REST framework.
|
Provides an APIView class that is the base of all views in REST framework.
|
||||||
"""
|
"""
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import connections, models
|
from django.db import connections, models
|
||||||
|
@ -139,6 +140,11 @@ class APIView(View):
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
view.initkwargs = initkwargs
|
view.initkwargs = initkwargs
|
||||||
|
|
||||||
|
# Exempt all DRF views from Django's LoginRequiredMiddleware. Users should set
|
||||||
|
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||||
|
if DJANGO_VERSION >= (5, 1):
|
||||||
|
view.login_required = False
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
# all other authentication is CSRF exempt.
|
# all other authentication is CSRF exempt.
|
||||||
return csrf_exempt(view)
|
return csrf_exempt(view)
|
||||||
|
|
|
@ -19,6 +19,7 @@ automatically.
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from inspect import getmembers
|
from inspect import getmembers
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from django.utils.decorators import classonlymethod
|
from django.utils.decorators import classonlymethod
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
@ -136,6 +137,12 @@ class ViewSetMixin:
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
view.initkwargs = initkwargs
|
view.initkwargs = initkwargs
|
||||||
view.actions = actions
|
view.actions = actions
|
||||||
|
|
||||||
|
# Exempt from Django's LoginRequiredMiddleware. Users should set
|
||||||
|
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||||
|
if DJANGO_VERSION >= (5, 1):
|
||||||
|
view.login_required = False
|
||||||
|
|
||||||
return csrf_exempt(view)
|
return csrf_exempt(view)
|
||||||
|
|
||||||
def initialize_request(self, request, *args, **kwargs):
|
def initialize_request(self, request, *args, **kwargs):
|
||||||
|
|
30
setup.cfg
30
setup.cfg
|
@ -1,33 +1,3 @@
|
||||||
[tool:pytest]
|
|
||||||
addopts=--tb=short --strict-markers -ra
|
|
||||||
testspath = tests
|
|
||||||
filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = E501,W503,W504
|
ignore = E501,W503,W504
|
||||||
banned-modules = json = use from rest_framework.utils import json!
|
banned-modules = json = use from rest_framework.utils import json!
|
||||||
|
|
||||||
[isort]
|
|
||||||
skip=.tox
|
|
||||||
atomic=true
|
|
||||||
multi_line_output=5
|
|
||||||
extra_standard_library=types
|
|
||||||
known_third_party=pytest,_pytest,django,pytz,uritemplate
|
|
||||||
known_first_party=rest_framework,tests
|
|
||||||
|
|
||||||
[coverage:run]
|
|
||||||
# NOTE: source is ignored with pytest-cov (but uses the same).
|
|
||||||
source = .
|
|
||||||
include = rest_framework/*,tests/*
|
|
||||||
branch = 1
|
|
||||||
|
|
||||||
[coverage:report]
|
|
||||||
include = rest_framework/*,tests/*
|
|
||||||
exclude_lines =
|
|
||||||
pragma: no cover
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
[codespell]
|
|
||||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
|
||||||
skip = */kickstarter-announcement.md,*.js,*.map,*.po
|
|
||||||
ignore-words-list = fo,malcom,ser
|
|
||||||
|
|
120
setup.py
Executable file
120
setup.py
Executable file
|
@ -0,0 +1,120 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from io import open
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
CURRENT_PYTHON = sys.version_info[:2]
|
||||||
|
REQUIRED_PYTHON = (3, 8)
|
||||||
|
|
||||||
|
# This check and everything above must remain compatible with Python 2.7.
|
||||||
|
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
||||||
|
sys.stderr.write("""
|
||||||
|
==========================
|
||||||
|
Unsupported Python version
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This version of Django REST Framework requires Python {}.{}, but you're trying
|
||||||
|
to install it on Python {}.{}.
|
||||||
|
|
||||||
|
This may be because you are using a version of pip that doesn't
|
||||||
|
understand the python_requires classifier. Make sure you
|
||||||
|
have pip >= 9.0 and setuptools >= 24.2, then try again:
|
||||||
|
|
||||||
|
$ python -m pip install --upgrade pip setuptools
|
||||||
|
$ python -m pip install djangorestframework
|
||||||
|
|
||||||
|
This will install the latest version of Django REST Framework which works on
|
||||||
|
your version of Python. If you can't upgrade your pip (or Python), request
|
||||||
|
an older version of Django REST Framework:
|
||||||
|
|
||||||
|
$ python -m pip install "djangorestframework<3.10"
|
||||||
|
""".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def read(f):
|
||||||
|
with open(f, 'r', encoding='utf-8') as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(package):
|
||||||
|
"""
|
||||||
|
Return package version as listed in `__version__` in `init.py`.
|
||||||
|
"""
|
||||||
|
init_py = open(os.path.join(package, '__init__.py')).read()
|
||||||
|
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
||||||
|
|
||||||
|
|
||||||
|
version = get_version('rest_framework')
|
||||||
|
|
||||||
|
|
||||||
|
if sys.argv[-1] == 'publish':
|
||||||
|
if os.system("pip freeze | grep twine"):
|
||||||
|
print("twine not installed.\nUse `pip install twine`.\nExiting.")
|
||||||
|
sys.exit()
|
||||||
|
os.system("python setup.py sdist bdist_wheel")
|
||||||
|
if os.system("twine check dist/*"):
|
||||||
|
print("twine check failed. Packages might be outdated.")
|
||||||
|
print("Try using `pip install -U twine wheel`.\nExiting.")
|
||||||
|
sys.exit()
|
||||||
|
os.system("twine upload dist/*")
|
||||||
|
print("You probably want to also tag the version now:")
|
||||||
|
print(" git tag -a %s -m 'version %s'" % (version, version))
|
||||||
|
print(" git push --tags")
|
||||||
|
shutil.rmtree('dist')
|
||||||
|
shutil.rmtree('build')
|
||||||
|
shutil.rmtree('djangorestframework.egg-info')
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='djangorestframework',
|
||||||
|
version=version,
|
||||||
|
url='https://www.django-rest-framework.org/',
|
||||||
|
license='BSD',
|
||||||
|
description='Web APIs for Django, made easy.',
|
||||||
|
long_description=read('README.md'),
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
author='Tom Christie',
|
||||||
|
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
|
||||||
|
packages=find_packages(exclude=['tests*']),
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=["django>=4.2", 'backports.zoneinfo;python_version<"3.9"'],
|
||||||
|
python_requires=">=3.8",
|
||||||
|
zip_safe=False,
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
'Environment :: Web Environment',
|
||||||
|
'Framework :: Django',
|
||||||
|
'Framework :: Django :: 4.2',
|
||||||
|
'Framework :: Django :: 5.0',
|
||||||
|
'Framework :: Django :: 5.1',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: BSD License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3.11',
|
||||||
|
'Programming Language :: Python :: 3.12',
|
||||||
|
'Programming Language :: Python :: 3.13',
|
||||||
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
|
],
|
||||||
|
project_urls={
|
||||||
|
'Funding': 'https://fund.django-rest-framework.org/topics/funding/',
|
||||||
|
'Source': 'https://github.com/encode/django-rest-framework',
|
||||||
|
'Changelog': 'https://www.django-rest-framework.org/community/release-notes/',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# (*) Please direct queries to the discussion group, rather than to me directly
|
||||||
|
# Doing so helps ensure your question is helpful to other users.
|
||||||
|
# Queries directly to my email are likely to receive a canned response.
|
||||||
|
#
|
||||||
|
# Many thanks for your understanding.
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import django
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -235,21 +234,13 @@ class SessionAuthTests(TestCase):
|
||||||
Ensure POSTing form over session authentication with CSRF token succeeds.
|
Ensure POSTing form over session authentication with CSRF token succeeds.
|
||||||
Regression test for #6088
|
Regression test for #6088
|
||||||
"""
|
"""
|
||||||
# Remove this shim when dropping support for Django 3.0.
|
|
||||||
if django.VERSION < (3, 1):
|
|
||||||
from django.middleware.csrf import _get_new_csrf_token
|
|
||||||
else:
|
|
||||||
from django.middleware.csrf import (
|
|
||||||
_get_new_csrf_string, _mask_cipher_secret
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_new_csrf_token():
|
|
||||||
return _mask_cipher_secret(_get_new_csrf_string())
|
|
||||||
|
|
||||||
self.csrf_client.login(username=self.username, password=self.password)
|
self.csrf_client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
# Set the csrf_token cookie so that CsrfViewMiddleware._get_token() works
|
# Set the csrf_token cookie so that CsrfViewMiddleware._get_token() works
|
||||||
token = _get_new_csrf_token()
|
from django.middleware.csrf import (
|
||||||
|
_get_new_csrf_string, _mask_cipher_secret
|
||||||
|
)
|
||||||
|
token = _mask_cipher_secret(_get_new_csrf_string())
|
||||||
self.csrf_client.cookies[settings.CSRF_COOKIE_NAME] = token
|
self.csrf_client.cookies[settings.CSRF_COOKIE_NAME] = token
|
||||||
|
|
||||||
# Post the token matching the cookie value
|
# Post the token matching the cookie value
|
||||||
|
|
|
@ -13,8 +13,6 @@ def pytest_addoption(parser):
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
# USE_L10N is deprecated, and will be removed in Django 5.0.
|
|
||||||
use_l10n = {"USE_L10N": True} if django.VERSION < (4, 0) else {}
|
|
||||||
settings.configure(
|
settings.configure(
|
||||||
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
||||||
DATABASES={
|
DATABASES={
|
||||||
|
@ -64,7 +62,6 @@ def pytest_configure(config):
|
||||||
PASSWORD_HASHERS=(
|
PASSWORD_HASHERS=(
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
),
|
),
|
||||||
**use_l10n,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# guardian is optional
|
# guardian is optional
|
||||||
|
@ -87,10 +84,7 @@ def pytest_configure(config):
|
||||||
import rest_framework
|
import rest_framework
|
||||||
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
|
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
|
||||||
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||||
if django.VERSION < (4, 2):
|
settings.STORAGES['staticfiles']['BACKEND'] = backend
|
||||||
settings.STATICFILES_STORAGE = backend
|
|
||||||
else:
|
|
||||||
settings.STORAGES['staticfiles']['BACKEND'] = backend
|
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ MARKDOWN_DOCSTRING = """<h2 id="an-example-docstring">an example docstring</h2>
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>indented</p>
|
<p>indented</p>
|
||||||
<h2 id="hash-style-header">hash style header</h2>
|
<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">"alpha"</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">"beta"</span><span class="p">:</span><span class="w"> </span><span class="s2">"this is a string"</span><span class="w"></span><br /><span class="p">}]</span><span class="w"></span><br /></pre></div>
|
<div class="highlight"><pre><span></span><span class="p">[{</span><br /><span class="w"> </span><span class="nt">"alpha"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><br /><span class="w"> </span><span class="nt">"beta"</span><span class="p">:</span><span class="w"> </span><span class="s2">"this is a string"</span><br /><span class="p">}]</span><br /></pre></div>
|
||||||
<p><br /></p>"""
|
<p><br /></p>"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
||||||
from enum import auto
|
from enum import auto
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
@ -1244,25 +1245,29 @@ class TestMinMaxDecimalField(FieldValues):
|
||||||
'20.0': Decimal('20.0'),
|
'20.0': Decimal('20.0'),
|
||||||
}
|
}
|
||||||
invalid_inputs = {
|
invalid_inputs = {
|
||||||
'9.9': ['Ensure this value is greater than or equal to 10.'],
|
'9.9': ['Ensure this value is greater than or equal to 10.0.'],
|
||||||
'20.1': ['Ensure this value is less than or equal to 20.'],
|
'20.1': ['Ensure this value is less than or equal to 20.0.'],
|
||||||
}
|
}
|
||||||
outputs = {}
|
outputs = {}
|
||||||
field = serializers.DecimalField(
|
field = serializers.DecimalField(
|
||||||
max_digits=3, decimal_places=1,
|
max_digits=3, decimal_places=1,
|
||||||
min_value=10, max_value=20
|
min_value=10.0, max_value=20.0
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_warning_when_not_decimal_types(self, caplog):
|
def test_warning_when_not_decimal_types(self, caplog):
|
||||||
import logging
|
with warnings.catch_warnings(record=True) as w:
|
||||||
serializers.DecimalField(
|
warnings.simplefilter('always')
|
||||||
max_digits=3, decimal_places=1,
|
|
||||||
min_value=10, max_value=20
|
serializers.DecimalField(
|
||||||
)
|
max_digits=3, decimal_places=1,
|
||||||
assert caplog.record_tuples == [
|
min_value=10.0, max_value=20.0
|
||||||
("rest_framework.fields", logging.WARNING, "max_value in DecimalField should be Decimal type."),
|
)
|
||||||
("rest_framework.fields", logging.WARNING, "min_value in DecimalField should be Decimal type.")
|
|
||||||
]
|
assert len(w) == 2
|
||||||
|
assert all(issubclass(i.category, UserWarning) for i in w)
|
||||||
|
|
||||||
|
assert 'max_value should be an integer or Decimal instance' in str(w[0].message)
|
||||||
|
assert 'min_value should be an integer or Decimal instance' in str(w[1].message)
|
||||||
|
|
||||||
|
|
||||||
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
||||||
|
@ -1628,7 +1633,7 @@ class TestCustomTimezoneForDateTimeField(TestCase):
|
||||||
assert rendered_date == rendered_date_in_timezone
|
assert rendered_date == rendered_date_in_timezone
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.")
|
@pytest.mark.skipif(pytz is None, reason="Django 5.0 has removed pytz; this test should eventually be able to get removed.")
|
||||||
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.urls import path
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
from rest_framework.decorators import api_view, renderer_classes
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.renderers import TemplateHTMLRenderer
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
@ -34,10 +35,17 @@ def not_found(request):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def validation_error(request):
|
||||||
|
raise ValidationError('error')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', example),
|
path('', example),
|
||||||
path('permission_denied', permission_denied),
|
path('permission_denied', permission_denied),
|
||||||
path('not_found', not_found),
|
path('not_found', not_found),
|
||||||
|
path('validation_error', validation_error),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,6 +99,12 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
self.assertEqual(response.content, b"403 Forbidden")
|
self.assertEqual(response.content, b"403 Forbidden")
|
||||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
|
def test_validation_error_html_view(self):
|
||||||
|
response = self.client.get('/validation_error')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertEqual(response.content, b"400 Bad Request")
|
||||||
|
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||||
|
|
||||||
# 2 tests below are based on order of if statements in corresponding method
|
# 2 tests below are based on order of if statements in corresponding method
|
||||||
# of TemplateHTMLRenderer
|
# of TemplateHTMLRenderer
|
||||||
def test_get_template_names_returns_own_template_name(self):
|
def test_get_template_names_returns_own_template_name(self):
|
||||||
|
|
|
@ -184,135 +184,6 @@ class TestMetadata:
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
|
||||||
def test_actions_with_default(self):
|
|
||||||
"""
|
|
||||||
On generic views OPTIONS should return an 'actions' key with metadata
|
|
||||||
on the fields with default that may be supplied to PUT and POST requests.
|
|
||||||
"""
|
|
||||||
class NestedField(serializers.Serializer):
|
|
||||||
a = serializers.IntegerField(default=2)
|
|
||||||
b = serializers.IntegerField()
|
|
||||||
|
|
||||||
class ExampleSerializer(serializers.Serializer):
|
|
||||||
choice_field = serializers.ChoiceField(['red', 'green', 'blue'], default='red')
|
|
||||||
integer_field = serializers.IntegerField(
|
|
||||||
min_value=1, max_value=1000, default=1
|
|
||||||
)
|
|
||||||
char_field = serializers.CharField(
|
|
||||||
min_length=3, max_length=40, default="example"
|
|
||||||
)
|
|
||||||
list_field = serializers.ListField(
|
|
||||||
child=serializers.ListField(
|
|
||||||
child=serializers.IntegerField(default=1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
nested_field = NestedField()
|
|
||||||
uuid_field = serializers.UUIDField(label="UUID field")
|
|
||||||
|
|
||||||
class ExampleView(views.APIView):
|
|
||||||
"""Example view."""
|
|
||||||
def post(self, request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_serializer(self):
|
|
||||||
return ExampleSerializer()
|
|
||||||
|
|
||||||
view = ExampleView.as_view()
|
|
||||||
response = view(request=request)
|
|
||||||
expected = {
|
|
||||||
'name': 'Example',
|
|
||||||
'description': 'Example view.',
|
|
||||||
'renders': [
|
|
||||||
'application/json',
|
|
||||||
'text/html'
|
|
||||||
],
|
|
||||||
'parses': [
|
|
||||||
'application/json',
|
|
||||||
'application/x-www-form-urlencoded',
|
|
||||||
'multipart/form-data'
|
|
||||||
],
|
|
||||||
'actions': {
|
|
||||||
'POST': {
|
|
||||||
'choice_field': {
|
|
||||||
'type': 'choice',
|
|
||||||
'required': False,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'Choice field',
|
|
||||||
"choices": [
|
|
||||||
{'value': 'red', 'display_name': 'red'},
|
|
||||||
{'value': 'green', 'display_name': 'green'},
|
|
||||||
{'value': 'blue', 'display_name': 'blue'}
|
|
||||||
],
|
|
||||||
'default': 'red'
|
|
||||||
},
|
|
||||||
'integer_field': {
|
|
||||||
'type': 'integer',
|
|
||||||
'required': False,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'Integer field',
|
|
||||||
'min_value': 1,
|
|
||||||
'max_value': 1000,
|
|
||||||
'default': 1
|
|
||||||
},
|
|
||||||
'char_field': {
|
|
||||||
'type': 'string',
|
|
||||||
'required': False,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'Char field',
|
|
||||||
'min_length': 3,
|
|
||||||
'max_length': 40,
|
|
||||||
'default': 'example'
|
|
||||||
},
|
|
||||||
'list_field': {
|
|
||||||
'type': 'list',
|
|
||||||
'required': True,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'List field',
|
|
||||||
'child': {
|
|
||||||
'type': 'list',
|
|
||||||
'required': True,
|
|
||||||
'read_only': False,
|
|
||||||
'child': {
|
|
||||||
'type': 'integer',
|
|
||||||
'required': False,
|
|
||||||
'read_only': False,
|
|
||||||
'default': 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'nested_field': {
|
|
||||||
'type': 'nested object',
|
|
||||||
'required': True,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'Nested field',
|
|
||||||
'children': {
|
|
||||||
'a': {
|
|
||||||
'type': 'integer',
|
|
||||||
'required': False,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'A',
|
|
||||||
'default': 2
|
|
||||||
},
|
|
||||||
'b': {
|
|
||||||
'type': 'integer',
|
|
||||||
'required': True,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'B'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'uuid_field': {
|
|
||||||
'type': 'string',
|
|
||||||
'required': True,
|
|
||||||
'read_only': False,
|
|
||||||
'label': 'UUID field'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == expected
|
|
||||||
|
|
||||||
def test_global_permissions(self):
|
def test_global_permissions(self):
|
||||||
"""
|
"""
|
||||||
If a user does not have global permissions on an action, then any
|
If a user does not have global permissions on an action, then any
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import django
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework.decorators import action, api_view
|
||||||
from rest_framework.request import is_form_media_type
|
from rest_framework.request import is_form_media_type
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.routers import SimpleRouter
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
|
|
||||||
class PostView(APIView):
|
class PostView(APIView):
|
||||||
|
@ -16,9 +23,39 @@ class PostView(APIView):
|
||||||
return Response(data=request.data, status=200)
|
return Response(data=request.data, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class GetAPIView(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
return Response(data="OK", status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def get_func_view(request):
|
||||||
|
return Response(data="OK", status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class ListViewSet(GenericViewSet):
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
response = Response()
|
||||||
|
response.view = self
|
||||||
|
return response
|
||||||
|
|
||||||
|
@action(detail=False, url_path='list-action')
|
||||||
|
def list_action(self, request, *args, **kwargs):
|
||||||
|
response = Response()
|
||||||
|
response.view = self
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
router = SimpleRouter()
|
||||||
|
router.register(r'view-set', ListViewSet, basename='view_set')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
|
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
|
||||||
path('post', PostView.as_view()),
|
path('post', PostView.as_view()),
|
||||||
|
path('get', GetAPIView.as_view()),
|
||||||
|
path('get-func', get_func_view),
|
||||||
|
path('api/', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,3 +111,38 @@ class TestMiddleware(APITestCase):
|
||||||
|
|
||||||
response = self.client.post('/post', {'foo': 'bar'}, format='json')
|
response = self.client.post('/post', {'foo': 'bar'}, format='json')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(django.VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
@override_settings(
|
||||||
|
ROOT_URLCONF='tests.test_middleware',
|
||||||
|
MIDDLEWARE=(
|
||||||
|
# Needed for AuthenticationMiddleware
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
# Needed for LoginRequiredMiddleware
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.LoginRequiredMiddleware',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class TestLoginRequiredMiddlewareCompat(APITestCase):
|
||||||
|
"""
|
||||||
|
Django's 5.1+ LoginRequiredMiddleware should NOT apply to DRF views.
|
||||||
|
|
||||||
|
Instead, users should put IsAuthenticated in their
|
||||||
|
DEFAULT_PERMISSION_CLASSES setting.
|
||||||
|
"""
|
||||||
|
def test_class_based_view(self):
|
||||||
|
response = self.client.get('/get')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_function_based_view(self):
|
||||||
|
response = self.client.get('/get-func')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_viewset_list(self):
|
||||||
|
response = self.client.get('/api/view-set/')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
def test_viewset_list_action(self):
|
||||||
|
response = self.client.get('/api/view-set/list-action/')
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
|
@ -12,7 +12,6 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
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
|
||||||
|
@ -174,7 +173,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
TestSerializer\(\):
|
TestSerializer\(\):
|
||||||
auto_field = IntegerField\(read_only=True\)
|
auto_field = IntegerField\(read_only=True\)
|
||||||
big_integer_field = IntegerField\(.*\)
|
big_integer_field = IntegerField\(.*\)
|
||||||
boolean_field = BooleanField\(default=False, required=False\)
|
boolean_field = BooleanField\(required=False\)
|
||||||
char_field = CharField\(max_length=100\)
|
char_field = CharField\(max_length=100\)
|
||||||
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
|
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
|
||||||
date_field = DateField\(\)
|
date_field = DateField\(\)
|
||||||
|
@ -183,7 +182,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
email_field = EmailField\(max_length=100\)
|
email_field = EmailField\(max_length=100\)
|
||||||
float_field = FloatField\(\)
|
float_field = FloatField\(\)
|
||||||
integer_field = IntegerField\(.*\)
|
integer_field = IntegerField\(.*\)
|
||||||
null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
|
null_boolean_field = BooleanField\(allow_null=True, required=False\)
|
||||||
positive_integer_field = IntegerField\(.*\)
|
positive_integer_field = IntegerField\(.*\)
|
||||||
positive_small_integer_field = IntegerField\(.*\)
|
positive_small_integer_field = IntegerField\(.*\)
|
||||||
slug_field = SlugField\(allow_unicode=False, max_length=100\)
|
slug_field = SlugField\(allow_unicode=False, max_length=100\)
|
||||||
|
@ -210,7 +209,7 @@ class TestRegularFieldMappings(TestCase):
|
||||||
length_limit_field = CharField\(max_length=12, min_length=3\)
|
length_limit_field = CharField\(max_length=12, min_length=3\)
|
||||||
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
|
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
|
||||||
null_field = IntegerField\(allow_null=True,.*required=False\)
|
null_field = IntegerField\(allow_null=True,.*required=False\)
|
||||||
default_field = IntegerField\(default=0,.*required=False\)
|
default_field = IntegerField\(.*required=False\)
|
||||||
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
|
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
|
||||||
choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
||||||
text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
||||||
|
@ -453,14 +452,11 @@ 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'%s))
|
array_field = ListField(allow_empty=False, child=CharField(label='Array field'))
|
||||||
array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False)
|
array_field_with_blank = ListField(child=CharField(label='Array field with blank'), 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')
|
||||||
|
|
|
@ -972,24 +972,17 @@ class TestCursorPagination(CursorPaginationTestsMixin):
|
||||||
def __init__(self, items):
|
def __init__(self, items):
|
||||||
self.items = items
|
self.items = items
|
||||||
|
|
||||||
def filter(self, q):
|
def filter(self, created__gt=None, created__lt=None):
|
||||||
q_args = dict(q.deconstruct()[1])
|
|
||||||
if not q_args:
|
|
||||||
# django 3.0.x artifact
|
|
||||||
q_args = dict(q.deconstruct()[2])
|
|
||||||
created__gt = q_args.get('created__gt')
|
|
||||||
created__lt = q_args.get('created__lt')
|
|
||||||
|
|
||||||
if created__gt is not None:
|
if created__gt is not None:
|
||||||
return MockQuerySet([
|
return MockQuerySet([
|
||||||
item for item in self.items
|
item for item in self.items
|
||||||
if item.created is None or item.created > int(created__gt)
|
if item.created > int(created__gt)
|
||||||
])
|
])
|
||||||
|
|
||||||
assert created__lt is not None
|
assert created__lt is not None
|
||||||
return MockQuerySet([
|
return MockQuerySet([
|
||||||
item for item in self.items
|
item for item in self.items
|
||||||
if item.created is None or item.created < int(created__lt)
|
if item.created < int(created__lt)
|
||||||
])
|
])
|
||||||
|
|
||||||
def order_by(self, *ordering):
|
def order_by(self, *ordering):
|
||||||
|
@ -1108,127 +1101,6 @@ class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase
|
||||||
return (previous, current, next, previous_url, next_url)
|
return (previous, current, next, previous_url, next_url)
|
||||||
|
|
||||||
|
|
||||||
class NullableCursorPaginationModel(models.Model):
|
|
||||||
created = models.IntegerField(null=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCursorPaginationWithNulls(TestCase):
|
|
||||||
"""
|
|
||||||
Unit tests for `pagination.CursorPagination` with ordering on a nullable field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
class ExamplePagination(pagination.CursorPagination):
|
|
||||||
page_size = 1
|
|
||||||
ordering = 'created'
|
|
||||||
|
|
||||||
self.pagination = ExamplePagination()
|
|
||||||
data = [
|
|
||||||
None, None, 3, 4
|
|
||||||
]
|
|
||||||
for idx in data:
|
|
||||||
NullableCursorPaginationModel.objects.create(created=idx)
|
|
||||||
|
|
||||||
self.queryset = NullableCursorPaginationModel.objects.all()
|
|
||||||
|
|
||||||
get_pages = TestCursorPagination.get_pages
|
|
||||||
|
|
||||||
def test_ascending(self):
|
|
||||||
"""Test paginating one row at a time, current should go 1, 2, 3, 4, 3, 2, 1."""
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages('/')
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [3] # [None] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L789
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [4]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [3]
|
|
||||||
assert current == [4]
|
|
||||||
assert next is None
|
|
||||||
assert next_url is None
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [4]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None] # [3] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
def test_descending(self):
|
|
||||||
"""Test paginating one row at a time, current should go 4, 3, 2, 1, 2, 3, 4."""
|
|
||||||
self.pagination.ordering = ('-created',)
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages('/')
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [4]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None] # [4] paging artifact
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None] # [3] paging artifact
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next is None
|
|
||||||
assert next_url is None
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [3]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [3] # [4] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731
|
|
||||||
|
|
||||||
# skip back artifact
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [4]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_displayed_page_numbers():
|
def test_get_displayed_page_numbers():
|
||||||
"""
|
"""
|
||||||
Test our contextual page display function.
|
Test our contextual page display function.
|
||||||
|
|
|
@ -80,8 +80,7 @@ class ModelPermissionsIntegrationTests(TestCase):
|
||||||
user.user_permissions.set([
|
user.user_permissions.set([
|
||||||
Permission.objects.get(codename='add_basicmodel'),
|
Permission.objects.get(codename='add_basicmodel'),
|
||||||
Permission.objects.get(codename='change_basicmodel'),
|
Permission.objects.get(codename='change_basicmodel'),
|
||||||
Permission.objects.get(codename='delete_basicmodel'),
|
Permission.objects.get(codename='delete_basicmodel')
|
||||||
Permission.objects.get(codename='view_basicmodel')
|
|
||||||
])
|
])
|
||||||
|
|
||||||
user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
|
user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
|
||||||
|
@ -140,15 +139,6 @@ class ModelPermissionsIntegrationTests(TestCase):
|
||||||
response = get_queryset_list_view(request, pk=1)
|
response = get_queryset_list_view(request, pk=1)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def test_has_get_permissions(self):
|
|
||||||
request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
||||||
response = root_view(request)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
request = factory.get('/1', HTTP_AUTHORIZATION=self.updateonly_credentials)
|
|
||||||
response = root_view(request, pk=1)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_has_put_permissions(self):
|
def test_has_put_permissions(self):
|
||||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||||
|
@ -166,15 +156,6 @@ class ModelPermissionsIntegrationTests(TestCase):
|
||||||
response = root_view(request, pk=1)
|
response = root_view(request, pk=1)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_does_not_have_get_permissions(self):
|
|
||||||
request = factory.get('/', HTTP_AUTHORIZATION=self.disallowed_credentials)
|
|
||||||
response = root_view(request)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
request = factory.get('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
|
|
||||||
response = root_view(request, pk=1)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
def test_does_not_have_put_permissions(self):
|
def test_does_not_have_put_permissions(self):
|
||||||
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
||||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||||
|
@ -735,3 +716,59 @@ class PermissionsCompositionTests(TestCase):
|
||||||
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
|
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
|
||||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||||
assert hasperm is False
|
assert hasperm is False
|
||||||
|
|
||||||
|
def test_operand_holder_is_hashable(self):
|
||||||
|
assert hash((permissions.IsAuthenticated & permissions.IsAdminUser))
|
||||||
|
|
||||||
|
def test_operand_holder_hash_same_for_same_operands_and_operator(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) == hash(second_operand_holder)
|
||||||
|
|
||||||
|
def test_operand_holder_hash_differs_for_different_operands(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.AllowAny & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
third_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.AllowAny
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) != hash(second_operand_holder)
|
||||||
|
assert hash(first_operand_holder) != hash(third_operand_holder)
|
||||||
|
assert hash(second_operand_holder) != hash(third_operand_holder)
|
||||||
|
|
||||||
|
def test_operand_holder_hash_differs_for_different_operators(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.IsAuthenticated | permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) != hash(second_operand_holder)
|
||||||
|
|
||||||
|
def test_filtering_permissions(self):
|
||||||
|
unfiltered_permissions = [
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.AllowAny,
|
||||||
|
]
|
||||||
|
expected_permissions = [
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered_permissions = [
|
||||||
|
perm for perm
|
||||||
|
in dict.fromkeys(unfiltered_permissions)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert filtered_permissions == expected_permissions
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.db.models.query import Prefetch
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework import generics, serializers
|
from rest_framework import generics, serializers
|
||||||
|
@ -9,84 +8,65 @@ factory = APIRequestFactory()
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
permissions = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_permissions(self, obj):
|
|
||||||
ret = []
|
|
||||||
for g in obj.groups.all():
|
|
||||||
ret.extend([p.pk for p in g.permissions.all()])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username', 'email', 'groups', 'permissions')
|
fields = ('id', 'username', 'email', 'groups')
|
||||||
|
|
||||||
|
|
||||||
class UserRetrieveUpdate(generics.RetrieveUpdateAPIView):
|
class UserUpdate(generics.UpdateAPIView):
|
||||||
queryset = User.objects.exclude(username='exclude').prefetch_related(
|
queryset = User.objects.exclude(username='exclude').prefetch_related('groups')
|
||||||
Prefetch('groups', queryset=Group.objects.exclude(name='exclude')),
|
|
||||||
'groups__permissions',
|
|
||||||
)
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateWithoutPrefetchRelated(generics.UpdateAPIView):
|
|
||||||
queryset = User.objects.exclude(username='exclude')
|
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
|
||||||
class TestPrefetchRelatedUpdates(TestCase):
|
class TestPrefetchRelatedUpdates(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(username='tom', email='tom@example.com')
|
self.user = User.objects.create(username='tom', email='tom@example.com')
|
||||||
self.groups = [Group.objects.create(name=f'group {i}') for i in range(10)]
|
self.groups = [Group.objects.create(name='a'), Group.objects.create(name='b')]
|
||||||
self.user.groups.set(self.groups)
|
self.user.groups.set(self.groups)
|
||||||
self.user.groups.add(Group.objects.create(name='exclude'))
|
|
||||||
self.expected = {
|
|
||||||
'id': self.user.pk,
|
|
||||||
'username': 'tom',
|
|
||||||
'groups': [group.pk for group in self.groups],
|
|
||||||
'email': 'tom@example.com',
|
|
||||||
'permissions': [],
|
|
||||||
}
|
|
||||||
self.view = UserRetrieveUpdate.as_view()
|
|
||||||
|
|
||||||
def test_prefetch_related_updates(self):
|
def test_prefetch_related_updates(self):
|
||||||
self.groups.append(Group.objects.create(name='c'))
|
view = UserUpdate.as_view()
|
||||||
request = factory.put(
|
pk = self.user.pk
|
||||||
'/', {'username': 'new', 'groups': [group.pk for group in self.groups]}, format='json'
|
groups_pk = self.groups[0].pk
|
||||||
)
|
request = factory.put('/', {'username': 'new', 'groups': [groups_pk]}, format='json')
|
||||||
self.expected['username'] = 'new'
|
response = view(request, pk=pk)
|
||||||
self.expected['groups'] = [group.pk for group in self.groups]
|
assert User.objects.get(pk=pk).groups.count() == 1
|
||||||
response = self.view(request, pk=self.user.pk)
|
expected = {
|
||||||
assert User.objects.get(pk=self.user.pk).groups.count() == 12
|
'id': pk,
|
||||||
assert response.data == self.expected
|
'username': 'new',
|
||||||
# Update and fetch should get same result
|
'groups': [1],
|
||||||
request = factory.get('/')
|
'email': 'tom@example.com'
|
||||||
response = self.view(request, pk=self.user.pk)
|
}
|
||||||
assert response.data == self.expected
|
assert response.data == expected
|
||||||
|
|
||||||
def test_prefetch_related_excluding_instance_from_original_queryset(self):
|
def test_prefetch_related_excluding_instance_from_original_queryset(self):
|
||||||
"""
|
"""
|
||||||
Regression test for https://github.com/encode/django-rest-framework/issues/4661
|
Regression test for https://github.com/encode/django-rest-framework/issues/4661
|
||||||
"""
|
"""
|
||||||
request = factory.put(
|
view = UserUpdate.as_view()
|
||||||
'/', {'username': 'exclude', 'groups': [self.groups[0].pk]}, format='json'
|
pk = self.user.pk
|
||||||
)
|
groups_pk = self.groups[0].pk
|
||||||
response = self.view(request, pk=self.user.pk)
|
request = factory.put('/', {'username': 'exclude', 'groups': [groups_pk]}, format='json')
|
||||||
assert User.objects.get(pk=self.user.pk).groups.count() == 2
|
response = view(request, pk=pk)
|
||||||
self.expected['username'] = 'exclude'
|
assert User.objects.get(pk=pk).groups.count() == 1
|
||||||
self.expected['groups'] = [self.groups[0].pk]
|
expected = {
|
||||||
assert response.data == self.expected
|
'id': pk,
|
||||||
|
'username': 'exclude',
|
||||||
|
'groups': [1],
|
||||||
|
'email': 'tom@example.com'
|
||||||
|
}
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
def test_db_query_count(self):
|
def test_can_update_without_queryset_on_class_view(self):
|
||||||
request = factory.put(
|
class UserUpdateWithoutQuerySet(generics.UpdateAPIView):
|
||||||
'/', {'username': 'new'}, format='json'
|
serializer_class = UserSerializer
|
||||||
)
|
|
||||||
with self.assertNumQueries(7):
|
|
||||||
self.view(request, pk=self.user.pk)
|
|
||||||
|
|
||||||
request = factory.put(
|
def get_object(self):
|
||||||
'/', {'username': 'new2'}, format='json'
|
return User.objects.get(pk=self.kwargs['pk'])
|
||||||
)
|
|
||||||
with self.assertNumQueries(16):
|
request = factory.patch('/', {'username': 'new'})
|
||||||
UserUpdateWithoutPrefetchRelated.as_view()(request, pk=self.user.pk)
|
response = UserUpdateWithoutQuerySet.as_view()(request, pk=self.user.pk)
|
||||||
|
assert response.data['id'] == self.user.id
|
||||||
|
assert response.data['username'] == 'new'
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
assert self.user.username == 'new'
|
||||||
|
|
|
@ -910,7 +910,7 @@ class TestDocumentationRenderer(TestCase):
|
||||||
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
||||||
}
|
}
|
||||||
html = template.render(context)
|
html = template.render(context)
|
||||||
assert 'testcases list' in html
|
assert 'testcases<span class="w"> </span>list' in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||||
|
|
|
@ -126,6 +126,25 @@ class TestContentParsing(TestCase):
|
||||||
request.parsers = (PlainTextParser(), )
|
request.parsers = (PlainTextParser(), )
|
||||||
assert request.data == content
|
assert request.data == content
|
||||||
|
|
||||||
|
def test_calling_data_fails_when_attribute_error_is_raised(self):
|
||||||
|
"""
|
||||||
|
Ensure attribute errors raised when parsing are properly re-raised.
|
||||||
|
"""
|
||||||
|
expected_message = "Internal error"
|
||||||
|
|
||||||
|
class BrokenParser:
|
||||||
|
media_type = "application/json"
|
||||||
|
|
||||||
|
def parse(self, *args, **kwargs):
|
||||||
|
raise AttributeError(expected_message)
|
||||||
|
|
||||||
|
http_request = factory.post('/', data={}, format="json")
|
||||||
|
request = Request(http_request)
|
||||||
|
request.parsers = (BrokenParser,)
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(WrappedAttributeError, expected_message):
|
||||||
|
request.data
|
||||||
|
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
authentication_classes = (SessionAuthentication,)
|
authentication_classes = (SessionAuthentication,)
|
||||||
|
|
|
@ -2,7 +2,6 @@ import itertools
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import django
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -319,10 +318,6 @@ class TestAPIRequestFactory(TestCase):
|
||||||
assert request.META['CONTENT_TYPE'] == 'application/json'
|
assert request.META['CONTENT_TYPE'] == 'application/json'
|
||||||
|
|
||||||
|
|
||||||
def check_urlpatterns(cls):
|
|
||||||
assert urlpatterns is not cls.urlpatterns
|
|
||||||
|
|
||||||
|
|
||||||
class TestUrlPatternTestCase(URLPatternsTestCase):
|
class TestUrlPatternTestCase(URLPatternsTestCase):
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', view),
|
path('', view),
|
||||||
|
@ -334,18 +329,11 @@ class TestUrlPatternTestCase(URLPatternsTestCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
assert urlpatterns is cls.urlpatterns
|
assert urlpatterns is cls.urlpatterns
|
||||||
|
|
||||||
if django.VERSION > (4, 0):
|
@classmethod
|
||||||
cls.addClassCleanup(
|
def doClassCleanups(cls):
|
||||||
check_urlpatterns,
|
assert urlpatterns is cls.urlpatterns
|
||||||
cls
|
super().doClassCleanups()
|
||||||
)
|
assert urlpatterns is not cls.urlpatterns
|
||||||
|
|
||||||
if django.VERSION < (4, 0):
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
assert urlpatterns is cls.urlpatterns
|
|
||||||
super().tearDownClass()
|
|
||||||
assert urlpatterns is not cls.urlpatterns
|
|
||||||
|
|
||||||
def test_urlpatterns(self):
|
def test_urlpatterns(self):
|
||||||
assert self.client.get('/').status_code == 200
|
assert self.client.get('/').status_code == 200
|
||||||
|
|
|
@ -109,89 +109,3 @@ class TestValidationErrorConvertsTuplesToLists(TestCase):
|
||||||
assert len(error.detail) == 2
|
assert len(error.detail) == 2
|
||||||
assert str(error.detail[0]) == 'message1'
|
assert str(error.detail[0]) == 'message1'
|
||||||
assert str(error.detail[1]) == 'message2'
|
assert str(error.detail[1]) == 'message2'
|
||||||
|
|
||||||
|
|
||||||
class TestValidationErrorWithDjangoStyle(TestCase):
|
|
||||||
def test_validation_error_details(self):
|
|
||||||
error = ValidationError('Invalid value: %(value)s', params={'value': '42'})
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
|
|
||||||
def test_validation_error_details_tuple(self):
|
|
||||||
error = ValidationError(
|
|
||||||
detail=('Invalid value: %(value1)s', 'Invalid value: %(value2)s'),
|
|
||||||
params={'value1': '42', 'value2': '43'},
|
|
||||||
)
|
|
||||||
assert isinstance(error.detail, list)
|
|
||||||
assert len(error.detail) == 2
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
assert str(error.detail[1]) == 'Invalid value: 43'
|
|
||||||
|
|
||||||
def test_validation_error_details_list(self):
|
|
||||||
error = ValidationError(
|
|
||||||
detail=['Invalid value: %(value1)s', 'Invalid value: %(value2)s', ],
|
|
||||||
params={'value1': '42', 'value2': '43'}
|
|
||||||
)
|
|
||||||
assert isinstance(error.detail, list)
|
|
||||||
assert len(error.detail) == 2
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
assert str(error.detail[1]) == 'Invalid value: 43'
|
|
||||||
|
|
||||||
def test_validation_error_details_validation_errors(self):
|
|
||||||
error = ValidationError(
|
|
||||||
detail=ValidationError(
|
|
||||||
detail='Invalid value: %(value1)s',
|
|
||||||
params={'value1': '42'},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
assert isinstance(error.detail, list)
|
|
||||||
assert len(error.detail) == 1
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
|
|
||||||
def test_validation_error_details_validation_errors_list(self):
|
|
||||||
error = ValidationError(
|
|
||||||
detail=[
|
|
||||||
ValidationError(
|
|
||||||
detail='Invalid value: %(value1)s',
|
|
||||||
params={'value1': '42'},
|
|
||||||
),
|
|
||||||
ValidationError(
|
|
||||||
detail='Invalid value: %(value2)s',
|
|
||||||
params={'value2': '43'},
|
|
||||||
),
|
|
||||||
'Invalid value: %(value3)s'
|
|
||||||
],
|
|
||||||
params={'value3': '44'}
|
|
||||||
)
|
|
||||||
assert isinstance(error.detail, list)
|
|
||||||
assert len(error.detail) == 3
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
assert str(error.detail[1]) == 'Invalid value: 43'
|
|
||||||
assert str(error.detail[2]) == 'Invalid value: 44'
|
|
||||||
|
|
||||||
def test_validation_error_details_validation_errors_nested_list(self):
|
|
||||||
error = ValidationError(
|
|
||||||
detail=[
|
|
||||||
ValidationError(
|
|
||||||
detail='Invalid value: %(value1)s',
|
|
||||||
params={'value1': '42'},
|
|
||||||
),
|
|
||||||
ValidationError(
|
|
||||||
detail=[
|
|
||||||
'Invalid value: %(value2)s',
|
|
||||||
ValidationError(
|
|
||||||
detail='Invalid value: %(value3)s',
|
|
||||||
params={'value3': '44'},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
params={'value2': '43'},
|
|
||||||
),
|
|
||||||
'Invalid value: %(value4)s'
|
|
||||||
],
|
|
||||||
params={'value4': '45'}
|
|
||||||
)
|
|
||||||
assert isinstance(error.detail, list)
|
|
||||||
assert len(error.detail) == 4
|
|
||||||
assert str(error.detail[0]) == 'Invalid value: 42'
|
|
||||||
assert str(error.detail[1]) == 'Invalid value: 43'
|
|
||||||
assert str(error.detail[2]) == 'Invalid value: 44'
|
|
||||||
assert str(error.detail[3]) == 'Invalid value: 45'
|
|
||||||
|
|
|
@ -469,6 +469,28 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert not mock.called
|
assert not mock.called
|
||||||
|
|
||||||
|
@patch("rest_framework.validators.qs_exists")
|
||||||
|
def test_unique_together_with_source(self, mock_qs_exists):
|
||||||
|
class UniqueTogetherWithSourceSerializer(serializers.ModelSerializer):
|
||||||
|
name = serializers.CharField(source="race_name")
|
||||||
|
pos = serializers.IntegerField(source="position")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UniquenessTogetherModel
|
||||||
|
fields = ["name", "pos"]
|
||||||
|
|
||||||
|
data = {"name": "Paris Marathon", "pos": 1}
|
||||||
|
instance = UniquenessTogetherModel.objects.create(
|
||||||
|
race_name="Paris Marathon", position=1
|
||||||
|
)
|
||||||
|
serializer = UniqueTogetherWithSourceSerializer(data=data)
|
||||||
|
assert not serializer.is_valid()
|
||||||
|
assert mock_qs_exists.called
|
||||||
|
mock_qs_exists.reset_mock()
|
||||||
|
serializer = UniqueTogetherWithSourceSerializer(data=data, instance=instance)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert not mock_qs_exists.called
|
||||||
|
|
||||||
def test_filter_queryset_do_not_skip_existing_attribute(self):
|
def test_filter_queryset_do_not_skip_existing_attribute(self):
|
||||||
"""
|
"""
|
||||||
filter_queryset should add value from existing instance attribute
|
filter_queryset should add value from existing instance attribute
|
||||||
|
|
|
@ -272,7 +272,7 @@ class TestInvalidVersion:
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
class TestAcceptHeaderAllowedAndDefaultVersion:
|
class TestAllowedAndDefaultVersion:
|
||||||
def test_missing_without_default(self):
|
def test_missing_without_default(self):
|
||||||
scheme = versioning.AcceptHeaderVersioning
|
scheme = versioning.AcceptHeaderVersioning
|
||||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
||||||
|
@ -318,97 +318,6 @@ class TestAcceptHeaderAllowedAndDefaultVersion:
|
||||||
assert response.data == {'version': 'v2'}
|
assert response.data == {'version': 'v2'}
|
||||||
|
|
||||||
|
|
||||||
class TestNamespaceAllowedAndDefaultVersion:
|
|
||||||
def test_no_namespace_without_default(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = None
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
||||||
|
|
||||||
def test_no_namespace_with_default(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = None
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == {'version': 'v2'}
|
|
||||||
|
|
||||||
def test_no_match_without_default(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = 'no_match'
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
||||||
|
|
||||||
def test_no_match_with_default(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = 'no_match'
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == {'version': 'v2'}
|
|
||||||
|
|
||||||
def test_with_default(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = 'v1'
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == {'version': 'v1'}
|
|
||||||
|
|
||||||
def test_no_match_without_default_but_none_allowed(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = 'no_match'
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == {'version': None}
|
|
||||||
|
|
||||||
def test_no_match_with_default_and_none_allowed(self):
|
|
||||||
class FakeResolverMatch:
|
|
||||||
namespace = 'no_match'
|
|
||||||
|
|
||||||
scheme = versioning.NamespaceVersioning
|
|
||||||
view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme)
|
|
||||||
|
|
||||||
request = factory.get('/endpoint/')
|
|
||||||
request.resolver_match = FakeResolverMatch
|
|
||||||
response = view(request)
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
|
||||||
assert response.data == {'version': 'v2'}
|
|
||||||
|
|
||||||
|
|
||||||
class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
|
class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
|
||||||
included = [
|
included = [
|
||||||
path('namespaced/<int:pk>/', dummy_pk_view, name='namespaced'),
|
path('namespaced/<int:pk>/', dummy_pk_view, name='namespaced'),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import copy
|
import copy
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
@ -136,3 +138,13 @@ class TestCustomSettings(TestCase):
|
||||||
response = self.view(request)
|
response = self.view(request)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.data == {'error': 'SyntaxError'}
|
assert response.data == {'error': 'SyntaxError'}
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
class TestLoginRequiredMiddlewareCompat(TestCase):
|
||||||
|
def test_class_based_view_opted_out(self):
|
||||||
|
class_based_view = BasicView.as_view()
|
||||||
|
assert class_based_view.login_required is False
|
||||||
|
|
||||||
|
def test_function_based_view_opted_out(self):
|
||||||
|
assert basic_view.login_required is False
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import unittest
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
@ -196,6 +198,11 @@ class InitializeViewSetsTestCase(TestCase):
|
||||||
assert get.view.action == 'list_action'
|
assert get.view.action == 'list_action'
|
||||||
assert head.view.action == 'list_action'
|
assert head.view.action == 'list_action'
|
||||||
|
|
||||||
|
@unittest.skipUnless(DJANGO_VERSION >= (5, 1), 'Only for Django 5.1+')
|
||||||
|
def test_login_required_middleware_compat(self):
|
||||||
|
view = ActionViewSet.as_view(actions={'get': 'list'})
|
||||||
|
assert view.login_required is False
|
||||||
|
|
||||||
|
|
||||||
class GetExtraActionsTests(TestCase):
|
class GetExtraActionsTests(TestCase):
|
||||||
|
|
||||||
|
|
23
tox.ini
23
tox.ini
|
@ -1,11 +1,10 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py36,py37,py38,py39}-django30
|
{py38,py39}-{django42}
|
||||||
{py36,py37,py38,py39}-django31
|
{py310}-{django42,django50,django51,djangomain}
|
||||||
{py36,py37,py38,py39,py310}-django32
|
{py311}-{django42,django50,django51,djangomain}
|
||||||
{py38,py39,py310}-{django40,django41,django42,djangomain}
|
{py312}-{django42,django50,django51,djangomain}
|
||||||
{py311}-{django41,django42,django50,djangomain}
|
{py313}-{django51,djangomain}
|
||||||
{py312}-{django42,djanggo50,djangomain}
|
|
||||||
base
|
base
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
@ -18,13 +17,9 @@ setenv =
|
||||||
PYTHONDONTWRITEBYTECODE=1
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
PYTHONWARNINGS=once
|
PYTHONWARNINGS=once
|
||||||
deps =
|
deps =
|
||||||
django30: Django>=3.0,<3.1
|
|
||||||
django31: Django>=3.1,<3.2
|
|
||||||
django32: Django>=3.2,<4.0
|
|
||||||
django40: Django>=4.0,<4.1
|
|
||||||
django41: Django>=4.1,<4.2
|
|
||||||
django42: Django>=4.2,<5.0
|
django42: Django>=4.2,<5.0
|
||||||
django50: Django>=5.0,<5.1
|
django50: Django>=5.0,<5.1
|
||||||
|
django51: Django>=5.1,<5.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
|
||||||
|
@ -50,12 +45,6 @@ deps =
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-documentation.txt
|
-rrequirements/requirements-documentation.txt
|
||||||
|
|
||||||
[testenv:py38-djangomain]
|
|
||||||
ignore_outcome = true
|
|
||||||
|
|
||||||
[testenv:py39-djangomain]
|
|
||||||
ignore_outcome = true
|
|
||||||
|
|
||||||
[testenv:py310-djangomain]
|
[testenv:py310-djangomain]
|
||||||
ignore_outcome = true
|
ignore_outcome = true
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user