Merge branch 'encode:master' into add-max-page-setting-for-pagination

This commit is contained in:
Stanislav Khlud 2024-09-13 12:51:35 +07:00 committed by GitHub
commit 6250dc0d6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 195 additions and 58 deletions

View File

@ -19,6 +19,7 @@ jobs:
- '3.10' - '3.10'
- '3.11' - '3.11'
- '3.12' - '3.12'
- '3.13-dev'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -36,7 +37,7 @@ jobs:
run: python -m pip install --upgrade codecov tox run: python -m pip install --upgrade codecov tox
- name: Run tox targets for ${{ matrix.python-version }} - name: Run tox targets for ${{ matrix.python-version }}
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-')
- name: Run extra tox targets - name: Run extra tox targets
if: ${{ matrix.python-version == '3.9' }} if: ${{ matrix.python-version == '3.9' }}

View File

@ -55,7 +55,7 @@ Some reasons you might want to use REST framework:
# Requirements # Requirements
* Python 3.8+ * Python 3.8+
* Django 5.0, 4.2 * 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.

View File

@ -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

View File

@ -291,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`.

View File

@ -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

View File

@ -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

View File

@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following: REST framework requires the following:
* Django (4.2, 5.0) * Django (4.2, 5.0, 5.1)
* Python (3.8, 3.9, 3.10, 3.11, 3.12) * 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.

View File

@ -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"))
] ]
``` ```

View File

@ -986,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)):
warnings.warn("max_value should be a Decimal instance.") 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)):
warnings.warn("min_value should be a Decimal instance.") 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

View File

@ -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."

View File

@ -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."

View File

@ -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."

View File

@ -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:

View File

@ -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,7 +108,9 @@ 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)
if converter_name not in get_converters():
suffix_converter = _get_format_path_converter(allowed)
register_converter(suffix_converter, converter_name) register_converter(suffix_converter, converter_name)
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg) suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)

View File

@ -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)

View File

@ -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):

View File

@ -91,6 +91,7 @@ setup(
'Framework :: Django', 'Framework :: Django',
'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',
@ -101,6 +102,7 @@ setup(
'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',
], ],

View File

@ -1245,13 +1245,13 @@ 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):
@ -1260,14 +1260,14 @@ class TestMinMaxDecimalField(FieldValues):
serializers.DecimalField( 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
) )
assert len(w) == 2 assert len(w) == 2
assert all(issubclass(i.category, UserWarning) for i in w) assert all(issubclass(i.category, UserWarning) for i in w)
assert 'max_value should be a Decimal instance' in str(w[0].message) assert 'max_value should be an integer or Decimal instance' in str(w[0].message)
assert 'min_value should be a Decimal instance' in str(w[1].message) assert 'min_value should be an integer or Decimal instance' in str(w[1].message)
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues): class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):

View File

@ -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

View File

@ -56,3 +56,17 @@ class TestPrefetchRelatedUpdates(TestCase):
'email': 'tom@example.com' 'email': 'tom@example.com'
} }
assert response.data == expected assert response.data == expected
def test_can_update_without_queryset_on_class_view(self):
class UserUpdateWithoutQuerySet(generics.UpdateAPIView):
serializer_class = UserSerializer
def get_object(self):
return User.objects.get(pk=self.kwargs['pk'])
request = factory.patch('/', {'username': 'new'})
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'

View File

@ -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

View File

@ -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):

14
tox.ini
View File

@ -1,9 +1,10 @@
[tox] [tox]
envlist = envlist =
{py38,py39}-{django42} {py38,py39}-{django42}
{py310}-{django42,django50,djangomain} {py310}-{django42,django50,django51,djangomain}
{py311}-{django42,django50,djangomain} {py311}-{django42,django50,django51,djangomain}
{py312}-{django42,django50,djangomain} {py312}-{django42,django50,django51,djangomain}
{py313}-{django51,djangomain}
base base
dist dist
docs docs
@ -17,6 +18,7 @@ setenv =
deps = deps =
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
@ -42,12 +44,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