Skip to content

Commit 6250dc0

Browse files
Merge branch 'encode:master' into add-max-page-setting-for-pagination
2 parents b51141c + b25028a commit 6250dc0

File tree

24 files changed

+195
-58
lines changed

24 files changed

+195
-58
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
- '3.10'
2020
- '3.11'
2121
- '3.12'
22+
- '3.13-dev'
2223

2324
steps:
2425
- uses: actions/checkout@v4
@@ -36,7 +37,7 @@ jobs:
3637
run: python -m pip install --upgrade codecov tox
3738

3839
- name: Run tox targets for ${{ matrix.python-version }}
39-
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
40+
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-')
4041

4142
- name: Run extra tox targets
4243
if: ${{ matrix.python-version == '3.9' }}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Some reasons you might want to use REST framework:
5555
# Requirements
5656

5757
* Python 3.8+
58-
* Django 5.0, 4.2
58+
* Django 4.2, 5.0, 5.1
5959

6060
We **highly recommend** and only officially support the latest patch release of
6161
each Python and Django series.

docs/api-guide/authentication.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ The kind of response that will be used depends on the authentication scheme. Al
9090

9191
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.
9292

93+
## Django 5.1+ `LoginRequiredMiddleware`
94+
95+
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.
96+
97+
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.
98+
9399
## Apache mod_wsgi specific configuration
94100

95101
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
484490
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
485491
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
486492
[django-rest-durin]: https://github.com/eshaan7/django-rest-durin
493+
[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware

docs/api-guide/fields.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ Corresponds to `django.db.models.fields.DecimalField`.
291291
* `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`.
292292
* `decimal_places` The number of decimal places to store with the number.
293293
* `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`.
294-
* `max_value` Validate that the number provided is no greater than this value.
295-
* `min_value` Validate that the number provided is no less than this value.
294+
* `max_value` Validate that the number provided is no greater than this value. Should be an integer or `Decimal` object.
295+
* `min_value` Validate that the number provided is no less than this value. Should be an integer or `Decimal` object.
296296
* `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.
297297
* `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`.
298298
* `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`.

docs/api-guide/routers.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ The above example would now generate the following URL pattern:
142142
* URL path: `^users/{pk}/change-password/$`
143143
* URL name: `'user-change_password'`
144144

145+
### Using Django `path()` with routers
146+
147+
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:
148+
149+
router = SimpleRouter(use_regex_path=False)
150+
151+
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:
152+
153+
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
154+
lookup_field = 'my_model_id'
155+
lookup_value_regex = '[0-9a-f]{32}'
156+
157+
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
158+
lookup_field = 'my_model_uuid'
159+
lookup_value_converter = 'uuid'
160+
161+
Note that path converters will be used on all URLs registered in the router, including viewset actions.
162+
145163
# API Guide
146164

147165
## SimpleRouter
@@ -160,30 +178,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
160178
<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>
161179
</table>
162180

163-
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
181+
By default, the URLs created by `SimpleRouter` are appended with a trailing slash.
164182
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
165183

166184
router = SimpleRouter(trailing_slash=False)
167185

168186
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.
169187

170-
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:
171-
172-
router = SimpleRouter(use_regex_path=False)
173-
174-
**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]
175-
176-
177-
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:
178-
179-
class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
180-
lookup_field = 'my_model_id'
181-
lookup_value_regex = '[0-9a-f]{32}'
182-
183-
class MyPathModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
184-
lookup_field = 'my_model_uuid'
185-
lookup_value_converter = 'uuid'
186-
187188
## DefaultRouter
188189

189190
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
351352
[drf-extensions-customizable-endpoint-names]: https://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
352353
[url-namespace-docs]: https://docs.djangoproject.com/en/4.0/topics/http/urls/#url-namespaces
353354
[include-api-reference]: https://docs.djangoproject.com/en/4.0/ref/urls/#include
354-
[simplified-routing-release-note]: https://docs.djangoproject.com/en/2.0/releases/2.0/#simplified-url-routing-syntax
355355
[path-converters-topic-reference]: https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters

docs/community/jobs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Looking for a new Django REST Framework related role? On this site we provide a
77

88
* [https://www.djangoproject.com/community/jobs/][djangoproject-website]
99
* [https://www.python.org/jobs/][python-org-jobs]
10+
* [https://django.on-remote.com][django-on-remote]
1011
* [https://djangogigs.com][django-gigs-com]
1112
* [https://djangojobs.net/jobs/][django-jobs-net]
1213
* [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
2627

2728
[djangoproject-website]: https://www.djangoproject.com/community/jobs/
2829
[python-org-jobs]: https://www.python.org/jobs/
30+
[django-on-remote]: https://django.on-remote.com/
2931
[django-gigs-com]: https://djangogigs.com
3032
[django-jobs-net]: https://djangojobs.net/jobs/
3133
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**.
8787

8888
REST framework requires the following:
8989

90-
* Django (4.2, 5.0)
91-
* Python (3.8, 3.9, 3.10, 3.11, 3.12)
90+
* Django (4.2, 5.0, 5.1)
91+
* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13)
9292

9393
We **highly recommend** and only officially support the latest patch release of
9494
each Python and Django series.

docs/topics/browsable-api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ By default, the API will return the format specified by the headers, which in th
2020
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:
2121

2222
```python
23+
from django.urls import include, path
24+
2325
urlpatterns = [
2426
# ...
25-
url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
27+
path("api-auth/", include("rest_framework.urls", namespace="rest_framework"))
2628
]
2729
```
2830

rest_framework/fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -986,10 +986,10 @@ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=
986986
self.max_value = max_value
987987
self.min_value = min_value
988988

989-
if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
990-
warnings.warn("max_value should be a Decimal instance.")
991-
if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal):
992-
warnings.warn("min_value should be a Decimal instance.")
989+
if self.max_value is not None and not isinstance(self.max_value, (int, decimal.Decimal)):
990+
warnings.warn("max_value should be an integer or Decimal instance.")
991+
if self.min_value is not None and not isinstance(self.min_value, (int, decimal.Decimal)):
992+
warnings.warn("min_value should be an integer or Decimal instance.")
993993

994994
if self.max_digits is not None and self.decimal_places is not None:
995995
self.max_whole_digits = self.max_digits - self.decimal_places

rest_framework/locale/fa/LC_MESSAGES/django.po

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Aryan Baghi <[email protected]>, 2020
88
# Omid Zarin <[email protected]>, 2019
99
# Xavier Ordoquy <[email protected]>, 2020
10+
# Sina Amini <[email protected]>, 2024
1011
msgid ""
1112
msgstr ""
1213
"Project-Id-Version: Django REST framework\n"
@@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
187188

188189
#: fields.py:816
189190
msgid "Enter a valid email address."
190-
msgstr "پست الکترونیکی صحبح وارد کنید."
191+
msgstr "پست الکترونیکی صحیح وارد کنید."
191192

192193
#: fields.py:827
193194
msgid "This value does not match the required pattern."
Binary file not shown.

rest_framework/locale/fa_IR/LC_MESSAGES/django.po

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Aryan Baghi <[email protected]>, 2020
88
# Omid Zarin <[email protected]>, 2019
99
# Xavier Ordoquy <[email protected]>, 2020
10+
# Sina Amini <[email protected]>, 2024
1011
msgid ""
1112
msgstr ""
1213
"Project-Id-Version: Django REST framework\n"
@@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
187188

188189
#: fields.py:816
189190
msgid "Enter a valid email address."
190-
msgstr "پست الکترونیکی صحبح وارد کنید."
191+
msgstr "پست الکترونیکی صحیح وارد کنید."
191192

192193
#: fields.py:827
193194
msgid "This value does not match the required pattern."

rest_framework/locale/zh_CN/LC_MESSAGES/django.po

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,12 @@ msgstr "列表字段不能为空值。"
353353
#: fields.py:1605
354354
#, python-brace-format
355355
msgid "Ensure this field has at least {min_length} elements."
356-
msgstr ""
356+
msgstr "请确保这个字段至少包含 {min_length} 个元素。"
357357

358358
#: fields.py:1606
359359
#, python-brace-format
360360
msgid "Ensure this field has no more than {max_length} elements."
361-
msgstr ""
361+
msgstr "请确保这个字段不能超过 {max_length} 个元素。"
362362

363363
#: fields.py:1682
364364
#, python-brace-format
@@ -367,7 +367,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。"
367367

368368
#: fields.py:1683
369369
msgid "This dictionary may not be empty."
370-
msgstr ""
370+
msgstr "这个字典可能不是空的。"
371371

372372
#: fields.py:1755
373373
msgid "Value must be valid JSON."

rest_framework/schemas/inspectors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ def get_description(self, path, method):
7979
view = self.view
8080

8181
method_name = getattr(view, 'action', method.lower())
82-
method_docstring = getattr(view, method_name, None).__doc__
83-
if method_docstring:
82+
method_func = getattr(view, method_name, None)
83+
method_docstring = method_func.__doc__
84+
if method_func and method_docstring:
8485
# An explicit docstring on the method or action.
8586
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
8687
else:

rest_framework/urlpatterns.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from django.urls import URLResolver, include, path, re_path, register_converter
2+
from django.urls.converters import get_converters
23
from django.urls.resolvers import RoutePattern
34

45
from rest_framework.settings import api_settings
56

67

7-
def _get_format_path_converter(suffix_kwarg, allowed):
8+
def _get_format_path_converter(allowed):
89
if allowed:
910
if len(allowed) == 1:
1011
allowed_pattern = allowed[0]
@@ -23,11 +24,14 @@ def to_python(self, value):
2324
def to_url(self, value):
2425
return '.' + value + '/'
2526

27+
return FormatSuffixConverter
28+
29+
30+
def _generate_converter_name(allowed):
2631
converter_name = 'drf_format_suffix'
2732
if allowed:
2833
converter_name += '_' + '_'.join(allowed)
29-
30-
return converter_name, FormatSuffixConverter
34+
return converter_name
3135

3236

3337
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):
104108
else:
105109
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
106110

107-
converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed)
108-
register_converter(suffix_converter, converter_name)
111+
converter_name = _generate_converter_name(allowed)
112+
if converter_name not in get_converters():
113+
suffix_converter = _get_format_path_converter(allowed)
114+
register_converter(suffix_converter, converter_name)
109115

110116
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
111117

rest_framework/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Provides an APIView class that is the base of all views in REST framework.
33
"""
4+
from django import VERSION as DJANGO_VERSION
45
from django.conf import settings
56
from django.core.exceptions import PermissionDenied
67
from django.db import connections, models
@@ -139,6 +140,11 @@ def force_evaluation():
139140
view.cls = cls
140141
view.initkwargs = initkwargs
141142

143+
# Exempt all DRF views from Django's LoginRequiredMiddleware. Users should set
144+
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
145+
if DJANGO_VERSION >= (5, 1):
146+
view.login_required = False
147+
142148
# Note: session based authentication is explicitly CSRF validated,
143149
# all other authentication is CSRF exempt.
144150
return csrf_exempt(view)

rest_framework/viewsets.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from functools import update_wrapper
2020
from inspect import getmembers
2121

22+
from django import VERSION as DJANGO_VERSION
2223
from django.urls import NoReverseMatch
2324
from django.utils.decorators import classonlymethod
2425
from django.views.decorators.csrf import csrf_exempt
@@ -136,6 +137,12 @@ def view(request, *args, **kwargs):
136137
view.cls = cls
137138
view.initkwargs = initkwargs
138139
view.actions = actions
140+
141+
# Exempt from Django's LoginRequiredMiddleware. Users should set
142+
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
143+
if DJANGO_VERSION >= (5, 1):
144+
view.login_required = False
145+
139146
return csrf_exempt(view)
140147

141148
def initialize_request(self, request, *args, **kwargs):

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def get_version(package):
9191
'Framework :: Django',
9292
'Framework :: Django :: 4.2',
9393
'Framework :: Django :: 5.0',
94+
'Framework :: Django :: 5.1',
9495
'Intended Audience :: Developers',
9596
'License :: OSI Approved :: BSD License',
9697
'Operating System :: OS Independent',
@@ -101,6 +102,7 @@ def get_version(package):
101102
'Programming Language :: Python :: 3.10',
102103
'Programming Language :: Python :: 3.11',
103104
'Programming Language :: Python :: 3.12',
105+
'Programming Language :: Python :: 3.13',
104106
'Programming Language :: Python :: 3 :: Only',
105107
'Topic :: Internet :: WWW/HTTP',
106108
],

tests/test_fields.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,13 +1245,13 @@ class TestMinMaxDecimalField(FieldValues):
12451245
'20.0': Decimal('20.0'),
12461246
}
12471247
invalid_inputs = {
1248-
'9.9': ['Ensure this value is greater than or equal to 10.'],
1249-
'20.1': ['Ensure this value is less than or equal to 20.'],
1248+
'9.9': ['Ensure this value is greater than or equal to 10.0.'],
1249+
'20.1': ['Ensure this value is less than or equal to 20.0.'],
12501250
}
12511251
outputs = {}
12521252
field = serializers.DecimalField(
12531253
max_digits=3, decimal_places=1,
1254-
min_value=10, max_value=20
1254+
min_value=10.0, max_value=20.0
12551255
)
12561256

12571257
def test_warning_when_not_decimal_types(self, caplog):
@@ -1260,14 +1260,14 @@ def test_warning_when_not_decimal_types(self, caplog):
12601260

12611261
serializers.DecimalField(
12621262
max_digits=3, decimal_places=1,
1263-
min_value=10, max_value=20
1263+
min_value=10.0, max_value=20.0
12641264
)
12651265

12661266
assert len(w) == 2
12671267
assert all(issubclass(i.category, UserWarning) for i in w)
12681268

1269-
assert 'max_value should be a Decimal instance' in str(w[0].message)
1270-
assert 'min_value should be a Decimal instance' in str(w[1].message)
1269+
assert 'max_value should be an integer or Decimal instance' in str(w[0].message)
1270+
assert 'min_value should be an integer or Decimal instance' in str(w[1].message)
12711271

12721272

12731273
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):

0 commit comments

Comments
 (0)