diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md
deleted file mode 100644
index 87fa57a89..000000000
--- a/.github/ISSUE_TEMPLATE/1-issue.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Issue
-about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏
----
-
-## Checklist
-
-
-
-- [ ] Raised initially as discussion #...
-- [ ] 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.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index 382fc521a..000000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-blank_issues_enabled: false
-contact_links:
-- name: Discussions
- url: https://github.com/encode/django-rest-framework/discussions
- about: >
- The "Discussions" forum is where you want to start. 💖
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6276dddcd..bf158311a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,16 +9,16 @@ on:
jobs:
tests:
name: Python ${{ matrix.python-version }}
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
strategy:
matrix:
python-version:
- - '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
+ - '3.13'
steps:
- uses: actions/checkout@v4
@@ -33,10 +33,10 @@ jobs:
run: python -m pip install --upgrade pip setuptools virtualenv wheel
- name: Install dependencies
- run: python -m pip install --upgrade codecov tox
+ run: python -m pip install --upgrade tox
- 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
if: ${{ matrix.python-version == '3.9' }}
@@ -44,12 +44,13 @@ jobs:
tox -e base,dist,docs
- name: Upload coverage
- run: |
- codecov -e TOXENV,DJANGO
+ uses: codecov/codecov-action@v5
+ with:
+ env_vars: TOXENV,DJANGO
test-docs:
name: Test documentation links
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8939dd3db..27bbcb763 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,3 +31,9 @@ repos:
hooks:
- id: codespell
exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js
+
+- repo: https://github.com/asottile/pyupgrade
+ rev: v3.19.1
+ hooks:
+ - id: pyupgrade
+ args: ["--py39-plus", "--keep-percent-format"]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fb01f8bf7..644a719c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,4 @@
At this point in its 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.
-Apart from minor documentation changes, the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion.
-
The [Contributing guide in the documentation](https://www.django-rest-framework.org/community/contributing/) gives some more information on our process and code of conduct.
diff --git a/README.md b/README.md
index d32fbc331..be6619b4e 100644
--- a/README.md
+++ b/README.md
@@ -28,8 +28,9 @@ The initial aim is to provide a single full-time position on REST framework.
[![][cryptapi-img]][cryptapi-url]
[![][fezto-img]][fezto-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].
---
@@ -53,8 +54,8 @@ Some reasons you might want to use REST framework:
# Requirements
-* Python 3.8+
-* Django 5.0, 4.2
+* Python 3.9+
+* Django 4.2, 5.0, 5.1, 5.2
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.
@@ -196,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
[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
+[zuplo-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/zuplo-readme.png
[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
@@ -206,6 +208,7 @@ Please see the [security policy][security-policy].
[cryptapi-url]: https://cryptapi.io
[fezto-url]: https://www.fezto.xyz/?utm_source=DjangoRESTFramework
[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
[oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
diff --git a/SECURITY.md b/SECURITY.md
index a92a1b0cf..88ff092a2 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,8 +2,6 @@
## Reporting a Vulnerability
-Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team).
+**Please report security issues by emailing security@encode.io**.
- **Please report security issues by emailing security@djangoproject.com**.
-
- The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
+The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index d6e6293fd..84e58bf4b 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -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.
+## 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
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.
@@ -448,6 +454,12 @@ There are currently two forks of this project.
More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html).
+## django-pyoidc
+
+[dango-pyoidc][django_pyoidc] adds support for OpenID Connect (OIDC) authentication. This allows you to delegate user management to an Identity Provider, which can be used to implement Single-Sign-On (SSO). It provides support for most uses-cases, such as customizing how token info are mapped to user models, using OIDC audiences for access control, etc.
+
+More information can be found in the [Documentation](https://django-pyoidc.readthedocs.io/latest/index.html).
+
[cite]: https://jacobian.org/writing/rest-worst-practices/
[http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
@@ -484,3 +496,5 @@ More information can be found in the [Documentation](https://django-rest-durin.r
[drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless
[django-rest-authemail]: https://github.com/celiao/django-rest-authemail
[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
+[django-pyoidc] : https://github.com/makinacorpus/django_pyoidc
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 94b6e7c21..3225191f1 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -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`.
* `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`.
-* `max_value` Validate that the number provided is no greater than this value.
-* `min_value` Validate that the number provided is no less 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. 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.
* `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`.
@@ -552,7 +552,7 @@ For further examples on `HiddenField` see the [validators](validators.md) docume
---
-**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259).
+**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request).
---
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 56eb61e43..7c4eece4b 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -628,12 +628,16 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati
The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys.
+The [rest-framework-gm2m-relations][drf-gm2m-relations] library provides read/write serialization for [django-gm2m][django-gm2m-field].
+
[cite]: http://users.ece.utexas.edu/~adnan/pike.html
[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward
[routers]: https://www.django-rest-framework.org/api-guide/routers#defaultrouter
[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations
+[drf-gm2m-relations]: https://github.com/mojtabaakbari221b/rest-framework-gm2m-relations
+[django-gm2m-field]: https://github.com/tkhyn/django-gm2m
[django-intermediary-manytomany]: https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany
[dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects
[to_internal_value]: https://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index d48f785ab..7a6bd39f4 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -525,7 +525,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
## LaTeX
-[Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble].
+[Rest Framework Latex] provides a renderer that outputs PDFs using Lualatex. It is maintained by [Pebble (S/F Software)][mypebble].
[cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 91ef0b96e..d6bdeb235 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -142,6 +142,24 @@ The above example would now generate the following URL pattern:
* URL path: `^users/{pk}/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
## SimpleRouter
@@ -160,30 +178,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
{prefix}/{lookup}/{url_path}/ | GET, or as specified by `methods` argument | `@action(detail=True)` decorated method | {basename}-{url_name} |
-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:
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.
-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
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
[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
-[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
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index eae79b62f..8d56d36f5 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -233,7 +233,7 @@ Serializer classes can also include reusable validators that are applied to the
class EventSerializer(serializers.Serializer):
name = serializers.CharField()
- room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
+ room_number = serializers.ChoiceField(choices=[101, 102, 103, 201])
date = serializers.DateField()
class Meta:
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 47e2ce993..7bee3166d 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -460,4 +460,4 @@ Default: `None`
[cite]: https://www.python.org/dev/peps/pep-0020/
[rfc4627]: https://www.ietf.org/rfc/rfc4627.txt
[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
-[strftime]: https://docs.python.org/3/library/time.html#time.strftime
+[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index 261df80f2..ed585faf2 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -25,9 +25,12 @@ The `APIRequestFactory` class supports an almost identical API to Django's stand
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'})
+ # Using the standard RequestFactory API to encode JSON data
+ request = factory.post('/notes/', {'title': 'new idea'}, content_type='application/json')
+
#### Using the `format` argument
-Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example:
+Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a wide set of request formats. When using this argument, the factory will select an appropriate renderer and its configured `content_type`. For example:
# Create a JSON POST request
factory = APIRequestFactory()
@@ -41,7 +44,7 @@ To support a wider set of request formats, or change the default format, [see th
If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example:
- request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')
+ request = factory.post('/notes/', yaml.dump({'title': 'new idea'}), content_type='application/yaml')
#### PUT and PATCH with form data
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index 4c58fa713..e6d7774a6 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -45,7 +45,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
}
}
-The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
+The rates used in `DEFAULT_THROTTLE_RATES` can be specified over a period of second, minute, hour or day. The period must be specified after the `/` separator using `s`, `m`, `h` or `d`, respectively. For increased clarity, extended units such as `second`, `minute`, `hour`, `day` or even abbreviations like `sec`, `min`, `hr` are allowed, as only the first character is relevant to identify the rate.
You can also set the throttling policy on a per-view or per-viewset basis,
using the `APIView` class-based views.
@@ -110,7 +110,7 @@ You'll need to remember to also set your custom throttle class in the `'DEFAULT_
The built-in throttle implementations are open to [race conditions][race], so under high concurrency they may allow a few extra requests through.
-If your project relies on guaranteeing the number of requests during concurrent requests, you will need to implement your own throttle class. See [issue #5181][gh5181] for more details.
+If your project relies on guaranteeing the number of requests during concurrent requests, you will need to implement your own throttle class.
---
@@ -220,5 +220,4 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
[identifying-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
[cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches
[cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache
-[gh5181]: https://github.com/encode/django-rest-framework/issues/5181
[race]: https://en.wikipedia.org/wiki/Race_condition#Data_race
diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md
index e181d4c61..57bcb8628 100644
--- a/docs/api-guide/validators.md
+++ b/docs/api-guide/validators.md
@@ -48,7 +48,7 @@ If we open up the Django shell using `manage.py shell` we can now
CustomerReportSerializer():
id = IntegerField(label='ID', read_only=True)
time_raised = DateTimeField(read_only=True)
- reference = CharField(max_length=20, validators=[])
+ reference = CharField(max_length=20, validators=[UniqueValidator(queryset=CustomerReportRecord.objects.all())])
description = CharField(style={'type': 'textarea'})
The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field.
@@ -166,7 +166,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
---
-**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259).
+**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request).
---
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 43007e95d..22acfe327 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -128,6 +128,8 @@ You may inspect these attributes to adjust behavior based on the current action.
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
+**Note**: the `action` attribute is not available in the `get_parsers`, `get_authenticators` and `get_content_negotiator` methods, as it is set _after_ they are called in the framework lifecycle. If you override one of these methods and try to access the `action` attribute in them, you will get an `AttributeError` error.
+
## Marking extra actions for routing
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
diff --git a/docs/community/3.16-announcement.md b/docs/community/3.16-announcement.md
new file mode 100644
index 000000000..b8f460ae7
--- /dev/null
+++ b/docs/community/3.16-announcement.md
@@ -0,0 +1,42 @@
+
+
+# Django REST framework 3.16
+
+At the Internet, on March 28th, 2025, we are happy to announce the release of Django REST framework 3.16.
+
+## Updated Django and Python support
+
+The latest release now fully supports Django 5.1 and the upcoming 5.2 LTS as well as Python 3.13.
+
+The current minimum versions of Django is now 4.2 and Python 3.9.
+
+## Django LoginRequiredMiddleware
+
+The new `LoginRequiredMiddleware` introduced by Django 5.1 can now be used alongside Django REST Framework, however it is not honored for API views as an equivalent behaviour can be configured via `DEFAULT_AUTHENTICATION_CLASSES`. See [our dedicated section](../api-guide/authentication.md#django-51-loginrequiredmiddleware) in the docs for more information.
+
+## Improved support for UniqueConstraint
+
+The generation of validators for [UniqueConstraint](https://docs.djangoproject.com/en/stable/ref/models/constraints/#uniqueconstraint) has been improved to support better nullable fields and constraints with conditions.
+
+## 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.
+
+See the [release notes](release-notes.md) page for a complete listing.
diff --git a/docs/community/contributing.md b/docs/community/contributing.md
index 5dea6426d..5a9188943 100644
--- a/docs/community/contributing.md
+++ b/docs/community/contributing.md
@@ -4,8 +4,6 @@
>
> — [Tim Berners-Lee][cite]
-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
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.
@@ -30,24 +28,9 @@ The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines f
# Issues
-Our contribution process is that the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion.
-
-Some tips on good potential issue reporting:
-
* 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.
* 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.
-## Triaging issues
-
-Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
-
-* 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?
-* 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, could the feature request instead be implemented as a third party package?
-* 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
To start developing on Django REST framework, first create a Fork from the
@@ -206,7 +189,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
[code-of-conduct]: https://www.djangoproject.com/conduct/
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[so-filter]: https://stackexchange.com/filters/66475/rest-framework
-[issues]: https://github.com/encode/django-rest-framework/issues?state=open
[pep-8]: https://www.python.org/dev/peps/pep-0008/
[build-status]: ../img/build-status.png
[pull-requests]: https://help.github.com/articles/using-pull-requests
diff --git a/docs/community/funding.md b/docs/community/funding.md
index 10e09bf71..7bca4bae4 100644
--- a/docs/community/funding.md
+++ b/docs/community/funding.md
@@ -114,7 +114,7 @@ If you use REST framework commercially we strongly encourage you to invest in it
Signing up for a paid plan will:
* Directly contribute to faster releases, more features, and higher quality software.
-* Allow more time to be invested in documentation, issue triage, and community support.
+* Allow more time to be invested in keeping the package up to date.
* Safeguard the future development of REST framework.
REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development.
@@ -134,18 +134,6 @@ REST framework continues to be open-source and permissively licensed, but we fir
---
-## What future funding will enable
-
-* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries.
-* Better authentication defaults, possibly bringing JWT & CORS support into the core package.
-* Securing the community & operations manager position long-term.
-* Opening up and securing a part-time position to focus on ticket triage and resolution.
-* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation.
-
-Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project.
-
----
-
## What our sponsors and users say
> As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem.
@@ -165,6 +153,8 @@ DRF is one of the core reasons why Django is top choice among web frameworks tod
>
> — Andrew Conti, Django REST framework user
+Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project.
+
---
## Individual plan
diff --git a/docs/community/jobs.md b/docs/community/jobs.md
index aa1c5d4b4..f3ce37d15 100644
--- a/docs/community/jobs.md
+++ b/docs/community/jobs.md
@@ -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.python.org/jobs/][python-org-jobs]
+* [https://django.on-remote.com][django-on-remote]
* [https://djangogigs.com][django-gigs-com]
* [https://djangojobs.net/jobs/][django-jobs-net]
* [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/
[python-org-jobs]: https://www.python.org/jobs/
+[django-on-remote]: https://django.on-remote.com/
[django-gigs-com]: https://djangogigs.com
[django-jobs-net]: https://djangojobs.net/jobs/
[findwork-dev]: https://findwork.dev/django-rest-framework-jobs
diff --git a/docs/community/project-management.md b/docs/community/project-management.md
index 4f203e13b..daf2cda8d 100644
--- a/docs/community/project-management.md
+++ b/docs/community/project-management.md
@@ -34,7 +34,6 @@ Further notes for maintainers:
* Code changes should come in the form of a pull request - do not push directly to master.
* Maintainers should typically not merge their own pull requests.
* Each issue/pull request should have exactly one label once triaged.
-* Search for un-triaged issues with [is:open no:label][un-triaged].
---
@@ -157,7 +156,6 @@ The following issues still need to be addressed:
* Document ownership and management of the security mailing list.
[bus-factor]: https://en.wikipedia.org/wiki/Bus_factor
-[un-triaged]: https://github.com/encode/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
[transifex-client]: https://pypi.org/project/transifex-client/
[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md
index 5ec415a79..c7b82e985 100644
--- a/docs/community/release-notes.md
+++ b/docs/community/release-notes.md
@@ -36,16 +36,101 @@ You can determine your currently installed version using `pip show`:
---
+## 3.16.x series
+
+### 3.16.0
+
+**Date**: 28th March 2025
+
+This release is considered a significant release to improve upstream support with Django and Python. Some of these may change the behaviour of existing features and pre-existing behaviour. Specifically, some fixes were added to around the support of `UniqueConstraint` with nullable fields which will improve built-in serializer validation.
+
+## Features
+
+* Add official support for Django 5.1 and its new `LoginRequiredMiddleware` in [#9514](https://github.com/encode/django-rest-framework/pull/9514) and [#9657](https://github.com/encode/django-rest-framework/pull/9657)
+* Add official Django 5.2a1 support in [#9634](https://github.com/encode/django-rest-framework/pull/9634)
+* Add support for Python 3.13 in [#9527](https://github.com/encode/django-rest-framework/pull/9527) and [#9556](https://github.com/encode/django-rest-framework/pull/9556)
+* Support Django 2.1+ test client JSON data automatically serialized in [#6511](https://github.com/encode/django-rest-framework/pull/6511) and fix a regression in [#9615](https://github.com/encode/django-rest-framework/pull/9615)
+
+## Bug fixes
+
+* Fix unique together validator to respect condition's fields from `UniqueConstraint` in [#9360](https://github.com/encode/django-rest-framework/pull/9360)
+* Fix raising on nullable fields part of `UniqueConstraint` in [#9531](https://github.com/encode/django-rest-framework/pull/9531)
+* Fix `unique_together` validation with source in [#9482](https://github.com/encode/django-rest-framework/pull/9482)
+* Added protections to `AttributeError` raised within properties in [#9455](https://github.com/encode/django-rest-framework/pull/9455)
+* Fix `get_template_context` to handle also lists in [#9467](https://github.com/encode/django-rest-framework/pull/9467)
+* Fix "Converter is already registered" deprecation warning. in [#9512](https://github.com/encode/django-rest-framework/pull/9512)
+* Fix noisy warning and accept integers as min/max values of `DecimalField` in [#9515](https://github.com/encode/django-rest-framework/pull/9515)
+* Fix usages of `open()` in `setup.py` in [#9661](https://github.com/encode/django-rest-framework/pull/9661)
+
+## Translations
+
+* Add some missing Chinese translations in [#9505](https://github.com/encode/django-rest-framework/pull/9505)
+* Fix spelling mistakes in Farsi language were corrected in [#9521](https://github.com/encode/django-rest-framework/pull/9521)
+* Fixing and adding missing Brazilian Portuguese translations in [#9535](https://github.com/encode/django-rest-framework/pull/9535)
+
+## Removals
+
+* Remove support for Python 3.8 in [#9670](https://github.com/encode/django-rest-framework/pull/9670)
+* Remove long deprecated code from request wrapper in [#9441](https://github.com/encode/django-rest-framework/pull/9441)
+* Remove deprecated `AutoSchema._get_reference` method in [#9525](https://github.com/encode/django-rest-framework/pull/9525)
+
+## Documentation and internal changes
+
+* Provide tests for hashing of `OperandHolder` in [#9437](https://github.com/encode/django-rest-framework/pull/9437)
+* Update documentation: Add `adrf` third party package in [#9198](https://github.com/encode/django-rest-framework/pull/9198)
+* Update tutorials links in Community contributions docs in [#9476](https://github.com/encode/django-rest-framework/pull/9476)
+* Fix usage of deprecated Django function in example from docs in [#9509](https://github.com/encode/django-rest-framework/pull/9509)
+* Move path converter docs into a separate section in [#9524](https://github.com/encode/django-rest-framework/pull/9524)
+* Add test covering update view without `queryset` attribute in [#9528](https://github.com/encode/django-rest-framework/pull/9528)
+* Fix Transifex link in [#9541](https://github.com/encode/django-rest-framework/pull/9541)
+* Fix example `httpie` call in docs in [#9543](https://github.com/encode/django-rest-framework/pull/9543)
+* Fix example for serializer field with choices in docs in [#9563](https://github.com/encode/django-rest-framework/pull/9563)
+* Remove extra `<>` in validators example in [#9590](https://github.com/encode/django-rest-framework/pull/9590)
+* Update `strftime` link in the docs in [#9624](https://github.com/encode/django-rest-framework/pull/9624)
+* Switch to codecov GHA in [#9618](https://github.com/encode/django-rest-framework/pull/9618)
+* Add note regarding availability of the `action` attribute in 'Introspecting ViewSet actions' docs section in [#9633](https://github.com/encode/django-rest-framework/pull/9633)
+* Improved description of allowed throttling rates in documentation in [#9640](https://github.com/encode/django-rest-framework/pull/9640)
+* Add `rest-framework-gm2m-relations` package to the list of 3rd party libraries in [#9063](https://github.com/encode/django-rest-framework/pull/9063)
+* Fix a number of typos in the test suite in the docs in [#9662](https://github.com/encode/django-rest-framework/pull/9662)
+* Add `django-pyoidc` as a third party authentication library in [#9667](https://github.com/encode/django-rest-framework/pull/9667)
+
+## New Contributors
+
+* [`@maerteijn`](https://github.com/maerteijn) made their first contribution in [#9198](https://github.com/encode/django-rest-framework/pull/9198)
+* [`@FraCata00`](https://github.com/FraCata00) made their first contribution in [#9444](https://github.com/encode/django-rest-framework/pull/9444)
+* [`@AlvaroVega`](https://github.com/AlvaroVega) made their first contribution in [#9451](https://github.com/encode/django-rest-framework/pull/9451)
+* [`@james`](https://github.com/james)-mchugh made their first contribution in [#9455](https://github.com/encode/django-rest-framework/pull/9455)
+* [`@ifeanyidavid`](https://github.com/ifeanyidavid) made their first contribution in [#9479](https://github.com/encode/django-rest-framework/pull/9479)
+* [`@p`](https://github.com/p)-schlickmann made their first contribution in [#9480](https://github.com/encode/django-rest-framework/pull/9480)
+* [`@akkuman`](https://github.com/akkuman) made their first contribution in [#9505](https://github.com/encode/django-rest-framework/pull/9505)
+* [`@rafaelgramoschi`](https://github.com/rafaelgramoschi) made their first contribution in [#9509](https://github.com/encode/django-rest-framework/pull/9509)
+* [`@Sinaatkd`](https://github.com/Sinaatkd) made their first contribution in [#9521](https://github.com/encode/django-rest-framework/pull/9521)
+* [`@gtkacz`](https://github.com/gtkacz) made their first contribution in [#9535](https://github.com/encode/django-rest-framework/pull/9535)
+* [`@sliverc`](https://github.com/sliverc) made their first contribution in [#9556](https://github.com/encode/django-rest-framework/pull/9556)
+* [`@gabrielromagnoli1987`](https://github.com/gabrielromagnoli1987) made their first contribution in [#9543](https://github.com/encode/django-rest-framework/pull/9543)
+* [`@cheehong1030`](https://github.com/cheehong1030) made their first contribution in [#9563](https://github.com/encode/django-rest-framework/pull/9563)
+* [`@amansharma612`](https://github.com/amansharma612) made their first contribution in [#9590](https://github.com/encode/django-rest-framework/pull/9590)
+* [`@Gluroda`](https://github.com/Gluroda) made their first contribution in [#9616](https://github.com/encode/django-rest-framework/pull/9616)
+* [`@deepakangadi`](https://github.com/deepakangadi) made their first contribution in [#9624](https://github.com/encode/django-rest-framework/pull/9624)
+* [`@EXG1O`](https://github.com/EXG1O) made their first contribution in [#9633](https://github.com/encode/django-rest-framework/pull/9633)
+* [`@decadenza`](https://github.com/decadenza) made their first contribution in [#9640](https://github.com/encode/django-rest-framework/pull/9640)
+* [`@mojtabaakbari221b`](https://github.com/mojtabaakbari221b) made their first contribution in [#9063](https://github.com/encode/django-rest-framework/pull/9063)
+* [`@mikemanger`](https://github.com/mikemanger) made their first contribution in [#9661](https://github.com/encode/django-rest-framework/pull/9661)
+* [`@gbip`](https://github.com/gbip) made their first contribution in [#9667](https://github.com/encode/django-rest-framework/pull/9667)
+
+**Full Changelog**: https://github.com/encode/django-rest-framework/compare/3.15.2...3.16.0
+
## 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/9157)
+* 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
@@ -120,7 +205,7 @@ Date: 15th March 2024
* Fix 404 when page query parameter is empty string [[#8578](https://github.com/encode/django-rest-framework/pull/8578)]
* Fixes instance check in ListSerializer.to_representation [[#8726](https://github.com/encode/django-rest-framework/pull/8726)] [[#8727](https://github.com/encode/django-rest-framework/pull/8727)]
* FloatField will crash if the input is a number that is too big [[#8725](https://github.com/encode/django-rest-framework/pull/8725)]
-* Add missing DurationField to SimpleMetada label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)]
+* Add missing DurationField to SimpleMetadata label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)]
* Add support for Python 3.11 [[#8752](https://github.com/encode/django-rest-framework/pull/8752)]
* Make request consistently available in pagination classes [[#8764](https://github.com/encode/django-rest-framework/pull/9764)]
* Possibility to remove trailing zeros on DecimalFields representation [[#6514](https://github.com/encode/django-rest-framework/pull/6514)]
@@ -427,7 +512,7 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10.
* Allow hashing of ErrorDetail. [#5932][gh5932]
* Correct schema parsing for JSONField [#5878][gh5878]
* Render descriptions (from help_text) using safe [#5869][gh5869]
-* Removed input value from deault_error_message [#5881][gh5881]
+* Removed input value from default_error_message [#5881][gh5881]
* Added min_value/max_value support in DurationField [#5643][gh5643]
* Fixed instance being overwritten in pk-only optimization try/except block [#5747][gh5747]
* Fixed AttributeError from items filter when value is None [#5981][gh5981]
diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md
index a92da82fc..d213cac3d 100644
--- a/docs/community/third-party-packages.md
+++ b/docs/community/third-party-packages.md
@@ -32,7 +32,7 @@ We suggest adding your package to the [REST Framework][rest-framework-grid] grid
#### Adding to the Django REST framework docs
-Create a [Pull Request][drf-create-pr] or [Issue][drf-create-issue] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section.
+Create a [Pull Request][drf-create-pr] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section.
#### Announce on the discussion group.
@@ -44,7 +44,11 @@ Django REST Framework has a growing community of developers, packages, and resou
Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages][rest-framework-grid].
-To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr].
+To submit new content, [create a pull request][drf-create-pr].
+
+## Async Support
+
+* [adrf](https://github.com/em1208/adrf) - Async support, provides async Views, ViewSets, and Serializers.
### Authentication
@@ -58,6 +62,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF.
* [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers.
* [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses.
+* [dango-pyoidc][django-pyoidc] adds support for OpenID Connect (OIDC) authentication.
### Permissions
@@ -155,6 +160,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Customization
+* [drf-restwind][drf-restwind] - a modern re-imagining of the Django REST Framework utilizes TailwindCSS and DaisyUI to provide flexible and customizable UI solutions with minimal coding effort.
* [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.
@@ -169,7 +175,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[drf-compat]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/compat.py
[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
[drf-create-pr]: https://github.com/encode/django-rest-framework/compare
-[drf-create-issue]: https://github.com/encode/django-rest-framework/issues/new
[authentication]: ../api-guide/authentication.md
[permissions]: ../api-guide/permissions.md
[third-party-packages]: ../topics/third-party-packages/#existing-third-party-packages
@@ -250,5 +255,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-requestlogs]: https://github.com/Raekkeri/django-requestlogs
[drf-standardized-errors]: https://github.com/ghazi-git/drf-standardized-errors
[drf-api-action]: https://github.com/Ori-Roza/drf-api-action
+[drf-restwind]: https://github.com/youzarsiph/drf-restwind
[drf-redesign]: https://github.com/youzarsiph/drf-redesign
[drf-material]: https://github.com/youzarsiph/drf-material
+[django-pyoidc]: https://github.com/makinacorpus/django_pyoidc
diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md
index f283e0e4c..427bdd2d7 100644
--- a/docs/community/tutorials-and-resources.md
+++ b/docs/community/tutorials-and-resources.md
@@ -12,7 +12,7 @@ There are a wide range of resources available for learning and using Django REST
-
+
@@ -28,7 +28,6 @@ There are a wide range of resources available for learning and using Django REST
* [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework]
* [Django REST Framework - An Introduction][drf-an-intro]
* [Django REST Framework Tutorial][drf-tutorial]
-* [Django REST Framework Course][django-rest-framework-course]
* [Building a RESTful API with Django REST Framework][building-a-restful-api-with-drf]
* [Getting Started with Django REST Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs]
* [End to End Web App with Django REST Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs]
@@ -39,8 +38,10 @@ 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]
* [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]
-* [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]
+* [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 Polls Tutorial API][django-polls-api]
+* [Django REST Framework Tutorial: Todo API][django-rest-framework-todo-api]
* [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog]
@@ -49,11 +50,11 @@ There are a wide range of resources available for learning and using Django REST
### Talks
* [Level Up! Rethinking the Web API Framework][pycon-us-2017]
-* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit]
+* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-toolkit]
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
* [Django REST Framework: Schemas, Hypermedia & Client Libraries][pycon-uk-2016]
-
+* [Finally Understand Authentication in Django REST Framework][django-con-2018]
### Tutorials
@@ -103,7 +104,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/
[cdrf.co]:http://www.cdrf.co
[medium-django-rest-framework]: https://medium.com/django-rest-framework
-[django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework
[pycon-uk-2016]: https://www.youtube.com/watch?v=FjmiGh7OqVg
[django-under-hood-2014]: https://www.youtube.com/watch?v=3cSsbe-tA0E
[integrating-pandas-drf-and-bokeh]: https://web.archive.org/web/20180104205117/http://machinalis.com/blog/pandas-django-rest-framework-bokeh/
@@ -115,12 +115,14 @@ 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
[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/
-[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-part2]: https://www.andreagrandi.it/2016/10/01/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/
-[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/
+[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/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-polls-api]: https://learndjango.com/tutorials/django-polls-tutorial-api
+[django-rest-framework-todo-api]: https://learndjango.com/tutorials/django-rest-framework-tutorial-todo-api
[django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ
-[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
+[full-fledged-rest-api-with-django-oauth-toolkit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk
[drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww
[building-a-rest-api-using-django-and-drf]: https://www.youtube.com/watch?v=PwssEec3IRw
[drf-tutorials]: https://www.youtube.com/watch?v=axRCBgbOJp8&list=PLJtp8Jm8EDzjgVg9vVyIUMoGyqtegj7FH
@@ -135,3 +137,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/
[doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/
[developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/
+[django-con-2018]: https://youtu.be/pY-oje5b5Qk?si=AOU6tLi0IL1_pVzq
\ No newline at end of file
diff --git a/docs/img/books/dfa-40-cover.jpg b/docs/img/books/dfa-40-cover.jpg
new file mode 100644
index 000000000..cc47312c7
Binary files /dev/null and b/docs/img/books/dfa-40-cover.jpg differ
diff --git a/docs/img/drf-m-api-root.png b/docs/img/drf-m-api-root.png
new file mode 100644
index 000000000..02a756287
Binary files /dev/null and b/docs/img/drf-m-api-root.png differ
diff --git a/docs/img/drf-m-detail-view.png b/docs/img/drf-m-detail-view.png
new file mode 100644
index 000000000..33e3515d8
Binary files /dev/null and b/docs/img/drf-m-detail-view.png differ
diff --git a/docs/img/drf-m-list-view.png b/docs/img/drf-m-list-view.png
new file mode 100644
index 000000000..a7771957d
Binary files /dev/null and b/docs/img/drf-m-list-view.png differ
diff --git a/docs/img/drf-r-api-root.png b/docs/img/drf-r-api-root.png
new file mode 100644
index 000000000..5704cc350
Binary files /dev/null and b/docs/img/drf-r-api-root.png differ
diff --git a/docs/img/drf-r-detail-view.png b/docs/img/drf-r-detail-view.png
new file mode 100644
index 000000000..2959c7201
Binary files /dev/null and b/docs/img/drf-r-detail-view.png differ
diff --git a/docs/img/drf-r-list-view.png b/docs/img/drf-r-list-view.png
new file mode 100644
index 000000000..1930b5dc0
Binary files /dev/null and b/docs/img/drf-r-list-view.png differ
diff --git a/docs/img/drf-rw-api-root.png b/docs/img/drf-rw-api-root.png
new file mode 100644
index 000000000..15b498b9b
Binary files /dev/null and b/docs/img/drf-rw-api-root.png differ
diff --git a/docs/img/drf-rw-detail-view.png b/docs/img/drf-rw-detail-view.png
new file mode 100644
index 000000000..6e821acda
Binary files /dev/null and b/docs/img/drf-rw-detail-view.png differ
diff --git a/docs/img/drf-rw-list-view.png b/docs/img/drf-rw-list-view.png
new file mode 100644
index 000000000..625b5c7c9
Binary files /dev/null and b/docs/img/drf-rw-list-view.png differ
diff --git a/docs/img/premium/zuplo-readme.png b/docs/img/premium/zuplo-readme.png
new file mode 100644
index 000000000..245ded35e
Binary files /dev/null and b/docs/img/premium/zuplo-readme.png differ
diff --git a/docs/img/rfm.png b/docs/img/rfm.png
deleted file mode 100644
index 7c82621af..000000000
Binary files a/docs/img/rfm.png and /dev/null differ
diff --git a/docs/img/rfr.png b/docs/img/rfr.png
deleted file mode 100644
index 1854511d0..000000000
Binary files a/docs/img/rfr.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index 864c1d072..d590d2c04 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -75,10 +75,11 @@ continued development by **[signing up for a paid plan][funding]**.
CryptAPI
FEZTO
Svix
+ Zuplo
-*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:
-* Django (4.2, 5.0)
-* Python (3.8, 3.9, 3.10, 3.11, 3.12)
+* Django (4.2, 5.0, 5.1, 5.2)
+* Python (3.9, 3.10, 3.11, 3.12, 3.13)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.
@@ -195,9 +196,7 @@ For priority support please sign up for a [professional or premium sponsorship p
## Security
-Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team).
-
-**Please report security issues by emailing security@djangoproject.com**.
+**Please report security issues by emailing security@encode.io**.
The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md
index 9a95edfc6..8cf530b7a 100644
--- a/docs/topics/browsable-api.md
+++ b/docs/topics/browsable-api.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:
```python
+from django.urls import include, path
+
urlpatterns = [
# ...
- url(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework"))
+ path("api-auth/", include("rest_framework.urls", namespace="rest_framework"))
]
```
@@ -79,22 +81,43 @@ For more specific CSS tweaks than simply overriding the default bootstrap theme
### Third party packages for customization
-You can use a third party package for customization, rather than doing it by yourself. Here is 2 packages for customizing the API:
+You can use a third party package for customization, rather than doing it by yourself. Here is 3 packages for customizing the API:
-* [rest-framework-redesign][rest-framework-redesign] - A package for customizing the API using Bootstrap 5. Modern and sleek design, it comes with the support for dark mode.
-* [rest-framework-material][rest-framework-material] - Material design for Django REST Framework.
+* [drf-restwind][drf-restwind] - a modern re-imagining of the Django REST Framework utilizes TailwindCSS and DaisyUI to provide flexible and customizable UI solutions with minimal coding effort.
+* [drf-redesign][drf-redesign] - A package for customizing the API using Bootstrap 5. Modern and sleek design, it comes with the support for dark mode.
+* [drf-material][drf-material] - Material design for Django REST Framework.
---
-![Django REST Framework Redesign][rfr]
+![API Root][drf-rw-api-root]
-*Screenshot of the rest-framework-redesign*
+![List View][drf-rw-list-view]
+
+![Detail View][drf-rw-detail-view]
+
+*Screenshots of the drf-restwind*
---
-![Django REST Framework Material][rfm]
+---
-*Screenshot of the rest-framework-material*
+![API Root][drf-r-api-root]
+
+![List View][drf-r-list-view]
+
+![Detail View][drf-r-detail-view]
+
+*Screenshot of the drf-redesign*
+
+---
+
+![API Root][drf-m-api-root]
+
+![List View][drf-m-api-root]
+
+![Detail View][drf-m-api-root]
+
+*Screenshot of the drf-material*
---
@@ -195,7 +218,15 @@ There are [a variety of packages for autocomplete widgets][autocomplete-packages
[bcomponentsnav]: https://getbootstrap.com/2.3.2/components.html#navbar
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
-[rest-framework-redesign]: https://github.com/youzarsiph/rest-framework-redesign
-[rest-framework-material]: https://github.com/youzarsiph/rest-framework-material
-[rfr]: ../img/rfr.png
-[rfm]: ../img/rfm.png
+[drf-restwind]: https://github.com/youzarsiph/drf-restwind
+[drf-rw-api-root]: ../img/drf-rw-api-root.png
+[drf-rw-list-view]: ../img/drf-rw-list-view.png
+[drf-rw-detail-view]: ../img/drf-rw-detail-view.png
+[drf-redesign]: https://github.com/youzarsiph/drf-redesign
+[drf-r-api-root]: ../img/drf-r-api-root.png
+[drf-r-list-view]: ../img/drf-r-list-view.png
+[drf-r-detail-view]: ../img/drf-r-detail-view.png
+[drf-material]: https://github.com/youzarsiph/drf-material
+[drf-m-api-root]: ../img/drf-m-api-root.png
+[drf-m-list-view]: ../img/drf-m-list-view.png
+[drf-m-detail-view]: ../img/drf-m-detail-view.png
diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md
index 267ccdb37..2f8f2abf0 100644
--- a/docs/topics/internationalization.md
+++ b/docs/topics/internationalization.md
@@ -105,7 +105,7 @@ For API clients the most appropriate of these will typically be to use the `Acce
[cite]: https://youtu.be/Wa0VfS2q94Y
[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation
[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-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
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 1dac5e0d8..b9bf67acb 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -321,7 +321,7 @@ You can install httpie using pip:
Finally, we can get a list of all of the snippets:
- http http://127.0.0.1:8000/snippets/ --unsorted
+ http GET http://127.0.0.1:8000/snippets/ --unsorted
HTTP/1.1 200 OK
...
@@ -354,7 +354,7 @@ Finally, we can get a list of all of the snippets:
Or we can get a particular snippet by referencing its id:
- http http://127.0.0.1:8000/snippets/2/ --unsorted
+ http GET http://127.0.0.1:8000/snippets/2/ --unsorted
HTTP/1.1 200 OK
...
diff --git a/mkdocs.yml b/mkdocs.yml
index a031dd69b..010aaefe2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -66,6 +66,7 @@ nav:
- 'Contributing to REST framework': 'community/contributing.md'
- 'Project management': 'community/project-management.md'
- 'Release Notes': 'community/release-notes.md'
+ - '3.16 Announcement': 'community/3.16-announcement.md'
- '3.15 Announcement': 'community/3.15-announcement.md'
- '3.14 Announcement': 'community/3.14-announcement.md'
- '3.13 Announcement': 'community/3.13-announcement.md'
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 636f0c8ad..692ce9cb1 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '3.15.2'
+__version__ = '3.16.0'
__author__ = 'Tom Christie'
__license__ = 'BSD 3-Clause'
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
@@ -23,9 +23,5 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
ISO_8601 = 'iso-8601'
-class RemovedInDRF316Warning(DeprecationWarning):
- pass
-
-
class RemovedInDRF317Warning(PendingDeprecationWarning):
pass
diff --git a/rest_framework/authtoken/management/commands/drf_create_token.py b/rest_framework/authtoken/management/commands/drf_create_token.py
index 3d6539244..3f4521fe4 100644
--- a/rest_framework/authtoken/management/commands/drf_create_token.py
+++ b/rest_framework/authtoken/management/commands/drf_create_token.py
@@ -42,4 +42,4 @@ class Command(BaseCommand):
username)
)
self.stdout.write(
- 'Generated token {} for user {}'.format(token.key, username))
+ f'Generated token {token.key} for user {username}')
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 27c5632be..ff21bacff 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -3,6 +3,9 @@ The `compat` module provides support for backwards compatibility with older
versions of Django/Python, and compatibility wrappers around optional packages.
"""
import django
+from django.db import models
+from django.db.models.constants import LOOKUP_SEP
+from django.db.models.sql.query import Node
from django.views.generic import View
@@ -157,6 +160,10 @@ if django.VERSION >= (5, 1):
# 1) the list of validators and 2) the error message. Starting from
# Django 5.1 ip_address_validators only returns the list of validators
from django.core.validators import ip_address_validators
+
+ def get_referenced_base_fields_from_q(q):
+ return q.referenced_base_fields
+
else:
# Django <= 5.1: create a compatibility shim for ip_address_validators
from django.core.validators import \
@@ -165,6 +172,35 @@ else:
def ip_address_validators(protocol, unpack_ipv4):
return _ip_address_validators(protocol, unpack_ipv4)[0]
+ # Django < 5.1: create a compatibility shim for Q.referenced_base_fields
+ # https://github.com/django/django/blob/5.1a1/django/db/models/query_utils.py#L179
+ def _get_paths_from_expression(expr):
+ if isinstance(expr, models.F):
+ yield expr.name
+ elif hasattr(expr, 'flatten'):
+ for child in expr.flatten():
+ if isinstance(child, models.F):
+ yield child.name
+ elif isinstance(child, models.Q):
+ yield from _get_children_from_q(child)
+
+ def _get_children_from_q(q):
+ for child in q.children:
+ if isinstance(child, Node):
+ yield from _get_children_from_q(child)
+ elif isinstance(child, tuple):
+ lhs, rhs = child
+ yield lhs
+ if hasattr(rhs, 'resolve_expression'):
+ yield from _get_paths_from_expression(rhs)
+ elif hasattr(child, 'resolve_expression'):
+ yield from _get_paths_from_expression(child)
+
+ def get_referenced_base_fields_from_q(q):
+ return {
+ child.split(LOOKUP_SEP, 1)[0] for child in _get_children_from_q(q)
+ }
+
# `separators` argument to `json.dumps()` differs between 2.x and 3.x
# See: https://bugs.python.org/issue22767
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index cbc02e2c2..89c0a714c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -111,7 +111,7 @@ def get_attribute(instance, attrs):
# If we raised an Attribute or KeyError here it'd get treated
# as an omitted field in `Field.get_attribute()`. Instead we
# raise a ValueError to ensure the exception is not masked.
- raise ValueError('Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc))
+ raise ValueError(f'Exception raised in callable attribute "{attr}"; original exception was: {exc}')
return instance
@@ -986,10 +986,10 @@ class DecimalField(Field):
self.max_value = max_value
self.min_value = min_value
- if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
- warnings.warn("max_value should be a Decimal instance.")
- if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal):
- warnings.warn("min_value should be a Decimal instance.")
+ if self.max_value is not None and not isinstance(self.max_value, (int, decimal.Decimal)):
+ warnings.warn("max_value should be an integer or Decimal instance.")
+ if self.min_value is not None and not isinstance(self.min_value, (int, decimal.Decimal)):
+ 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:
self.max_whole_digits = self.max_digits - self.decimal_places
@@ -1103,7 +1103,7 @@ class DecimalField(Field):
if self.localize:
return localize_input(quantized)
- return '{:f}'.format(quantized)
+ return f'{quantized:f}'
def quantize(self, value):
"""
@@ -1861,7 +1861,7 @@ class SerializerMethodField(Field):
def bind(self, field_name, parent):
# The method name defaults to `get_{field_name}`.
if self.method_name is None:
- self.method_name = 'get_{field_name}'.format(field_name=field_name)
+ self.method_name = f'get_{field_name}'
super().bind(field_name, parent)
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.mo b/rest_framework/locale/ar/LC_MESSAGES/django.mo
index f793d7c73..a3c7c5ca2 100644
Binary files a/rest_framework/locale/ar/LC_MESSAGES/django.mo and b/rest_framework/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.po b/rest_framework/locale/ar/LC_MESSAGES/django.po
index 58f72ec17..be261d8dd 100644
--- a/rest_framework/locale/ar/LC_MESSAGES/django.po
+++ b/rest_framework/locale/ar/LC_MESSAGES/django.po
@@ -8,6 +8,7 @@
# Bashar Al-Abdulhadi, 2016-2017
# Eyad Toma , 2015,2017
# zak zak , 2020
+# Salman Saeed Albukhaitan , 2024
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
@@ -24,19 +25,19 @@ msgstr ""
#: authentication.py:70
msgid "Invalid basic header. No credentials provided."
-msgstr "رأس أساسي غير صالح, لم تقدم اي بيانات."
+msgstr "ترويسة أساسية غير صالحة. لم تقدم أي بيانات تفويض."
#: authentication.py:73
msgid "Invalid basic header. Credentials string should not contain spaces."
-msgstr "رأس أساسي غير صالح, سلسلة البيانات لا يجب أن تحتوي على أي أحرف مسافات"
+msgstr "ترويسة أساسية غير صالحة. يجب أن لا تحتوي سلسلة بيانات التفويض على مسافات."
#: authentication.py:83
msgid "Invalid basic header. Credentials not correctly base64 encoded."
-msgstr "رأس أساسي غير صالح, البيانات ليست مرمّزة بصحة على أساس64."
+msgstr "ترويسة أساسية غير صالحة. بيانات التفويض لم تُشفر بشكل صحيح بنظام أساس64."
#: authentication.py:101
msgid "Invalid username/password."
-msgstr "اسم المستخدم/كلمة السر غير صحيحين."
+msgstr "اسم المستخدم/كلمة المرور غير صحيحة."
#: authentication.py:104 authentication.py:206
msgid "User inactive or deleted."
@@ -93,7 +94,7 @@ msgstr "كلمة المرور"
#: authtoken/serializers.py:35
msgid "Unable to log in with provided credentials."
-msgstr "تعذر تسجيل الدخول بالبيانات التي ادخلتها."
+msgstr "تعذر تسجيل الدخول بالبيانات المدخلة."
#: authtoken/serializers.py:38
msgid "Must include \"username\" and \"password\"."
@@ -101,11 +102,11 @@ msgstr "يجب أن تتضمن \"اسم المستخدم\" و \"كلمة الم
#: exceptions.py:102
msgid "A server error occurred."
-msgstr "حدث خطأ في المخدم."
+msgstr "حدث خطأ في الخادم."
#: exceptions.py:142
msgid "Invalid input."
-msgstr ""
+msgstr "مدخل غير صالح."
#: exceptions.py:161
msgid "Malformed request."
@@ -130,11 +131,11 @@ msgstr "غير موجود."
#: exceptions.py:191
#, python-brace-format
msgid "Method \"{method}\" not allowed."
-msgstr "الطريقة \"{method}\" غير مسموح بها."
+msgstr "طريقة \"{method}\" غير مسموح بها."
#: exceptions.py:202
msgid "Could not satisfy the request Accept header."
-msgstr "لم نتمكن من تلبية الرٱس Accept في الطلب."
+msgstr "تعذر تلبية ترويسة Accept في الطلب."
#: exceptions.py:212
#, python-brace-format
@@ -143,17 +144,17 @@ msgstr "الوسيط \"{media_type}\" الموجود في الطلب غير مع
#: exceptions.py:223
msgid "Request was throttled."
-msgstr "تم تقييد الطلب."
+msgstr "تم حد الطلب."
#: exceptions.py:224
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "متوقع التوفر خلال {wait} ثانية."
#: exceptions.py:225
#, python-brace-format
msgid "Expected available in {wait} seconds."
-msgstr ""
+msgstr "متوقع التوفر خلال {wait} ثواني."
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
#: validators.py:183
@@ -166,11 +167,11 @@ msgstr "لا يمكن لهذا الحقل ان يكون فارغاً null."
#: fields.py:701
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "يجب أن يكون قيمة منطقية صالحة."
#: fields.py:766
msgid "Not a valid string."
-msgstr ""
+msgstr "ليس نصاً صالحاً."
#: fields.py:767
msgid "This field may not be blank."
@@ -179,16 +180,16 @@ msgstr "لا يمكن لهذا الحقل ان يكون فارغاً."
#: fields.py:768 fields.py:1881
#, python-brace-format
msgid "Ensure this field has no more than {max_length} characters."
-msgstr "تأكد ان الحقل لا يزيد عن {max_length} محرف."
+msgstr "تأكد ان عدد الحروف في هذا الحقل لا تتجاوز {max_length}."
#: fields.py:769
#, python-brace-format
msgid "Ensure this field has at least {min_length} characters."
-msgstr "تأكد ان الحقل {min_length} محرف على الاقل."
+msgstr "تأكد ان عدد الحروف في هذا الحقل لا يقل عن {min_length}."
#: fields.py:816
msgid "Enter a valid email address."
-msgstr "عليك ان تدخل بريد إلكتروني صالح."
+msgstr "يرجى إدخال عنوان بريد إلكتروني صحيح."
#: fields.py:827
msgid "This value does not match the required pattern."
@@ -204,7 +205,7 @@ msgstr "أدخل \"slug\" صالح يحتوي على حروف، أرقام، ش
msgid ""
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
"or hyphens."
-msgstr ""
+msgstr "أدخل \"slug\" صالح يحتوي على حروف يونيكود، أرقام، شُرط سفلية أو واصلات."
#: fields.py:854
msgid "Enter a valid URL."
@@ -212,7 +213,7 @@ msgstr "الرجاء إدخال رابط إلكتروني صالح."
#: fields.py:867
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "يجب أن يكون معرف UUID صالح."
#: fields.py:903
msgid "Enter a valid IPv4 or IPv6 address."
@@ -225,16 +226,16 @@ msgstr "الرجاء إدخال رقم صحيح صالح."
#: fields.py:932 fields.py:969 fields.py:1005 fields.py:1366
#, python-brace-format
msgid "Ensure this value is less than or equal to {max_value}."
-msgstr "تأكد ان القيمة أقل أو تساوي {max_value}."
+msgstr "تأكد ان القيمة أقل من أو تساوي {max_value}."
#: fields.py:933 fields.py:970 fields.py:1006 fields.py:1367
#, python-brace-format
msgid "Ensure this value is greater than or equal to {min_value}."
-msgstr "تأكد ان القيمة أكبر أو تساوي {min_value}."
+msgstr "تأكد ان القيمة أكبر من أو تساوي {min_value}."
#: fields.py:934 fields.py:971 fields.py:1010
msgid "String value too large."
-msgstr "السلسلة اطول من القيمة المسموح بها."
+msgstr "النص طويل جداً."
#: fields.py:968 fields.py:1004
msgid "A valid number is required."
@@ -249,7 +250,7 @@ msgstr "تأكد ان القيمة لا تحوي أكثر من {max_digits} رق
#, python-brace-format
msgid ""
"Ensure that there are no more than {max_decimal_places} decimal places."
-msgstr "تأكد انه لا يوجد اكثر من {max_decimal_places} منازل عشرية."
+msgstr "تأكد انه لا يوجد اكثر من {max_decimal_places} أرقام عشرية."
#: fields.py:1009
#, python-brace-format
@@ -261,7 +262,7 @@ msgstr "تأكد انه لا يوجد اكثر من {max_whole_digits} أرقا
#: fields.py:1148
#, python-brace-format
msgid "Datetime has wrong format. Use one of these formats instead: {format}."
-msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن تستخدم احد الصيغ التالية: {format}."
#: fields.py:1149
msgid "Expected a datetime but got a date."
@@ -270,11 +271,11 @@ msgstr "متوقع تاريخ و وقت و وجد تاريخ فقط"
#: fields.py:1150
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "تاريخ و وقت غير صالح للمنطقة الزمنية \"{timezone}\"."
#: fields.py:1151
msgid "Datetime value out of range."
-msgstr ""
+msgstr "قيمة التاريخ و الوقت خارج النطاق."
#: fields.py:1236
#, python-brace-format
@@ -293,12 +294,12 @@ msgstr "صيغة الوقت غير صحيحة. عليك أن تستخدم واح
#: fields.py:1365
#, python-brace-format
msgid "Duration has wrong format. Use one of these formats instead: {format}."
-msgstr "صيغة المدة غير صحيحه, يرجى إستخدام إحدى هذه الصيغ: {format}."
+msgstr "صيغة المدة غير صحيحة. يرجى استخدام احد الصيغ التالية: {format}."
#: fields.py:1399 fields.py:1456
#, python-brace-format
msgid "\"{input}\" is not a valid choice."
-msgstr "\"{input}\" ليست واحدة من الخيارات الصالحة."
+msgstr "\"{input}\" ليس خياراً صالحاً."
#: fields.py:1402
#, python-brace-format
@@ -317,7 +318,7 @@ msgstr "هذا التحديد لا يجب أن يكون فارغا."
#: fields.py:1495
#, python-brace-format
msgid "\"{input}\" is not a valid path choice."
-msgstr "{input} كإختيار مسار غير صالح."
+msgstr "{input} ليس خيار مسار صالح."
#: fields.py:1514
msgid "No file was submitted."
@@ -326,41 +327,41 @@ msgstr "لم يتم إرسال أي ملف."
#: fields.py:1515
msgid ""
"The submitted data was not a file. Check the encoding type on the form."
-msgstr "المعطيات المرسولة ليست ملف. إفحص نوع الترميز في النموذج."
+msgstr "البيانات المرسلة ليست ملف. تأكد من نوع الترميز في النموذج."
#: fields.py:1516
msgid "No filename could be determined."
-msgstr "ما من إسم ملف أمكن تحديده."
+msgstr "تعذر تحديد اسم الملف."
#: fields.py:1517
msgid "The submitted file is empty."
-msgstr "الملف الذي تم إرساله فارغ."
+msgstr "الملف المرسل فارغ."
#: fields.py:1518
#, python-brace-format
msgid ""
"Ensure this filename has at most {max_length} characters (it has {length})."
-msgstr "تأكد ان اسم الملف لا يحوي أكثر من {max_length} محرف (الإسم المرسل يحوي {length} محرف)."
+msgstr "تأكد ان طول إسم الملف لا يتجاوز {max_length} حرف (عدد الحروف الحالي {length})."
#: fields.py:1566
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
-msgstr "الرجاء تحميل صورة صالحة. الملف الذي تم تحميله إما لم يكن صورة او انه كان صورة تالفة."
+msgstr "يرجى رفع صورة صالحة. الملف الذي قمت برفعه ليس صورة أو أنه ملف تالف."
#: fields.py:1604 relations.py:486 serializers.py:571
msgid "This list may not be empty."
-msgstr "القائمة يجب أن لا تكون فارغة."
+msgstr "يجب أن لا تكون القائمة فارغة."
#: fields.py:1605
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "تأكد ان عدد العناصر في هذا الحقل لا يقل عن {min_length}."
#: fields.py:1606
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "تأكد ان عدد العناصر في هذا الحقل لا يتجاوز {max_length}."
#: fields.py:1682
#, python-brace-format
@@ -369,7 +370,7 @@ msgstr "المتوقع كان قاموس عناصر و لكن النوع الم
#: fields.py:1683
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "يجب أن لا يكون القاموس فارغاً."
#: fields.py:1755
msgid "Value must be valid JSON."
@@ -381,7 +382,7 @@ msgstr "بحث"
#: filters.py:50
msgid "A search term."
-msgstr ""
+msgstr "مصطلح البحث."
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
@@ -389,7 +390,7 @@ msgstr "الترتيب"
#: filters.py:181
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "أي حقل يجب استخدامه عند ترتيب النتائج."
#: filters.py:287
msgid "ascending"
@@ -401,11 +402,11 @@ msgstr "تنازلي"
#: pagination.py:174
msgid "A page number within the paginated result set."
-msgstr ""
+msgstr "رقم الصفحة ضمن النتائج المقسمة."
#: pagination.py:179 pagination.py:372 pagination.py:590
msgid "Number of results to return per page."
-msgstr ""
+msgstr "عدد النتائج التي يجب إرجاعها في كل صفحة."
#: pagination.py:189
msgid "Invalid page."
@@ -413,11 +414,11 @@ msgstr "صفحة غير صحيحة."
#: pagination.py:374
msgid "The initial index from which to return the results."
-msgstr ""
+msgstr "الفهرس الأولي الذي يجب البدء منه لإرجاع النتائج."
#: pagination.py:581
msgid "The pagination cursor value."
-msgstr ""
+msgstr "قيمة المؤشر للتقسيم."
#: pagination.py:583
msgid "Invalid cursor"
@@ -431,24 +432,24 @@ msgstr "معرف العنصر \"{pk_value}\" غير صالح - العنصر غ
#: relations.py:247
#, python-brace-format
msgid "Incorrect type. Expected pk value, received {data_type}."
-msgstr "نوع خاطئ. المتوقع قيمة من pk، لكن المتحصل عليه {data_type}."
+msgstr "نوع غير صحيح. يتوقع قيمة معرف العنصر، بينما حصل على {data_type}."
#: relations.py:280
msgid "Invalid hyperlink - No URL match."
-msgstr "إرتباط تشعبي غير صالح - لا مطابقة لURL."
+msgstr "رابط تشعبي غير صالح - لا يوجد تطابق URL."
#: relations.py:281
msgid "Invalid hyperlink - Incorrect URL match."
-msgstr "إرتباط تشعبي غير صالح - مطابقة خاطئة لURL."
+msgstr "رابط تشعبي غير صالح - تطابق URL غير صحيح."
#: relations.py:282
msgid "Invalid hyperlink - Object does not exist."
-msgstr "إرتباط تشعبي غير صالح - عنصر غير موجود."
+msgstr "رابط تشعبي غير صالح - العنصر غير موجود."
#: relations.py:283
#, python-brace-format
msgid "Incorrect type. Expected URL string, received {data_type}."
-msgstr "نوع خاطئ. المتوقع سلسلة URL، لكن المتحصل عليه {data_type}."
+msgstr "نوع غير صحيح. المتوقع هو رابط URL، ولكن تم الحصول على {data_type}."
#: relations.py:448
#, python-brace-format
@@ -461,20 +462,20 @@ msgstr "قيمة غير صالحة."
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "رقم صحيح فريد"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "نص UUID"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "قيمة فريدة"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "نوع {value_type} يحدد هذا {name}."
#: serializers.py:337
#, python-brace-format
@@ -484,7 +485,7 @@ msgstr "معطيات غير صالحة. المتوقع هو قاموس، لكن
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "إجراءات إضافية"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -493,27 +494,27 @@ msgstr "مرشحات"
#: templates/rest_framework/base.html:37
msgid "navbar"
-msgstr ""
+msgstr "شريط التنقل"
#: templates/rest_framework/base.html:75
msgid "content"
-msgstr ""
+msgstr "المحتوى"
#: templates/rest_framework/base.html:78
msgid "request form"
-msgstr ""
+msgstr "نموذج الطلب"
#: templates/rest_framework/base.html:157
msgid "main content"
-msgstr ""
+msgstr "المحتوى الرئيسي"
#: templates/rest_framework/base.html:173
msgid "request info"
-msgstr ""
+msgstr "معلومات الطلب"
#: templates/rest_framework/base.html:177
msgid "response info"
-msgstr ""
+msgstr "معلومات الرد"
#: templates/rest_framework/horizontal/radio.html:4
#: templates/rest_framework/inline/radio.html:3
@@ -525,7 +526,7 @@ msgstr "لا شيء"
#: templates/rest_framework/inline/select_multiple.html:3
#: templates/rest_framework/vertical/select_multiple.html:3
msgid "No items to select."
-msgstr "ما من عناصر للتحديد."
+msgstr "لا يوجد عناصر لتحديدها."
#: validators.py:39
msgid "This field must be unique."
@@ -539,7 +540,7 @@ msgstr "الحقول {field_names} يجب أن تشكل مجموعة فريدة.
#: validators.py:171
#, python-brace-format
msgid "Surrogate characters are not allowed: U+{code_point:X}."
-msgstr ""
+msgstr "لا يُسمح بالحروف البديلة: U+{code_point:X}."
#: validators.py:243
#, python-brace-format
@@ -558,11 +559,11 @@ msgstr "الحقل يجب ان يكون فريد للعام {date_field}."
#: versioning.py:40
msgid "Invalid version in \"Accept\" header."
-msgstr "إصدار غير صالح في الرٱس \"Accept\"."
+msgstr "إصدار غير صالح في ترويسة \"Accept\"."
#: versioning.py:71
msgid "Invalid version in URL path."
-msgstr "إصدار غير صالح في المسار URL."
+msgstr "نسخة غير صالحة في مسار URL."
#: versioning.py:116
msgid "Invalid version in URL path. Does not match any version namespace."
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.mo b/rest_framework/locale/de/LC_MESSAGES/django.mo
index 986880397..99bdec2c0 100644
Binary files a/rest_framework/locale/de/LC_MESSAGES/django.mo and b/rest_framework/locale/de/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.po b/rest_framework/locale/de/LC_MESSAGES/django.po
index 12ae5ba18..48e59e879 100644
--- a/rest_framework/locale/de/LC_MESSAGES/django.po
+++ b/rest_framework/locale/de/LC_MESSAGES/django.po
@@ -11,6 +11,7 @@
# Thomas Tanner, 2015
# Tom Jaster , 2015
# Xavier Ordoquy , 2015
+# stefan6419846, 2025
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
@@ -27,19 +28,19 @@ msgstr ""
#: authentication.py:70
msgid "Invalid basic header. No credentials provided."
-msgstr "Ungültiger basic header. Keine Zugangsdaten angegeben."
+msgstr "Ungültiger Basic Header. Keine Zugangsdaten angegeben."
#: authentication.py:73
msgid "Invalid basic header. Credentials string should not contain spaces."
-msgstr "Ungültiger basic header. Zugangsdaten sollen keine Leerzeichen enthalten."
+msgstr "Ungültiger Basic Header. Zugangsdaten sollen keine Leerzeichen enthalten."
#: authentication.py:83
msgid "Invalid basic header. Credentials not correctly base64 encoded."
-msgstr "Ungültiger basic header. Zugangsdaten sind nicht korrekt mit base64 kodiert."
+msgstr "Ungültiger Basic Header. Zugangsdaten sind nicht korrekt mit base64 kodiert."
#: authentication.py:101
msgid "Invalid username/password."
-msgstr "Ungültiger Benutzername/Passwort"
+msgstr "Ungültiger Benutzername/Passwort."
#: authentication.py:104 authentication.py:206
msgid "User inactive or deleted."
@@ -47,16 +48,16 @@ msgstr "Benutzer inaktiv oder gelöscht."
#: authentication.py:184
msgid "Invalid token header. No credentials provided."
-msgstr "Ungültiger token header. Keine Zugangsdaten angegeben."
+msgstr "Ungültiger Token Header. Keine Zugangsdaten angegeben."
#: authentication.py:187
msgid "Invalid token header. Token string should not contain spaces."
-msgstr "Ungültiger token header. Zugangsdaten sollen keine Leerzeichen enthalten."
+msgstr "Ungültiger Token Header. Zugangsdaten sollen keine Leerzeichen enthalten."
#: authentication.py:193
msgid ""
"Invalid token header. Token string should not contain invalid characters."
-msgstr "Ungültiger Token Header. Tokens dürfen keine ungültigen Zeichen enthalten."
+msgstr "Ungültiger Token Header. Zugangsdaten dürfen keine ungültigen Zeichen enthalten."
#: authentication.py:203
msgid "Invalid token."
@@ -108,7 +109,7 @@ msgstr "Ein Serverfehler ist aufgetreten."
#: exceptions.py:142
msgid "Invalid input."
-msgstr ""
+msgstr "Ungültige Eingabe."
#: exceptions.py:161
msgid "Malformed request."
@@ -124,7 +125,7 @@ msgstr "Anmeldedaten fehlen."
#: exceptions.py:179
msgid "You do not have permission to perform this action."
-msgstr "Sie sind nicht berechtigt diese Aktion durchzuführen."
+msgstr "Sie sind nicht berechtigt, diese Aktion durchzuführen."
#: exceptions.py:185
msgid "Not found."
@@ -151,17 +152,17 @@ msgstr "Die Anfrage wurde gedrosselt."
#: exceptions.py:224
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "Erwarte Verfügbarkeit in {wait} Sekunde."
#: exceptions.py:225
#, python-brace-format
msgid "Expected available in {wait} seconds."
-msgstr ""
+msgstr "Erwarte Verfügbarkeit in {wait} Sekunden."
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
#: validators.py:183
msgid "This field is required."
-msgstr "Dieses Feld ist erforderlich."
+msgstr "Dieses Feld ist zwingend erforderlich."
#: fields.py:317
msgid "This field may not be null."
@@ -169,11 +170,11 @@ msgstr "Dieses Feld darf nicht null sein."
#: fields.py:701
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "Muss ein gültiger Wahrheitswert sein."
#: fields.py:766
msgid "Not a valid string."
-msgstr ""
+msgstr "Kein gültiger String."
#: fields.py:767
msgid "This field may not be blank."
@@ -207,7 +208,7 @@ msgstr "Gib ein gültiges \"slug\" aus Buchstaben, Ziffern, Unterstrichen und Mi
msgid ""
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
"or hyphens."
-msgstr ""
+msgstr "Gib ein gültiges \"slug\" aus Unicode-Buchstaben, Ziffern, Unterstrichen und Minuszeichen ein."
#: fields.py:854
msgid "Enter a valid URL."
@@ -215,11 +216,11 @@ msgstr "Gib eine gültige URL ein."
#: fields.py:867
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "Muss eine gültige UUID sein."
#: fields.py:903
msgid "Enter a valid IPv4 or IPv6 address."
-msgstr "Geben Sie eine gültige IPv4 oder IPv6 Adresse an"
+msgstr "Geben Sie eine gültige IPv4 oder IPv6 Adresse an."
#: fields.py:931
msgid "A valid integer is required."
@@ -273,11 +274,11 @@ msgstr "Erwarte eine Datums- und Zeitangabe, erhielt aber ein Datum."
#: fields.py:1150
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "Ungültige Datumsangabe für die Zeitzone \"{timezone}\"."
#: fields.py:1151
msgid "Datetime value out of range."
-msgstr ""
+msgstr "Datumsangabe außerhalb des Bereichs."
#: fields.py:1236
#, python-brace-format
@@ -358,12 +359,12 @@ msgstr "Diese Liste darf nicht leer sein."
#: fields.py:1605
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "Dieses Feld muss mindestens {min_length} Einträge enthalten."
#: fields.py:1606
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "Dieses Feld darf nicht mehr als {max_length} Einträge enthalten."
#: fields.py:1682
#, python-brace-format
@@ -372,7 +373,7 @@ msgstr "Erwartete ein Dictionary mit Elementen, erhielt aber den Typ \"{input_ty
#: fields.py:1683
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "Dieses Dictionary darf nicht leer sein."
#: fields.py:1755
msgid "Value must be valid JSON."
@@ -384,7 +385,7 @@ msgstr "Suche"
#: filters.py:50
msgid "A search term."
-msgstr ""
+msgstr "Ein Suchbegriff."
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
@@ -392,7 +393,7 @@ msgstr "Sortierung"
#: filters.py:181
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "Feld, das zum Sortieren der Ergebnisse verwendet werden soll."
#: filters.py:287
msgid "ascending"
@@ -404,11 +405,11 @@ msgstr "Absteigend"
#: pagination.py:174
msgid "A page number within the paginated result set."
-msgstr ""
+msgstr "Eine Seitenzahl in der paginierten Ergebnismenge."
#: pagination.py:179 pagination.py:372 pagination.py:590
msgid "Number of results to return per page."
-msgstr ""
+msgstr "Anzahl der pro Seite zurückzugebenden Ergebnisse."
#: pagination.py:189
msgid "Invalid page."
@@ -416,11 +417,11 @@ msgstr "Ungültige Seite."
#: pagination.py:374
msgid "The initial index from which to return the results."
-msgstr ""
+msgstr "Der initiale Index, von dem die Ergebnisse zurückgegeben werden sollen."
#: pagination.py:581
msgid "The pagination cursor value."
-msgstr ""
+msgstr "Der Zeigerwert für die Paginierung"
#: pagination.py:583
msgid "Invalid cursor"
@@ -434,7 +435,7 @@ msgstr "Ungültiger pk \"{pk_value}\" - Object existiert nicht."
#: relations.py:247
#, python-brace-format
msgid "Incorrect type. Expected pk value, received {data_type}."
-msgstr "Falscher Typ. Erwarte pk Wert, erhielt aber {data_type}."
+msgstr "Falscher Typ. Erwarte pk-Wert, erhielt aber {data_type}."
#: relations.py:280
msgid "Invalid hyperlink - No URL match."
@@ -451,7 +452,7 @@ msgstr "Ungültiger Hyperlink - Objekt existiert nicht."
#: relations.py:283
#, python-brace-format
msgid "Incorrect type. Expected URL string, received {data_type}."
-msgstr "Falscher Typ. Erwarte URL Zeichenkette, erhielt aber {data_type}."
+msgstr "Falscher Typ. Erwarte URL-Zeichenkette, erhielt aber {data_type}."
#: relations.py:448
#, python-brace-format
@@ -464,20 +465,20 @@ msgstr "Ungültiger Wert."
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "eindeutiger Ganzzahl-Wert"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "UUID-String"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "eindeutiger Wert"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "Ein {value_type}, der {name} identifiziert."
#: serializers.py:337
#, python-brace-format
@@ -487,7 +488,7 @@ msgstr "Ungültige Daten. Dictionary erwartet, aber {datatype} erhalten."
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "Zusätzliche Aktionen"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -496,27 +497,27 @@ msgstr "Filter"
#: templates/rest_framework/base.html:37
msgid "navbar"
-msgstr ""
+msgstr "Navigation"
#: templates/rest_framework/base.html:75
msgid "content"
-msgstr ""
+msgstr "Inhalt"
#: templates/rest_framework/base.html:78
msgid "request form"
-msgstr ""
+msgstr "Anfrage-Formular"
#: templates/rest_framework/base.html:157
msgid "main content"
-msgstr ""
+msgstr "Hauptteil"
#: templates/rest_framework/base.html:173
msgid "request info"
-msgstr ""
+msgstr "Anfrage-Informationen"
#: templates/rest_framework/base.html:177
msgid "response info"
-msgstr ""
+msgstr "Antwort-Informationen"
#: templates/rest_framework/horizontal/radio.html:4
#: templates/rest_framework/inline/radio.html:3
@@ -542,7 +543,7 @@ msgstr "Die Felder {field_names} müssen eine eindeutige Menge bilden."
#: validators.py:171
#, python-brace-format
msgid "Surrogate characters are not allowed: U+{code_point:X}."
-msgstr ""
+msgstr "Ersatzzeichen sind nicht erlaubt: U+{code_point:X}."
#: validators.py:243
#, python-brace-format
@@ -565,7 +566,7 @@ msgstr "Ungültige Version in der \"Accept\" Kopfzeile."
#: versioning.py:71
msgid "Invalid version in URL path."
-msgstr "Ungültige Version im URL Pfad."
+msgstr "Ungültige Version im URL-Pfad."
#: versioning.py:116
msgid "Invalid version in URL path. Does not match any version namespace."
diff --git a/rest_framework/locale/fa/LC_MESSAGES/django.mo b/rest_framework/locale/fa/LC_MESSAGES/django.mo
index 099318e69..53b8fd98e 100644
Binary files a/rest_framework/locale/fa/LC_MESSAGES/django.mo and b/rest_framework/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/fa/LC_MESSAGES/django.po b/rest_framework/locale/fa/LC_MESSAGES/django.po
index 6a5b99acf..8d76372fa 100644
--- a/rest_framework/locale/fa/LC_MESSAGES/django.po
+++ b/rest_framework/locale/fa/LC_MESSAGES/django.po
@@ -7,6 +7,7 @@
# Aryan Baghi , 2020
# Omid Zarin , 2019
# Xavier Ordoquy , 2020
+# Sina Amini , 2024
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
@@ -104,7 +105,7 @@ msgstr "خطای سمت سرور رخ داده است."
#: exceptions.py:142
msgid "Invalid input."
-msgstr ""
+msgstr "ورودی نامعتبر"
#: exceptions.py:161
msgid "Malformed request."
@@ -147,12 +148,12 @@ msgstr "تعداد درخواستهای شما محدود شده است."
#: exceptions.py:224
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "انتظار میرود در {wait} ثانیه در دسترس باشد."
#: exceptions.py:225
#, python-brace-format
msgid "Expected available in {wait} seconds."
-msgstr ""
+msgstr "انتظار میرود در {wait} ثانیه در دسترس باشد."
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
#: validators.py:183
@@ -165,11 +166,11 @@ msgstr "این مقدار نباید توهی باشد."
#: fields.py:701
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "باید یک مقدار منطقی(بولی) معتبر باشد."
#: fields.py:766
msgid "Not a valid string."
-msgstr ""
+msgstr "یک رشته معتبر نیست."
#: fields.py:767
msgid "This field may not be blank."
@@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
#: fields.py:816
msgid "Enter a valid email address."
-msgstr "پست الکترونیکی صحبح وارد کنید."
+msgstr "پست الکترونیکی صحیح وارد کنید."
#: fields.py:827
msgid "This value does not match the required pattern."
@@ -211,7 +212,7 @@ msgstr "یک URL معتبر وارد کنید"
#: fields.py:867
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "باید یک UUID معتبر باشد."
#: fields.py:903
msgid "Enter a valid IPv4 or IPv6 address."
@@ -269,11 +270,11 @@ msgstr "باید datetime باشد اما date دریافت شد."
#: fields.py:1150
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "تاریخ و زمان برای منطقه زمانی \"{timezone}\" نامعتبر است."
#: fields.py:1151
msgid "Datetime value out of range."
-msgstr ""
+msgstr "مقدار تاریخ و زمان خارج از محدوده است."
#: fields.py:1236
#, python-brace-format
@@ -354,12 +355,12 @@ msgstr "این لیست نمی تواند خالی باشد"
#: fields.py:1605
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "اطمینان حاصل کنید که این فیلد حداقل {min_length} عنصر دارد."
#: fields.py:1606
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "اطمینان حاصل کنید که این فیلد بیش از {max_length} عنصر ندارد."
#: fields.py:1682
#, python-brace-format
@@ -368,7 +369,7 @@ msgstr "باید دیکشنری از آیتم ها ارسال می شد، اما
#: fields.py:1683
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "این دیکشنری نمیتواند خالی باشد."
#: fields.py:1755
msgid "Value must be valid JSON."
@@ -380,7 +381,7 @@ msgstr "جستجو"
#: filters.py:50
msgid "A search term."
-msgstr ""
+msgstr "یک عبارت جستجو."
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
@@ -388,7 +389,7 @@ msgstr "ترتیب"
#: filters.py:181
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "کدام فیلد باید هنگام مرتبسازی نتایج استفاده شود."
#: filters.py:287
msgid "ascending"
@@ -400,11 +401,11 @@ msgstr "نزولی"
#: pagination.py:174
msgid "A page number within the paginated result set."
-msgstr ""
+msgstr "یک شماره صفحه در مجموعه نتایج صفحهبندی شده."
#: pagination.py:179 pagination.py:372 pagination.py:590
msgid "Number of results to return per page."
-msgstr ""
+msgstr "تعداد نتایج برای نمایش در هر صفحه."
#: pagination.py:189
msgid "Invalid page."
@@ -412,11 +413,11 @@ msgstr "صفحه نامعتبر"
#: pagination.py:374
msgid "The initial index from which to return the results."
-msgstr ""
+msgstr "ایندکس اولیهای که از آن نتایج بازگردانده میشود."
#: pagination.py:581
msgid "The pagination cursor value."
-msgstr ""
+msgstr "مقدار نشانگر صفحهبندی."
#: pagination.py:583
msgid "Invalid cursor"
@@ -460,20 +461,20 @@ msgstr "مقدار نامعتبر."
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "مقداد عدد یکتا"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "رشته UUID"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "مقدار یکتا"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "یک {value_type} که این {name} را شناسایی میکند."
#: serializers.py:337
#, python-brace-format
@@ -483,7 +484,7 @@ msgstr "داده نامعتبر. باید دیکشنری ارسال می شد،
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "اقدامات اضافی"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -492,27 +493,27 @@ msgstr "فیلترها"
#: templates/rest_framework/base.html:37
msgid "navbar"
-msgstr ""
+msgstr "نوار ناوبری"
#: templates/rest_framework/base.html:75
msgid "content"
-msgstr ""
+msgstr "محتوا"
#: templates/rest_framework/base.html:78
msgid "request form"
-msgstr ""
+msgstr "فرم درخواست"
#: templates/rest_framework/base.html:157
msgid "main content"
-msgstr ""
+msgstr "محتوای اصلی"
#: templates/rest_framework/base.html:173
msgid "request info"
-msgstr ""
+msgstr "اطلاعات درخواست"
#: templates/rest_framework/base.html:177
msgid "response info"
-msgstr ""
+msgstr "اطلاعات پاسخ"
#: templates/rest_framework/horizontal/radio.html:4
#: templates/rest_framework/inline/radio.html:3
@@ -538,7 +539,7 @@ msgstr "فیلدهای {field_names} باید یک مجموعه یکتا باش
#: validators.py:171
#, python-brace-format
msgid "Surrogate characters are not allowed: U+{code_point:X}."
-msgstr ""
+msgstr "کاراکترهای جایگزین مجاز نیستند: U+{code_point:X}."
#: validators.py:243
#, python-brace-format
diff --git a/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo b/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo
index 52d3f3bf8..35775d9f2 100644
Binary files a/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo and b/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/fa_IR/LC_MESSAGES/django.po b/rest_framework/locale/fa_IR/LC_MESSAGES/django.po
index 61361d50e..280725a73 100644
--- a/rest_framework/locale/fa_IR/LC_MESSAGES/django.po
+++ b/rest_framework/locale/fa_IR/LC_MESSAGES/django.po
@@ -7,6 +7,7 @@
# Aryan Baghi , 2020
# Omid Zarin , 2019
# Xavier Ordoquy , 2020
+# Sina Amini , 2024
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
@@ -187,7 +188,7 @@ msgstr "مطمعن شوید طول این مقدار حداقل {min_length} ا
#: fields.py:816
msgid "Enter a valid email address."
-msgstr "پست الکترونیکی صحبح وارد کنید."
+msgstr "پست الکترونیکی صحیح وارد کنید."
#: fields.py:827
msgid "This value does not match the required pattern."
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo
index c3aeb27a9..2228adcf7 100644
Binary files a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo and b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
index 722dc2c82..f98cad67d 100644
--- a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
+++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
@@ -1,37 +1,37 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
-#
+#
# Translators:
-# GarakdongBigBoy , 2017
+# JAEGYUN JUNG , 2024
# Hochul Kwak , 2018
+# GarakdongBigBoy , 2017
# Joon Hwan 김준환 , 2017
# SUN CHOI , 2015
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-10-13 21:45+0200\n"
+"POT-Creation-Date: 2024-10-22 16:13+0900\n"
"PO-Revision-Date: 2020-10-13 19:45+0000\n"
-"Last-Translator: Xavier Ordoquy \n"
-"Language-Team: Korean (Korea) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ko_KR/)\n"
+"Last-Translator: JAEGYUN JUNG \n"
+"Language: ko_KR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language: ko_KR\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: authentication.py:70
msgid "Invalid basic header. No credentials provided."
-msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+msgstr "기본 헤더가 유효하지 않습니다. 인증 데이터가 제공되지 않았습니다."
#: authentication.py:73
msgid "Invalid basic header. Credentials string should not contain spaces."
-msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials) 문자열은 빈칸(spaces)을 포함하지 않아야 합니다."
+msgstr "기본 헤더가 유효하지 않습니다. 인증 데이터 문자열은 공백을 포함하지 않아야 합니다."
-#: authentication.py:83
+#: authentication.py:84
msgid "Invalid basic header. Credentials not correctly base64 encoded."
-msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 base64로 적절히 부호화(encode)되지 않았습니다."
+msgstr "기본 헤더가 유효하지 않습니다. 인증 데이터가 올바르게 base64 인코딩되지 않았습니다."
#: authentication.py:101
msgid "Invalid username/password."
@@ -43,11 +43,11 @@ msgstr "계정이 중지되었거나 삭제되었습니다."
#: authentication.py:184
msgid "Invalid token header. No credentials provided."
-msgstr "토큰 헤더가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+msgstr "토큰 헤더가 유효하지 않습니다. 인증 데이터가 제공되지 않았습니다."
#: authentication.py:187
msgid "Invalid token header. Token string should not contain spaces."
-msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 빈칸(spaces)을 포함하지 않아야 합니다."
+msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 공백을 포함하지 않아야 합니다."
#: authentication.py:193
msgid ""
@@ -58,6 +58,10 @@ msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 유효
msgid "Invalid token."
msgstr "토큰이 유효하지 않습니다."
+#: authtoken/admin.py:28 authtoken/serializers.py:9
+msgid "Username"
+msgstr "사용자 이름"
+
#: authtoken/apps.py:7
msgid "Auth Token"
msgstr "인증 토큰"
@@ -68,23 +72,19 @@ msgstr "키"
#: authtoken/models.py:16
msgid "User"
-msgstr "유저"
+msgstr "사용자"
#: authtoken/models.py:18
msgid "Created"
-msgstr "생성됨"
+msgstr "생성일시"
-#: authtoken/models.py:27 authtoken/serializers.py:19
+#: authtoken/models.py:27 authtoken/models.py:54 authtoken/serializers.py:19
msgid "Token"
msgstr "토큰"
-#: authtoken/models.py:28
+#: authtoken/models.py:28 authtoken/models.py:55
msgid "Tokens"
-msgstr ""
-
-#: authtoken/serializers.py:9
-msgid "Username"
-msgstr "유저이름"
+msgstr "토큰(들)"
#: authtoken/serializers.py:13
msgid "Password"
@@ -92,390 +92,398 @@ msgstr "비밀번호"
#: authtoken/serializers.py:35
msgid "Unable to log in with provided credentials."
-msgstr "제공된 인증데이터(credentials)로는 로그인할 수 없습니다."
+msgstr "제공된 인증 데이터로는 로그인할 수 없습니다."
#: authtoken/serializers.py:38
msgid "Must include \"username\" and \"password\"."
msgstr "\"아이디\"와 \"비밀번호\"를 포함해야 합니다."
-#: exceptions.py:102
+#: exceptions.py:105
msgid "A server error occurred."
msgstr "서버 장애가 발생했습니다."
-#: exceptions.py:142
+#: exceptions.py:145
msgid "Invalid input."
-msgstr ""
+msgstr "유효하지 않은 입력입니다."
-#: exceptions.py:161
+#: exceptions.py:166
msgid "Malformed request."
msgstr "잘못된 요청입니다."
-#: exceptions.py:167
+#: exceptions.py:172
msgid "Incorrect authentication credentials."
-msgstr "자격 인증데이터(authentication credentials)가 정확하지 않습니다."
+msgstr "자격 인증 데이터가 올바르지 않습니다."
-#: exceptions.py:173
+#: exceptions.py:178
msgid "Authentication credentials were not provided."
-msgstr "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
+msgstr "자격 인증 데이터가 제공되지 않았습니다."
-#: exceptions.py:179
+#: exceptions.py:184
msgid "You do not have permission to perform this action."
-msgstr "이 작업을 수행할 권한(permission)이 없습니다."
+msgstr "이 작업을 수행할 권한이 없습니다."
-#: exceptions.py:185
+#: exceptions.py:190
msgid "Not found."
msgstr "찾을 수 없습니다."
-#: exceptions.py:191
+#: exceptions.py:196
#, python-brace-format
msgid "Method \"{method}\" not allowed."
-msgstr "메소드(Method) \"{method}\"는 허용되지 않습니다."
+msgstr "메서드 \"{method}\"는 허용되지 않습니다."
-#: exceptions.py:202
+#: exceptions.py:207
msgid "Could not satisfy the request Accept header."
-msgstr "Accept header 요청을 만족할 수 없습니다."
+msgstr "요청 Accept 헤더를 만족시킬 수 없습니다."
-#: exceptions.py:212
+#: exceptions.py:217
#, python-brace-format
msgid "Unsupported media type \"{media_type}\" in request."
msgstr "요청된 \"{media_type}\"가 지원되지 않는 미디어 형태입니다."
-#: exceptions.py:223
+#: exceptions.py:228
msgid "Request was throttled."
-msgstr "요청이 지연(throttled)되었습니다."
+msgstr "요청이 제한되었습니다."
-#: exceptions.py:224
+#: exceptions.py:229
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "{wait} 초 후에 사용 가능합니다."
-#: exceptions.py:225
+#: exceptions.py:230
#, python-brace-format
msgid "Expected available in {wait} seconds."
-msgstr ""
+msgstr "{wait} 초 후에 사용 가능합니다."
-#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
-#: validators.py:183
+#: fields.py:292 relations.py:240 relations.py:276 validators.py:99
+#: validators.py:219
msgid "This field is required."
msgstr "이 필드는 필수 항목입니다."
-#: fields.py:317
+#: fields.py:293
msgid "This field may not be null."
msgstr "이 필드는 null일 수 없습니다."
-#: fields.py:701
+#: fields.py:661
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "유효한 불리언이어야 합니다."
-#: fields.py:766
+#: fields.py:724
msgid "Not a valid string."
-msgstr ""
+msgstr "유효한 문자열이 아닙니다."
-#: fields.py:767
+#: fields.py:725
msgid "This field may not be blank."
msgstr "이 필드는 blank일 수 없습니다."
-#: fields.py:768 fields.py:1881
+#: fields.py:726 fields.py:1881
#, python-brace-format
msgid "Ensure this field has no more than {max_length} characters."
-msgstr "이 필드의 글자 수가 {max_length} 이하인지 확인하십시오."
+msgstr "이 필드의 글자 수가 {max_length} 이하인지 확인하세요."
-#: fields.py:769
+#: fields.py:727
#, python-brace-format
msgid "Ensure this field has at least {min_length} characters."
-msgstr "이 필드의 글자 수가 적어도 {min_length} 이상인지 확인하십시오."
+msgstr "이 필드의 글자 수가 적어도 {min_length} 이상인지 확인하세요."
-#: fields.py:816
+#: fields.py:774
msgid "Enter a valid email address."
-msgstr "유효한 이메일 주소를 입력하십시오."
+msgstr "유효한 이메일 주소를 입력하세요."
-#: fields.py:827
+#: fields.py:785
msgid "This value does not match the required pattern."
-msgstr "형식에 맞지 않는 값입니다."
+msgstr "이 값은 요구되는 패턴과 일치하지 않습니다."
-#: fields.py:838
+#: fields.py:796
msgid ""
"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
"hyphens."
-msgstr "문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하십시오."
+msgstr "문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하세요."
-#: fields.py:839
+#: fields.py:797
msgid ""
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
"or hyphens."
-msgstr ""
+msgstr "유니코드 문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하세요."
-#: fields.py:854
+#: fields.py:812
msgid "Enter a valid URL."
-msgstr "유효한 URL을 입력하십시오."
+msgstr "유효한 URL을 입력하세요."
-#: fields.py:867
+#: fields.py:825
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "유효한 UUID 이어야 합니다."
-#: fields.py:903
+#: fields.py:861
msgid "Enter a valid IPv4 or IPv6 address."
-msgstr "유효한 IPv4 또는 IPv6 주소를 입력하십시오."
+msgstr "유효한 IPv4 또는 IPv6 주소를 입력하세요."
-#: fields.py:931
+#: fields.py:889
msgid "A valid integer is required."
-msgstr "유효한 정수(integer)를 넣어주세요."
+msgstr "유효한 정수를 입력하세요."
-#: fields.py:932 fields.py:969 fields.py:1005 fields.py:1366
+#: fields.py:890 fields.py:927 fields.py:966 fields.py:1349
#, python-brace-format
msgid "Ensure this value is less than or equal to {max_value}."
-msgstr "이 값이 {max_value}보다 작거나 같은지 확인하십시오."
+msgstr "이 값이 {max_value}보다 작거나 같은지 확인하세요."
-#: fields.py:933 fields.py:970 fields.py:1006 fields.py:1367
+#: fields.py:891 fields.py:928 fields.py:967 fields.py:1350
#, python-brace-format
msgid "Ensure this value is greater than or equal to {min_value}."
-msgstr "이 값이 {min_value}보다 크거나 같은지 확인하십시오."
+msgstr "이 값이 {min_value}보다 크거나 같은지 확인하세요."
-#: fields.py:934 fields.py:971 fields.py:1010
+#: fields.py:892 fields.py:929 fields.py:971
msgid "String value too large."
-msgstr "문자열 값이 너무 큽니다."
+msgstr "문자열 값이 너무 깁니다."
-#: fields.py:968 fields.py:1004
+#: fields.py:926 fields.py:965
msgid "A valid number is required."
-msgstr "유효한 숫자를 넣어주세요."
+msgstr "유효한 숫자를 입력하세요."
-#: fields.py:1007
+#: fields.py:930
+msgid "Integer value too large to convert to float"
+msgstr "정수 값이 너무 커서 부동 소수점으로 변환할 수 없습니다."
+
+#: fields.py:968
#, python-brace-format
msgid "Ensure that there are no more than {max_digits} digits in total."
-msgstr "전체 숫자(digits)가 {max_digits} 이하인지 확인하십시오."
+msgstr "총 자릿수가 {max_digits}을(를) 초과하지 않는지 확인하세요."
-#: fields.py:1008
+#: fields.py:969
#, python-brace-format
-msgid ""
-"Ensure that there are no more than {max_decimal_places} decimal places."
-msgstr "소수점 자릿수가 {max_decimal_places} 이하인지 확인하십시오."
+msgid "Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "소수점 이하 자릿수가 {max_decimal_places}을(를) 초과하지 않는지 확인하세요."
-#: fields.py:1009
+#: fields.py:970
#, python-brace-format
msgid ""
"Ensure that there are no more than {max_whole_digits} digits before the "
"decimal point."
-msgstr "소수점 자리 앞에 숫자(digits)가 {max_whole_digits} 이하인지 확인하십시오."
+msgstr "소수점 앞 자릿수가 {max_whole_digits}을(를) 초과하지 않는지 확인하세요."
-#: fields.py:1148
+#: fields.py:1129
#, python-brace-format
msgid "Datetime has wrong format. Use one of these formats instead: {format}."
msgstr "Datetime의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
-#: fields.py:1149
+#: fields.py:1130
msgid "Expected a datetime but got a date."
-msgstr "예상된 datatime 대신 date를 받았습니다."
+msgstr "datatime이 예상되었지만 date를 받았습니다."
-#: fields.py:1150
+#: fields.py:1131
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "\"{timezone}\" 시간대에 대한 유효하지 않은 datetime 입니다."
-#: fields.py:1151
+#: fields.py:1132
msgid "Datetime value out of range."
-msgstr ""
+msgstr "Datetime 값이 범위를 벗어났습니다."
-#: fields.py:1236
+#: fields.py:1219
#, python-brace-format
msgid "Date has wrong format. Use one of these formats instead: {format}."
msgstr "Date의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
-#: fields.py:1237
+#: fields.py:1220
msgid "Expected a date but got a datetime."
msgstr "예상된 date 대신 datetime을 받았습니다."
-#: fields.py:1303
+#: fields.py:1286
#, python-brace-format
msgid "Time has wrong format. Use one of these formats instead: {format}."
msgstr "Time의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
-#: fields.py:1365
+#: fields.py:1348
#, python-brace-format
msgid "Duration has wrong format. Use one of these formats instead: {format}."
msgstr "Duration의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
-#: fields.py:1399 fields.py:1456
+#: fields.py:1351
+#, python-brace-format
+msgid "The number of days must be between {min_days} and {max_days}."
+msgstr "일수는 {min_days} 이상 {max_days} 이하이어야 합니다."
+
+#: fields.py:1386 fields.py:1446
#, python-brace-format
msgid "\"{input}\" is not a valid choice."
-msgstr "\"{input}\"이 유효하지 않은 선택(choice)입니다."
+msgstr "\"{input}\"은 유효하지 않은 선택입니다."
-#: fields.py:1402
+#: fields.py:1389
#, python-brace-format
msgid "More than {count} items..."
-msgstr ""
+msgstr "{count}개 이상의 아이템이 있습니다..."
-#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
+#: fields.py:1447 fields.py:1596 relations.py:486 serializers.py:593
#, python-brace-format
msgid "Expected a list of items but got type \"{input_type}\"."
msgstr "아이템 리스트가 예상되었으나 \"{input_type}\"를 받았습니다."
-#: fields.py:1458
+#: fields.py:1448
msgid "This selection may not be empty."
msgstr "이 선택 항목은 비워 둘 수 없습니다."
-#: fields.py:1495
+#: fields.py:1487
#, python-brace-format
msgid "\"{input}\" is not a valid path choice."
-msgstr "\"{input}\"이 유효하지 않은 경로 선택입니다."
+msgstr "\"{input}\"은 유효하지 않은 경로 선택입니다."
-#: fields.py:1514
+#: fields.py:1507
msgid "No file was submitted."
msgstr "파일이 제출되지 않았습니다."
-#: fields.py:1515
-msgid ""
-"The submitted data was not a file. Check the encoding type on the form."
+#: fields.py:1508
+msgid "The submitted data was not a file. Check the encoding type on the form."
msgstr "제출된 데이터는 파일이 아닙니다. 제출된 서식의 인코딩 형식을 확인하세요."
-#: fields.py:1516
+#: fields.py:1509
msgid "No filename could be determined."
msgstr "파일명을 알 수 없습니다."
-#: fields.py:1517
+#: fields.py:1510
msgid "The submitted file is empty."
msgstr "제출한 파일이 비어있습니다."
-#: fields.py:1518
+#: fields.py:1511
#, python-brace-format
msgid ""
"Ensure this filename has at most {max_length} characters (it has {length})."
-msgstr "이 파일명의 글자수가 최대 {max_length}를 넘지 않는지 확인하십시오. (이것은 {length}가 있습니다)."
+msgstr "이 파일명의 글자수가 최대 {max_length}자를 넘지 않는지 확인하세요. (현재 {length}자입니다)."
-#: fields.py:1566
+#: fields.py:1559
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
-msgstr "유효한 이미지 파일을 업로드 하십시오. 업로드 하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다."
+msgstr "유효한 이미지 파일을 업로드하세요. 업로드하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다."
-#: fields.py:1604 relations.py:486 serializers.py:571
+#: fields.py:1597 relations.py:487 serializers.py:594
msgid "This list may not be empty."
msgstr "이 리스트는 비워 둘 수 없습니다."
-#: fields.py:1605
+#: fields.py:1598 serializers.py:596
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "이 필드가 최소 {min_length} 개의 요소를 가지는지 확인하세요."
-#: fields.py:1606
+#: fields.py:1599 serializers.py:595
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "이 필드가 최대 {max_length} 개의 요소를 가지는지 확인하세요."
-#: fields.py:1682
+#: fields.py:1677
#, python-brace-format
msgid "Expected a dictionary of items but got type \"{input_type}\"."
msgstr "아이템 딕셔너리가 예상되었으나 \"{input_type}\" 타입을 받았습니다."
-#: fields.py:1683
+#: fields.py:1678
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "이 딕셔너리는 비어있을 수 없습니다."
-#: fields.py:1755
+#: fields.py:1750
msgid "Value must be valid JSON."
-msgstr "Value 는 유효한 JSON형식이어야 합니다."
+msgstr "유효한 JSON 값이어야 합니다."
-#: filters.py:49 templates/rest_framework/filters/search.html:2
+#: filters.py:72 templates/rest_framework/filters/search.html:2
+#: templates/rest_framework/filters/search.html:8
msgid "Search"
msgstr "검색"
-#: filters.py:50
+#: filters.py:73
msgid "A search term."
-msgstr ""
+msgstr "검색어."
-#: filters.py:180 templates/rest_framework/filters/ordering.html:3
+#: filters.py:224 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
msgstr "순서"
-#: filters.py:181
+#: filters.py:225
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "결과 정렬 시 사용할 필드."
-#: filters.py:287
+#: filters.py:341
msgid "ascending"
msgstr "오름차순"
-#: filters.py:288
+#: filters.py:342
msgid "descending"
msgstr "내림차순"
-#: pagination.py:174
+#: pagination.py:180
msgid "A page number within the paginated result set."
-msgstr ""
+msgstr "페이지네이션된 결과 집합 내의 페이지 번호."
-#: pagination.py:179 pagination.py:372 pagination.py:590
+#: pagination.py:185 pagination.py:382 pagination.py:599
msgid "Number of results to return per page."
-msgstr ""
+msgstr "페이지당 반환할 결과 수."
-#: pagination.py:189
+#: pagination.py:195
msgid "Invalid page."
msgstr "페이지가 유효하지 않습니다."
-#: pagination.py:374
+#: pagination.py:384
msgid "The initial index from which to return the results."
-msgstr ""
+msgstr "결과를 반환할 초기 인덱스."
-#: pagination.py:581
+#: pagination.py:590
msgid "The pagination cursor value."
-msgstr ""
+msgstr "페이지네이션 커서 값."
-#: pagination.py:583
+#: pagination.py:592
msgid "Invalid cursor"
-msgstr "커서(cursor)가 유효하지 않습니다."
+msgstr "커서가 유효하지 않습니다."
-#: relations.py:246
+#: relations.py:241
#, python-brace-format
msgid "Invalid pk \"{pk_value}\" - object does not exist."
msgstr "유효하지 않은 pk \"{pk_value}\" - 객체가 존재하지 않습니다."
-#: relations.py:247
+#: relations.py:242
#, python-brace-format
msgid "Incorrect type. Expected pk value, received {data_type}."
-msgstr "잘못된 형식입니다. pk 값 대신 {data_type}를 받았습니다."
+msgstr "잘못된 형식입니다. pk 값이 예상되었지만, {data_type}을(를) 받았습니다."
-#: relations.py:280
+#: relations.py:277
msgid "Invalid hyperlink - No URL match."
msgstr "유효하지 않은 하이퍼링크 - 일치하는 URL이 없습니다."
-#: relations.py:281
+#: relations.py:278
msgid "Invalid hyperlink - Incorrect URL match."
msgstr "유효하지 않은 하이퍼링크 - URL이 일치하지 않습니다."
-#: relations.py:282
+#: relations.py:279
msgid "Invalid hyperlink - Object does not exist."
msgstr "유효하지 않은 하이퍼링크 - 객체가 존재하지 않습니다."
-#: relations.py:283
+#: relations.py:280
#, python-brace-format
msgid "Incorrect type. Expected URL string, received {data_type}."
msgstr "잘못된 형식입니다. URL 문자열을 예상했으나 {data_type}을 받았습니다."
-#: relations.py:448
+#: relations.py:445
#, python-brace-format
msgid "Object with {slug_name}={value} does not exist."
msgstr "{slug_name}={value} 객체가 존재하지 않습니다."
-#: relations.py:449
+#: relations.py:446
msgid "Invalid value."
msgstr "값이 유효하지 않습니다."
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "고유한 정수 값"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "UUID 문자열"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "고유한 값"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "{name}을 식별하는 {value_type}."
-#: serializers.py:337
+#: serializers.py:340
#, python-brace-format
msgid "Invalid data. Expected a dictionary, but got {datatype}."
msgstr "유효하지 않은 데이터. 딕셔너리(dictionary)대신 {datatype}를 받았습니다."
@@ -483,7 +491,7 @@ msgstr "유효하지 않은 데이터. 딕셔너리(dictionary)대신 {datatype}
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "추가 Action들"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -492,33 +500,33 @@ msgstr "필터"
#: templates/rest_framework/base.html:37
msgid "navbar"
-msgstr ""
+msgstr "네비게이션 바"
#: templates/rest_framework/base.html:75
msgid "content"
-msgstr ""
+msgstr "콘텐츠"
#: templates/rest_framework/base.html:78
msgid "request form"
-msgstr ""
+msgstr "요청 폼"
#: templates/rest_framework/base.html:157
msgid "main content"
-msgstr ""
+msgstr "메인 콘텐츠"
#: templates/rest_framework/base.html:173
msgid "request info"
-msgstr ""
+msgstr "요청 정보"
#: templates/rest_framework/base.html:177
msgid "response info"
-msgstr ""
+msgstr "응답 정보"
#: templates/rest_framework/horizontal/radio.html:4
#: templates/rest_framework/inline/radio.html:3
#: templates/rest_framework/vertical/radio.html:3
msgid "None"
-msgstr ""
+msgstr "없음"
#: templates/rest_framework/horizontal/select_multiple.html:4
#: templates/rest_framework/inline/select_multiple.html:3
@@ -528,49 +536,49 @@ msgstr "선택할 아이템이 없습니다."
#: validators.py:39
msgid "This field must be unique."
-msgstr "이 필드는 반드시 고유(unique)해야 합니다."
+msgstr "이 필드는 반드시 고유해야 합니다."
-#: validators.py:89
+#: validators.py:98
#, python-brace-format
msgid "The fields {field_names} must make a unique set."
-msgstr "필드 {field_names} 는 반드시 고유(unique)해야 합니다."
+msgstr "필드 {field_names} 는 반드시 고유해야 합니다."
-#: validators.py:171
+#: validators.py:200
#, python-brace-format
msgid "Surrogate characters are not allowed: U+{code_point:X}."
-msgstr ""
+msgstr "대체(surrogate) 문자는 허용되지 않습니다: U+{code_point:X}."
-#: validators.py:243
+#: validators.py:290
#, python-brace-format
msgid "This field must be unique for the \"{date_field}\" date."
-msgstr "이 필드는 고유(unique)한 \"{date_field}\" 날짜를 갖습니다."
+msgstr "이 필드는 \"{date_field}\" 날짜에 대해 고유해야 합니다."
-#: validators.py:258
+#: validators.py:305
#, python-brace-format
msgid "This field must be unique for the \"{date_field}\" month."
-msgstr "이 필드는 고유(unique)한 \"{date_field}\" 월을 갖습니다. "
+msgstr "이 필드는 \"{date_field}\" 월에 대해 고유해야 합니다."
-#: validators.py:271
+#: validators.py:318
#, python-brace-format
msgid "This field must be unique for the \"{date_field}\" year."
-msgstr "이 필드는 고유(unique)한 \"{date_field}\" 년을 갖습니다. "
+msgstr "이 필드는 \"{date_field}\" 연도에 대해 고유해야 합니다."
#: versioning.py:40
msgid "Invalid version in \"Accept\" header."
-msgstr "\"Accept\" 헤더(header)의 버전이 유효하지 않습니다."
+msgstr "\"Accept\" 헤더의 버전이 유효하지 않습니다."
#: versioning.py:71
msgid "Invalid version in URL path."
-msgstr "URL path의 버전이 유효하지 않습니다."
+msgstr "URL 경로의 버전이 유효하지 않습니다."
-#: versioning.py:116
+#: versioning.py:118
msgid "Invalid version in URL path. Does not match any version namespace."
-msgstr "URL 경로에 유효하지 않은 버전이 있습니다. 버전 네임 스페이스와 일치하지 않습니다."
+msgstr "URL 경로에 유효하지 않은 버전이 있습니다. 버전 네임스페이스와 일치하지 않습니다."
-#: versioning.py:148
+#: versioning.py:150
msgid "Invalid version in hostname."
-msgstr "hostname내 버전이 유효하지 않습니다."
+msgstr "hostname 내 버전이 유효하지 않습니다."
-#: versioning.py:170
+#: versioning.py:172
msgid "Invalid version in query parameter."
-msgstr "쿼리 파라메터내 버전이 유효하지 않습니다."
+msgstr "쿼리 파라메터 내 버전이 유효하지 않습니다."
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo
index 5a6e3788e..03a0651fa 100644
Binary files a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo and b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
index 40651552d..bfd53ee0f 100644
--- a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
+++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
@@ -1,7 +1,7 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
-#
+#
# Translators:
# Cloves Oliveira , 2020
# Craig Blaszczyk , 2015
@@ -9,6 +9,8 @@
# Filipe Rinaldi , 2015
# Hugo Leonardo Chalhoub Mendonça , 2015
# Jonatas Baldin , 2017
+# Gabriel Mitelman Tkacz , 2024
+# Matheus Oliveira , 2025
msgid ""
msgstr ""
"Project-Id-Version: Django REST framework\n"
@@ -106,11 +108,11 @@ msgstr "Ocorreu um erro de servidor."
#: exceptions.py:142
msgid "Invalid input."
-msgstr ""
+msgstr "Entrada inválida"
#: exceptions.py:161
msgid "Malformed request."
-msgstr "Pedido malformado."
+msgstr "Requisição malformada."
#: exceptions.py:167
msgid "Incorrect authentication credentials."
@@ -140,7 +142,7 @@ msgstr "Não foi possível satisfazer a requisição do cabeçalho Accept."
#: exceptions.py:212
#, python-brace-format
msgid "Unsupported media type \"{media_type}\" in request."
-msgstr "Tipo de mídia \"{media_type}\" no pedido não é suportado."
+msgstr "Tipo de mídia \"{media_type}\" no pedido não é suportado."
#: exceptions.py:223
msgid "Request was throttled."
@@ -149,12 +151,12 @@ msgstr "Pedido foi limitado."
#: exceptions.py:224
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "Disponível em {wait} segundo."
#: exceptions.py:225
#, python-brace-format
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
#: validators.py:183
@@ -167,15 +169,15 @@ msgstr "Este campo não pode ser nulo."
#: fields.py:701
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "Deve ser um valor booleano válido."
#: fields.py:766
msgid "Not a valid string."
-msgstr ""
+msgstr "Não é uma string válida."
#: fields.py:767
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
#, python-brace-format
@@ -199,21 +201,21 @@ msgstr "Este valor não corresponde ao padrão exigido."
msgid ""
"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
"hyphens."
-msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados ou hífens."
+msgstr "Insira um \"slug\" válido que consista de letras, números, sublinhados ou hífens."
#: fields.py:839
msgid ""
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
"or hyphens."
-msgstr ""
+msgstr "Insira um \"slug\" válido que consista de letras Unicode, números, sublinhados ou hífens."
#: fields.py:854
msgid "Enter a valid URL."
-msgstr "Entrar um URL válido."
+msgstr "Insira um URL válido."
#: fields.py:867
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "Deve ser um UUID válido."
#: fields.py:903
msgid "Enter a valid IPv4 or IPv6 address."
@@ -271,11 +273,11 @@ msgstr "Necessário uma data e hora mas recebeu uma data."
#: fields.py:1150
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "Data e hora inválidas para o fuso horário \"{timezone}\"."
#: fields.py:1151
msgid "Datetime value out of range."
-msgstr ""
+msgstr "Valor de data e hora fora do intervalo."
#: fields.py:1236
#, python-brace-format
@@ -289,17 +291,17 @@ msgstr "Necessário uma data mas recebeu uma data e hora."
#: fields.py:1303
#, python-brace-format
msgid "Time has wrong format. Use one of these formats instead: {format}."
-msgstr "Formato inválido para Tempo. Use um dos formatos a seguir: {format}."
+msgstr "Formato inválido para tempo. Use um dos formatos a seguir: {format}."
#: fields.py:1365
#, python-brace-format
msgid "Duration has wrong format. Use one of these formats instead: {format}."
-msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format}."
+msgstr "Formato inválido para duração. Use um dos formatos a seguir: {format}."
#: fields.py:1399 fields.py:1456
#, python-brace-format
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
#, python-brace-format
@@ -309,7 +311,7 @@ msgstr "Mais de {count} itens..."
#: fields.py:1457 fields.py:1603 relations.py:485 serializers.py:570
#, python-brace-format
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
msgid "This selection may not be empty."
@@ -347,7 +349,7 @@ msgstr "Certifique-se de que o nome do arquivo tem menos de {max_length} caracte
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
-msgstr "Fazer upload de uma imagem válida. O arquivo enviado não é um arquivo de imagem ou está corrompido."
+msgstr "Faça upload de uma imagem válida. O arquivo enviado não é um arquivo de imagem ou está corrompido."
#: fields.py:1604 relations.py:486 serializers.py:571
msgid "This list may not be empty."
@@ -356,25 +358,25 @@ msgstr "Esta lista não pode estar vazia."
#: fields.py:1605
#, python-brace-format
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
#, python-brace-format
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
#, python-brace-format
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
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "Este dicionário não pode estar vazio."
#: fields.py:1755
msgid "Value must be valid JSON."
-msgstr "Valor devo ser JSON válido."
+msgstr "Valor deve ser JSON válido."
#: filters.py:49 templates/rest_framework/filters/search.html:2
msgid "Search"
@@ -382,7 +384,7 @@ msgstr "Buscar"
#: filters.py:50
msgid "A search term."
-msgstr ""
+msgstr "Um termo de busca."
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
@@ -390,23 +392,23 @@ msgstr "Ordenando"
#: filters.py:181
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "Qual campo usar ao ordenar os resultados."
#: filters.py:287
msgid "ascending"
-msgstr "ascendente"
+msgstr "crescente"
#: filters.py:288
msgid "descending"
-msgstr "descendente"
+msgstr "decrescente"
#: pagination.py:174
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
msgid "Number of results to return per page."
-msgstr ""
+msgstr "Número de resultados a serem retornados por página."
#: pagination.py:189
msgid "Invalid page."
@@ -414,11 +416,11 @@ msgstr "Página inválida."
#: pagination.py:374
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
msgid "The pagination cursor value."
-msgstr ""
+msgstr "O valor do cursor de paginação."
#: pagination.py:583
msgid "Invalid cursor"
@@ -432,7 +434,7 @@ msgstr "Pk inválido \"{pk_value}\" - objeto não existe."
#: relations.py:247
#, python-brace-format
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
msgid "Invalid hyperlink - No URL match."
@@ -462,20 +464,20 @@ msgstr "Valor inválido."
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "valor inteiro único"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "string UUID"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "valor único"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "Um {value_type} que identifica este {name}."
#: serializers.py:337
#, python-brace-format
@@ -485,7 +487,7 @@ msgstr "Dado inválido. Necessário um dicionário mas recebeu {datatype}."
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "Ações Extras"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -530,7 +532,7 @@ msgstr "Nenhum item para escholher."
#: validators.py:39
msgid "This field must be unique."
-msgstr "Esse campo deve ser único."
+msgstr "Esse campo deve ser único."
#: validators.py:89
#, python-brace-format
@@ -540,7 +542,7 @@ msgstr "Os campos {field_names} devem criar um set único."
#: validators.py:171
#, python-brace-format
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
#, python-brace-format
diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
index 7e131db42..719df05a1 100644
--- a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
+++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
@@ -353,12 +353,12 @@ msgstr "列表字段不能为空值。"
#: fields.py:1605
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "请确保这个字段至少包含 {min_length} 个元素。"
#: fields.py:1606
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "请确保这个字段不能超过 {max_length} 个元素。"
#: fields.py:1682
#, python-brace-format
@@ -367,7 +367,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。"
#: fields.py:1683
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "这个字典可能不是空的。"
#: fields.py:1755
msgid "Value must be valid JSON."
diff --git a/rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo
index 670228a83..b30686c1f 100644
Binary files a/rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo and b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/zh_Hans/LC_MESSAGES/django.po b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.po
index ce85b52c7..f85c7119e 100644
--- a/rest_framework/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.po
@@ -104,7 +104,7 @@ msgstr "服务器出现了错误。"
#: exceptions.py:142
msgid "Invalid input."
-msgstr ""
+msgstr "无效的输入。"
#: exceptions.py:161
msgid "Malformed request."
@@ -142,17 +142,17 @@ msgstr "不支持请求中的媒体类型 “{media_type}”。"
#: exceptions.py:223
msgid "Request was throttled."
-msgstr "请求超过了限速。"
+msgstr "请求已被限流。"
#: exceptions.py:224
#, python-brace-format
msgid "Expected available in {wait} second."
-msgstr ""
+msgstr "预计 {wait} 秒后可用。"
#: exceptions.py:225
#, python-brace-format
msgid "Expected available in {wait} seconds."
-msgstr ""
+msgstr "预计 {wait} 秒后可用。"
#: fields.py:316 relations.py:245 relations.py:279 validators.py:90
#: validators.py:183
@@ -165,11 +165,11 @@ msgstr "该字段不能为 null。"
#: fields.py:701
msgid "Must be a valid boolean."
-msgstr ""
+msgstr "必须是有效的布尔值。"
#: fields.py:766
msgid "Not a valid string."
-msgstr ""
+msgstr "不是有效的字符串。"
#: fields.py:767
msgid "This field may not be blank."
@@ -203,7 +203,7 @@ msgstr "请输入合法的“短语“,只能包含字母,数字,下划线
msgid ""
"Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, "
"or hyphens."
-msgstr ""
+msgstr "请输入有效的“slug”,由 Unicode 字母、数字、下划线或连字符组成。"
#: fields.py:854
msgid "Enter a valid URL."
@@ -211,7 +211,7 @@ msgstr "请输入合法的URL。"
#: fields.py:867
msgid "Must be a valid UUID."
-msgstr ""
+msgstr "必须是有效的 UUID。"
#: fields.py:903
msgid "Enter a valid IPv4 or IPv6 address."
@@ -269,11 +269,11 @@ msgstr "期望为日期时间,获得的是日期。"
#: fields.py:1150
#, python-brace-format
msgid "Invalid datetime for the timezone \"{timezone}\"."
-msgstr ""
+msgstr "时区“{timezone}”的时间格式无效。"
#: fields.py:1151
msgid "Datetime value out of range."
-msgstr ""
+msgstr "时间数值超出有效范围。"
#: fields.py:1236
#, python-brace-format
@@ -354,12 +354,12 @@ msgstr "列表不能为空。"
#: fields.py:1605
#, python-brace-format
msgid "Ensure this field has at least {min_length} elements."
-msgstr ""
+msgstr "该字段必须包含至少 {min_length} 个元素。"
#: fields.py:1606
#, python-brace-format
msgid "Ensure this field has no more than {max_length} elements."
-msgstr ""
+msgstr "该字段不能超过 {max_length} 个元素。"
#: fields.py:1682
#, python-brace-format
@@ -368,7 +368,7 @@ msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。"
#: fields.py:1683
msgid "This dictionary may not be empty."
-msgstr ""
+msgstr "该字典不能为空。"
#: fields.py:1755
msgid "Value must be valid JSON."
@@ -380,7 +380,7 @@ msgstr " 搜索"
#: filters.py:50
msgid "A search term."
-msgstr ""
+msgstr "搜索关键词。"
#: filters.py:180 templates/rest_framework/filters/ordering.html:3
msgid "Ordering"
@@ -388,7 +388,7 @@ msgstr "排序"
#: filters.py:181
msgid "Which field to use when ordering the results."
-msgstr ""
+msgstr "用于排序结果的字段。"
#: filters.py:287
msgid "ascending"
@@ -400,11 +400,11 @@ msgstr "倒排序"
#: pagination.py:174
msgid "A page number within the paginated result set."
-msgstr ""
+msgstr "分页结果集中的页码。"
#: pagination.py:179 pagination.py:372 pagination.py:590
msgid "Number of results to return per page."
-msgstr ""
+msgstr "每页返回的结果数量。"
#: pagination.py:189
msgid "Invalid page."
@@ -412,11 +412,11 @@ msgstr "无效页面。"
#: pagination.py:374
msgid "The initial index from which to return the results."
-msgstr ""
+msgstr "返回结果的起始索引位置。"
#: pagination.py:581
msgid "The pagination cursor value."
-msgstr ""
+msgstr "分页游标值"
#: pagination.py:583
msgid "Invalid cursor"
@@ -460,20 +460,20 @@ msgstr "无效值。"
#: schemas/utils.py:32
msgid "unique integer value"
-msgstr ""
+msgstr "唯一整数值"
#: schemas/utils.py:34
msgid "UUID string"
-msgstr ""
+msgstr "UUID 字符串"
#: schemas/utils.py:36
msgid "unique value"
-msgstr ""
+msgstr "唯一值"
#: schemas/utils.py:38
#, python-brace-format
msgid "A {value_type} identifying this {name}."
-msgstr ""
+msgstr "标识此 {name} 的 {value_type}。"
#: serializers.py:337
#, python-brace-format
@@ -483,7 +483,7 @@ msgstr "无效数据。期待为字典类型,得到的是 {datatype} 。"
#: templates/rest_framework/admin.html:116
#: templates/rest_framework/base.html:136
msgid "Extra Actions"
-msgstr ""
+msgstr "扩展操作"
#: templates/rest_framework/admin.html:130
#: templates/rest_framework/base.html:150
@@ -492,27 +492,27 @@ msgstr "过滤器"
#: templates/rest_framework/base.html:37
msgid "navbar"
-msgstr ""
+msgstr "导航栏"
#: templates/rest_framework/base.html:75
msgid "content"
-msgstr ""
+msgstr "内容主体"
#: templates/rest_framework/base.html:78
msgid "request form"
-msgstr ""
+msgstr "请求表单"
#: templates/rest_framework/base.html:157
msgid "main content"
-msgstr ""
+msgstr "主要内容区"
#: templates/rest_framework/base.html:173
msgid "request info"
-msgstr ""
+msgstr "请求信息"
#: templates/rest_framework/base.html:177
msgid "response info"
-msgstr ""
+msgstr "响应信息"
#: templates/rest_framework/horizontal/radio.html:4
#: templates/rest_framework/inline/radio.html:3
@@ -538,7 +538,7 @@ msgstr "字段 {field_names} 必须能构成唯一集合。"
#: validators.py:171
#, python-brace-format
msgid "Surrogate characters are not allowed: U+{code_point:X}."
-msgstr ""
+msgstr "不允许使用代理字符: U+{code_point:X}。"
#: validators.py:243
#, python-brace-format
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index b4bbfa1f5..23012f71f 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -65,7 +65,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
full_media_type = ';'.join(
(renderer.media_type,) +
tuple(
- '{}={}'.format(key, value)
+ f'{key}={value}'
for key, value in media_type_wrapper.params.items()
)
)
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 7c15eca58..768f6cb95 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -225,7 +225,7 @@ class DjangoModelPermissions(BasePermission):
if hasattr(view, 'get_queryset'):
queryset = view.get_queryset()
assert queryset is not None, (
- '{}.get_queryset() returned None'.format(view.__class__.__name__)
+ f'{view.__class__.__name__}.get_queryset() returned None'
)
return queryset
return view.queryset
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index ea73c6657..b81f9ab46 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -171,6 +171,10 @@ class TemplateHTMLRenderer(BaseRenderer):
def get_template_context(self, data, renderer_context):
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:
data['status_code'] = response.status_code
return data
diff --git a/rest_framework/request.py b/rest_framework/request.py
index f30578fa2..1527e435b 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -217,7 +217,8 @@ class Request:
@property
def data(self):
if not _hasattr(self, '_full_data'):
- self._load_data_and_files()
+ with wrap_attributeerrors():
+ self._load_data_and_files()
return self._full_data
@property
@@ -420,20 +421,14 @@ class Request:
_request = self.__getattribute__("_request")
return getattr(_request, attr)
except AttributeError:
- return self.__getattribute__(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.'
- )
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
@property
def POST(self):
# Ensure that request.POST uses our request parsing.
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):
return self._data
return QueryDict('', encoding=self._request._encoding)
@@ -444,16 +439,10 @@ class Request:
# Different from the other two cases, which are not valid property
# names on the WSGIRequest class.
if not _hasattr(self, '_files'):
- self._load_data_and_files()
+ with wrap_attributeerrors():
+ self._load_data_and_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):
# Hack to allow our exception handler to force choice of
# plaintext or html error responses.
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 6e756544c..507ea595f 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -66,7 +66,7 @@ class Response(SimpleTemplateResponse):
content_type = self.content_type
if content_type is None and charset is not None:
- content_type = "{}; charset={}".format(media_type, charset)
+ content_type = f"{media_type}; charset={charset}"
elif content_type is None:
content_type = media_type
self['Content-Type'] = content_type
diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py
index 582aba196..657178304 100644
--- a/rest_framework/schemas/coreapi.py
+++ b/rest_framework/schemas/coreapi.py
@@ -68,7 +68,7 @@ class LinkNode(dict):
current_val = self.methods_counter[preferred_key]
self.methods_counter[preferred_key] += 1
- key = '{}_{}'.format(preferred_key, current_val)
+ key = f'{preferred_key}_{current_val}'
if key not in self:
return key
diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py
index cb880e79d..e027b46a7 100644
--- a/rest_framework/schemas/inspectors.py
+++ b/rest_framework/schemas/inspectors.py
@@ -79,8 +79,9 @@ class ViewInspector:
view = self.view
method_name = getattr(view, 'action', method.lower())
- method_docstring = getattr(view, method_name, None).__doc__
- if method_docstring:
+ method_func = getattr(view, method_name, None)
+ method_docstring = method_func.__doc__
+ if method_func and method_docstring:
# An explicit docstring on the method or action.
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
else:
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index f35106fe5..eb7dc909d 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -11,9 +11,7 @@ from django.core.validators import (
from django.db import models
from django.utils.encoding import force_str
-from rest_framework import (
- RemovedInDRF316Warning, exceptions, renderers, serializers
-)
+from rest_framework import exceptions, renderers, serializers
from rest_framework.compat import inflection, uritemplate
from rest_framework.fields import _UnvalidatedField, empty
from rest_framework.settings import api_settings
@@ -84,7 +82,7 @@ class SchemaGenerator(BaseSchemaGenerator):
continue
if components_schemas[k] == components[k]:
continue
- warnings.warn('Schema component "{}" has been overridden with a different value.'.format(k))
+ warnings.warn(f'Schema component "{k}" has been overridden with a different value.')
components_schemas.update(components)
@@ -646,7 +644,7 @@ class AutoSchema(ViewInspector):
return self.get_serializer(path, method)
def get_reference(self, serializer):
- return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
+ return {'$ref': f'#/components/schemas/{self.get_component_name(serializer)}'}
def get_request_body(self, path, method):
if method not in ('PUT', 'PATCH', 'POST'):
@@ -721,11 +719,3 @@ class AutoSchema(ViewInspector):
path = path[1:]
return [path.split('/')[0].replace('_', '-')]
-
- def _get_reference(self, serializer):
- warnings.warn(
- "Method `_get_reference()` has been renamed to `get_reference()`. "
- "The old name will be removed in DRF v3.16.",
- RemovedInDRF316Warning, stacklevel=2
- )
- return self.get_reference(serializer)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index b1b7b6477..0b87aa8fc 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -26,7 +26,9 @@ from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
-from rest_framework.compat import postgres_fields
+from rest_framework.compat import (
+ get_referenced_base_fields_from_q, postgres_fields
+)
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import get_error_detail
from rest_framework.settings import api_settings
@@ -1425,20 +1427,20 @@ class ModelSerializer(Serializer):
def get_unique_together_constraints(self, model):
"""
- Returns iterator of (fields, queryset), each entry describes an unique together
- constraint on `fields` in `queryset`.
+ Returns iterator of (fields, queryset, condition_fields, condition),
+ each entry describes an unique together constraint on `fields` in `queryset`
+ with respect of constraint's `condition`.
"""
for parent_class in [model] + list(model._meta.parents):
for unique_together in parent_class._meta.unique_together:
- yield unique_together, model._default_manager
+ yield unique_together, model._default_manager, [], None
for constraint in parent_class._meta.constraints:
if isinstance(constraint, models.UniqueConstraint) and len(constraint.fields) > 1:
- yield (
- constraint.fields,
- model._default_manager
- if constraint.condition is None
- else model._default_manager.filter(constraint.condition)
- )
+ if constraint.condition is None:
+ condition_fields = []
+ else:
+ condition_fields = list(get_referenced_base_fields_from_q(constraint.condition))
+ yield (constraint.fields, model._default_manager, condition_fields, constraint.condition)
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
"""
@@ -1470,9 +1472,10 @@ class ModelSerializer(Serializer):
# Include each of the `unique_together` and `UniqueConstraint` field names,
# so long as all the field names are included on the serializer.
- for unique_together_list, queryset in self.get_unique_together_constraints(model):
- if set(field_names).issuperset(unique_together_list):
- unique_constraint_names |= set(unique_together_list)
+ for unique_together_list, queryset, condition_fields, condition in self.get_unique_together_constraints(model):
+ unique_together_list_and_condition_fields = set(unique_together_list) | set(condition_fields)
+ if set(field_names).issuperset(unique_together_list_and_condition_fields):
+ unique_constraint_names |= unique_together_list_and_condition_fields
# Now we have all the field names that have uniqueness constraints
# applied, we can add the extra 'required=...' or 'default=...'
@@ -1490,6 +1493,8 @@ class ModelSerializer(Serializer):
default = timezone.now
elif unique_constraint_field.has_default():
default = unique_constraint_field.default
+ elif unique_constraint_field.null:
+ default = None
else:
default = empty
@@ -1592,12 +1597,13 @@ class ModelSerializer(Serializer):
# Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes.
validators = []
- for unique_together, queryset in self.get_unique_together_constraints(self.Meta.model):
+ for unique_together, queryset, condition_fields, condition in self.get_unique_together_constraints(self.Meta.model):
# Skip if serializer does not map to all unique together sources
- if not set(source_map).issuperset(unique_together):
+ unique_together_and_condition_fields = set(unique_together) | set(condition_fields)
+ if not set(source_map).issuperset(unique_together_and_condition_fields):
continue
- for source in unique_together:
+ for source in unique_together_and_condition_fields:
assert len(source_map[source]) == 1, (
"Unable to create `UniqueTogetherValidator` for "
"`{model}.{field}` as `{serializer}` has multiple "
@@ -1616,7 +1622,9 @@ class ModelSerializer(Serializer):
field_names = tuple(source_map[f][0] for f in unique_together)
validator = UniqueTogetherValidator(
queryset=queryset,
- fields=field_names
+ fields=field_names,
+ condition_fields=tuple(source_map[f][0] for f in condition_fields),
+ condition=condition,
)
validators.append(validator)
return validators
diff --git a/rest_framework/test.py b/rest_framework/test.py
index e939adcd7..c273724b9 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -150,15 +150,19 @@ class APIRequestFactory(DjangoRequestFactory):
"""
Encode the data returning a two tuple of (bytes, content_type)
"""
-
if data is None:
- return ('', content_type)
+ return (b'', content_type)
assert format is None or content_type is None, (
'You may not set both `format` and `content_type`.'
)
if content_type:
+ try:
+ data = self._encode_json(data, content_type)
+ except AttributeError:
+ pass
+
# Content type specified explicitly, treat data as a raw bytestring
ret = force_bytes(data, settings.DEFAULT_CHARSET)
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index bed5708eb..47a8194cf 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,10 +1,11 @@
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 rest_framework.settings import api_settings
-def _get_format_path_converter(suffix_kwarg, allowed):
+def _get_format_path_converter(allowed):
if allowed:
if len(allowed) == 1:
allowed_pattern = allowed[0]
@@ -23,11 +24,14 @@ def _get_format_path_converter(suffix_kwarg, allowed):
def to_url(self, value):
return '.' + value + '/'
+ return FormatSuffixConverter
+
+
+def _generate_converter_name(allowed):
converter_name = 'drf_format_suffix'
if allowed:
converter_name += '_' + '_'.join(allowed)
-
- return converter_name, FormatSuffixConverter
+ return converter_name
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:
suffix_pattern = r'\.(?P<%s>[a-z0-9]+)/?$' % suffix_kwarg
- converter_name, suffix_converter = _get_format_path_converter(suffix_kwarg, allowed)
- register_converter(suffix_converter, converter_name)
+ 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)
suffix_route = '<%s:%s>' % (converter_name, suffix_kwarg)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index fc63f96fe..15c4b9105 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -66,7 +66,7 @@ def get_unique_validators(field_name, model_field):
"""
Returns a list of UniqueValidators that should be applied to the field.
"""
- field_set = set([field_name])
+ field_set = {field_name}
conditions = {
c.condition
for c in model_field.model._meta.constraints
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
index 0e59aa66a..e6bd84f30 100644
--- a/rest_framework/utils/serializer_helpers.py
+++ b/rest_framework/utils/serializer_helpers.py
@@ -1,5 +1,4 @@
import contextlib
-import sys
from collections.abc import Mapping, MutableMapping
from django.utils.encoding import force_str
@@ -29,21 +28,20 @@ class ReturnDict(dict):
# but preserve the raw data.
return (dict, (dict(self),))
- if sys.version_info >= (3, 9):
- # These are basically copied from OrderedDict, with `serializer` added.
- def __or__(self, other):
- if not isinstance(other, dict):
- return NotImplemented
- new = self.__class__(self, serializer=self.serializer)
- new.update(other)
- return new
+ # These are basically copied from OrderedDict, with `serializer` added.
+ def __or__(self, other):
+ if not isinstance(other, dict):
+ return NotImplemented
+ new = self.__class__(self, serializer=self.serializer)
+ new.update(other)
+ return new
- def __ror__(self, other):
- if not isinstance(other, dict):
- return NotImplemented
- new = self.__class__(other, serializer=self.serializer)
- new.update(self)
- return new
+ def __ror__(self, other):
+ if not isinstance(other, dict):
+ return NotImplemented
+ new = self.__class__(other, serializer=self.serializer)
+ new.update(self)
+ return new
class ReturnList(list):
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index 3f09c15cd..4c444cf01 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -6,7 +6,9 @@ This gives us better separation of concerns, allows us to use single-step
object creation, and makes it possible to switch between using the implicit
`ModelSerializer` class and an equivalent explicit `Serializer` class.
"""
+from django.core.exceptions import FieldError
from django.db import DataError
+from django.db.models import Exists
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
@@ -23,6 +25,17 @@ def qs_exists(queryset):
return False
+def qs_exists_with_condition(queryset, condition, against):
+ if condition is None:
+ return qs_exists(queryset)
+ try:
+ # use the same query as UniqueConstraint.validate
+ # https://github.com/django/django/blob/7ba2a0db20c37a5b1500434ca4ed48022311c171/django/db/models/constraints.py#L672
+ return (condition & Exists(queryset.filter(condition))).check(against)
+ except (TypeError, ValueError, DataError, FieldError):
+ return False
+
+
def qs_filter(queryset, **kwargs):
try:
return queryset.filter(**kwargs)
@@ -99,10 +112,12 @@ class UniqueTogetherValidator:
missing_message = _('This field is required.')
requires_context = True
- def __init__(self, queryset, fields, message=None):
+ def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None):
self.queryset = queryset
self.fields = fields
self.message = message or self.message
+ self.condition_fields = [] if condition_fields is None else condition_fields
+ self.condition = condition
def enforce_required_fields(self, attrs, serializer):
"""
@@ -114,7 +129,7 @@ class UniqueTogetherValidator:
missing_items = {
field_name: self.missing_message
- for field_name in self.fields
+ for field_name in (*self.fields, *self.condition_fields)
if serializer.fields[field_name].source not in attrs
}
if missing_items:
@@ -159,29 +174,34 @@ class UniqueTogetherValidator:
queryset = self.filter_queryset(attrs, queryset, serializer)
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
if serializer.instance is None:
- checked_values = [
- value for field, value in attrs.items() if field in self.fields
- ]
+ checked_values = [attrs[field_name] for field_name in checked_names]
else:
# Ignore validation if all field values are unchanged
checked_values = [
- value
- for field, value in attrs.items()
- if field in self.fields and value != getattr(serializer.instance, field)
+ attrs[field_name]
+ for field_name in checked_names
+ if attrs[field_name] != getattr(serializer.instance, field_name)
]
- if checked_values and None not in checked_values and qs_exists(queryset):
+ condition_sources = (serializer.fields[field_name].source for field_name in self.condition_fields)
+ condition_kwargs = {source: attrs[source] for source in condition_sources}
+ if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs):
field_names = ', '.join(self.fields)
message = self.message.format(field_names=field_names)
raise ValidationError(message, code='unique')
def __repr__(self):
- return '<%s(queryset=%s, fields=%s)>' % (
+ return '<{}({})>'.format(
self.__class__.__name__,
- smart_repr(self.queryset),
- smart_repr(self.fields)
+ ', '.join(
+ f'{attr}={smart_repr(getattr(self, attr))}'
+ for attr in ('queryset', 'fields', 'condition')
+ if getattr(self, attr) is not None)
)
def __eq__(self, other):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 411c1ee38..327ebe903 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -1,6 +1,7 @@
"""
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.core.exceptions import PermissionDenied
from django.db import connections, models
@@ -139,6 +140,11 @@ class APIView(View):
view.cls = cls
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,
# all other authentication is CSRF exempt.
return csrf_exempt(view)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index 2eba17b4a..a9c90a8d9 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -19,6 +19,7 @@ automatically.
from functools import update_wrapper
from inspect import getmembers
+from django import VERSION as DJANGO_VERSION
from django.urls import NoReverseMatch
from django.utils.decorators import classonlymethod
from django.views.decorators.csrf import csrf_exempt
@@ -136,6 +137,12 @@ class ViewSetMixin:
view.cls = cls
view.initkwargs = initkwargs
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)
def initialize_request(self, request, *args, **kwargs):
diff --git a/setup.py b/setup.py
index d2cfe877e..e5e78c2c7 100755
--- a/setup.py
+++ b/setup.py
@@ -1,14 +1,12 @@
-#!/usr/bin/env python3
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)
+REQUIRED_PYTHON = (3, 9)
# This check and everything above must remain compatible with Python 2.7.
if CURRENT_PYTHON < REQUIRED_PYTHON:
@@ -37,7 +35,7 @@ an older version of Django REST Framework:
def read(f):
- with open(f, 'r', encoding='utf-8') as file:
+ with open(f, encoding='utf-8') as file:
return file.read()
@@ -83,8 +81,8 @@ setup(
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",
+ install_requires=["django>=4.2"],
+ python_requires=">=3.9",
zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -92,16 +90,18 @@ setup(
'Framework :: Django',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
+ 'Framework :: Django :: 5.1',
+ 'Framework :: Django :: 5.2',
'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',
],
diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py
index 98fd46f9f..a97b02fe1 100644
--- a/tests/schemas/test_coreapi.py
+++ b/tests/schemas/test_coreapi.py
@@ -1177,7 +1177,7 @@ class NamingCollisionViewSet(GenericViewSet):
"""
Example via: https://stackoverflow.com/questions/43778668/django-rest-framwork-occured-typeerror-link-object-does-not-support-item-ass/
"""
- permision_class = ()
+ permission_classes = ()
@action(detail=False)
def detail(self, request):
@@ -1234,7 +1234,7 @@ class TestURLNamingCollisions(TestCase):
for method, suffix in zip(methods, suffixes):
if suffix is not None:
- key = '{}_{}'.format(method, suffix)
+ key = f'{method}_{suffix}'
else:
key = method
assert loc[key].url == url
diff --git a/tests/schemas/test_managementcommand.py b/tests/schemas/test_managementcommand.py
index c0713f43c..fa1b75fbf 100644
--- a/tests/schemas/test_managementcommand.py
+++ b/tests/schemas/test_managementcommand.py
@@ -70,7 +70,7 @@ class GenerateSchemaTests(TestCase):
def test_accepts_custom_schema_generator(self):
call_command('generateschema',
- '--generator_class={}.{}'.format(__name__, CustomSchemaGenerator.__name__),
+ f'--generator_class={__name__}.{CustomSchemaGenerator.__name__}',
stdout=self.out)
out_json = yaml.safe_load(self.out.getvalue())
assert out_json == CustomSchemaGenerator.SCHEMA
@@ -78,7 +78,7 @@ class GenerateSchemaTests(TestCase):
def test_writes_schema_to_file_on_parameter(self):
fd, path = tempfile.mkstemp()
try:
- call_command('generateschema', '--file={}'.format(path), stdout=self.out)
+ call_command('generateschema', f'--file={path}', stdout=self.out)
# nothing on stdout
assert not self.out.getvalue()
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 9516bfec9..67be5cfbd 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -43,12 +43,12 @@ class ExceptionTestCase(TestCase):
exception = Throttled(wait=2)
assert exception.get_full_details() == {
- 'message': 'Request was throttled. Expected available in {} seconds.'.format(2),
+ 'message': f'Request was throttled. Expected available in {2} seconds.',
'code': 'throttled'}
exception = Throttled(wait=2, detail='Slow down!')
assert exception.get_full_details() == {
- 'message': 'Slow down! Expected available in {} seconds.'.format(2),
+ 'message': f'Slow down! Expected available in {2} seconds.',
'code': 'throttled'}
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 430681763..d574b07eb 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -2,12 +2,12 @@ import datetime
import math
import os
import re
-import sys
import uuid
import warnings
from decimal import ROUND_DOWN, ROUND_UP, Decimal
from enum import auto
from unittest.mock import patch
+from zoneinfo import ZoneInfo
import pytest
@@ -30,11 +30,6 @@ from rest_framework.fields import (
)
from tests.models import UUIDForeignKeyTarget
-if sys.version_info >= (3, 9):
- from zoneinfo import ZoneInfo
-else:
- from backports.zoneinfo import ZoneInfo
-
utc = datetime.timezone.utc
# Tests for helper functions.
@@ -641,10 +636,6 @@ class Test5087Regression:
class TestTyping(TestCase):
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_field_is_subscriptable(self):
assert serializers.Field is serializers.Field["foo"]
@@ -671,7 +662,7 @@ class FieldValues:
"""
for input_value, expected_output in get_items(self.valid_inputs):
assert self.field.run_validation(input_value) == expected_output, \
- 'input value: {}'.format(repr(input_value))
+ f'input value: {repr(input_value)}'
def test_invalid_inputs(self, *args):
"""
@@ -681,12 +672,12 @@ class FieldValues:
with pytest.raises(serializers.ValidationError) as exc_info:
self.field.run_validation(input_value)
assert exc_info.value.detail == expected_failure, \
- 'input value: {}'.format(repr(input_value))
+ f'input value: {repr(input_value)}'
def test_outputs(self, *args):
for output_value, expected_output in get_items(self.outputs):
assert self.field.to_representation(output_value) == expected_output, \
- 'output value: {}'.format(repr(output_value))
+ f'output value: {repr(output_value)}'
# Boolean types...
@@ -1245,13 +1236,13 @@ class TestMinMaxDecimalField(FieldValues):
'20.0': Decimal('20.0'),
}
invalid_inputs = {
- '9.9': ['Ensure this value is greater than or equal to 10.'],
- '20.1': ['Ensure this value is less than or equal to 20.'],
+ '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.0.'],
}
outputs = {}
field = serializers.DecimalField(
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):
@@ -1260,14 +1251,14 @@ class TestMinMaxDecimalField(FieldValues):
serializers.DecimalField(
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 all(issubclass(i.category, UserWarning) for i in w)
- assert 'max_value should be a Decimal instance' in str(w[0].message)
- assert 'min_value should be a Decimal instance' in str(w[1].message)
+ 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):
@@ -1431,7 +1422,7 @@ class TestDateField(FieldValues):
outputs = {
datetime.date(2001, 1, 1): '2001-01-01',
'2001-01-01': '2001-01-01',
- str('2016-01-10'): '2016-01-10',
+ '2016-01-10': '2016-01-10',
None: None,
'': None,
}
@@ -1498,7 +1489,7 @@ class TestDateTimeField(FieldValues):
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00Z',
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): '2001-01-01T13:00:00Z',
'2001-01-01T00:00:00': '2001-01-01T00:00:00',
- str('2016-01-10T00:00:00'): '2016-01-10T00:00:00',
+ '2016-01-10T00:00:00': '2016-01-10T00:00:00',
None: None,
'': None,
}
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 6db0c3deb..9fc8ad1a9 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -19,7 +19,7 @@ factory = APIRequestFactory()
class SearchSplitTests(SimpleTestCase):
- def test_keep_quoted_togheter_regardless_of_commas(self):
+ def test_keep_quoted_together_regardless_of_commas(self):
assert ['hello, world'] == list(filters.search_smart_split('"hello, world"'))
def test_strips_commas_around_quoted(self):
@@ -516,7 +516,7 @@ class OrderingFilterModel(models.Model):
class OrderingFilterRelatedModel(models.Model):
- related_object = models.ForeignKey(OrderingFilterModel, related_name="relateds", on_delete=models.CASCADE)
+ related_object = models.ForeignKey(OrderingFilterModel, related_name="related", on_delete=models.CASCADE)
index = models.SmallIntegerField(help_text="A non-related field to test with", default=0)
@@ -725,9 +725,9 @@ class OrderingFilterTests(TestCase):
def test_ordering_by_aggregate_field(self):
# create some related models to aggregate order by
num_objs = [2, 5, 3]
- for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
- num_objs):
- for _ in range(num_relateds):
+ for obj, num_related in zip(OrderingFilterModel.objects.all(),
+ num_objs):
+ for _ in range(num_related):
new_related = OrderingFilterRelatedModel(
related_object=obj
)
@@ -739,10 +739,10 @@ class OrderingFilterTests(TestCase):
ordering = 'title'
ordering_fields = '__all__'
queryset = OrderingFilterModel.objects.all().annotate(
- models.Count("relateds"))
+ models.Count("related"))
view = OrderingListView.as_view()
- request = factory.get('/', {'ordering': 'relateds__count'})
+ request = factory.get('/', {'ordering': 'related__count'})
response = view(request)
assert response.data == [
{'id': 1, 'title': 'zyx', 'text': 'abc'},
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 9990389c9..25b96fcbb 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,5 +1,3 @@
-import sys
-
import pytest
from django.db import models
from django.http import Http404
@@ -291,7 +289,7 @@ class TestInstanceView(TestCase):
"""
data = {'text': 'foo'}
filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
- request = factory.put('/{}'.format(filtered_out_pk), data, format='json')
+ request = factory.put(f'/{filtered_out_pk}', data, format='json')
response = self.view(request, pk=filtered_out_pk).render()
assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -703,23 +701,11 @@ class TestSerializer(TestCase):
class TestTyping(TestCase):
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_genericview_is_subscriptable(self):
assert generics.GenericAPIView is generics.GenericAPIView["foo"]
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_listview_is_subscriptable(self):
assert generics.ListAPIView is generics.ListAPIView["foo"]
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_instanceview_is_subscriptable(self):
assert generics.RetrieveAPIView is generics.RetrieveAPIView["foo"]
diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py
index fa0f4efc6..aa0cfb19c 100644
--- a/tests/test_htmlrenderer.py
+++ b/tests/test_htmlrenderer.py
@@ -8,6 +8,7 @@ from django.urls import path
from rest_framework import status
from rest_framework.decorators import api_view, renderer_classes
+from rest_framework.exceptions import ValidationError
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
@@ -34,10 +35,17 @@ def not_found(request):
raise Http404()
+@api_view(('GET',))
+@renderer_classes((TemplateHTMLRenderer,))
+def validation_error(request):
+ raise ValidationError('error')
+
+
urlpatterns = [
path('', example),
path('permission_denied', permission_denied),
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-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
# of TemplateHTMLRenderer
def test_get_template_names_returns_own_template_name(self):
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 6b2c91db7..11d4bc01e 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -1,14 +1,21 @@
+import unittest
+
+import django
from django.contrib.auth.models import User
from django.http import HttpRequest
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.authtoken.models import Token
+from rest_framework.decorators import action, api_view
from rest_framework.request import is_form_media_type
from rest_framework.response import Response
+from rest_framework.routers import SimpleRouter
from rest_framework.test import APITestCase
from rest_framework.views import APIView
+from rest_framework.viewsets import GenericViewSet
class PostView(APIView):
@@ -16,9 +23,39 @@ class PostView(APIView):
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 = [
path('auth', APIView.as_view(authentication_classes=(TokenAuthentication,))),
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')
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
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index ae1a2b0fa..eac51ae70 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -9,7 +9,6 @@ import datetime
import decimal
import json # noqa
import re
-import sys
import tempfile
import pytest
@@ -397,10 +396,6 @@ class TestDurationFieldMapping(TestCase):
fields = '__all__'
expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- duration_field = DurationField(max_value=datetime.timedelta(3), min_value=datetime.timedelta(1))
- """) if sys.version_info < (3, 7) else dedent("""
TestSerializer():
id = IntegerField(label='ID', read_only=True)
duration_field = DurationField(max_value=datetime.timedelta(days=3), min_value=datetime.timedelta(days=1))
@@ -797,7 +792,7 @@ class TestIntegration(TestCase):
)
self.instance.many_to_many.set(self.many_to_many_targets)
- def test_pk_retrival(self):
+ def test_pk_retrieval(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
diff --git a/tests/test_negotiation.py b/tests/test_negotiation.py
index 089a86c62..1fe659095 100644
--- a/tests/test_negotiation.py
+++ b/tests/test_negotiation.py
@@ -61,8 +61,8 @@ class TestAcceptedMediaType(TestCase):
def test_match_is_false_if_main_types_not_match(self):
mediatype = _MediaType('test_1')
- anoter_mediatype = _MediaType('test_2')
- assert mediatype.match(anoter_mediatype) is False
+ another_mediatype = _MediaType('test_2')
+ assert mediatype.match(another_mediatype) is False
def test_mediatype_match_is_false_if_keys_not_match(self):
mediatype = _MediaType(';test_param=foo')
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 02d443ade..d8f66e95b 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -513,7 +513,7 @@ class TestLimitOffset:
]
}
- def test_erronous_offset(self):
+ def test_erroneous_offset(self):
request = Request(factory.get('/', {'limit': 5, 'offset': 1000}))
queryset = self.paginate_queryset(request)
self.get_paginated_content(queryset)
@@ -536,7 +536,7 @@ class TestLimitOffset:
content = self.get_paginated_content(queryset)
next_limit = self.pagination.default_limit
next_offset = self.pagination.default_limit
- next_url = 'http://testserver/?limit={}&offset={}'.format(next_limit, next_offset)
+ next_url = f'http://testserver/?limit={next_limit}&offset={next_offset}'
assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
assert content.get('next') == next_url
@@ -549,7 +549,7 @@ class TestLimitOffset:
content = self.get_paginated_content(queryset)
next_limit = self.pagination.default_limit
next_offset = self.pagination.default_limit
- next_url = 'http://testserver/?limit={}&offset={}'.format(next_limit, next_offset)
+ next_url = f'http://testserver/?limit={next_limit}&offset={next_offset}'
assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
assert content.get('next') == next_url
@@ -565,9 +565,9 @@ class TestLimitOffset:
max_limit = self.pagination.max_limit
next_offset = offset + max_limit
prev_offset = offset - max_limit
- base_url = 'http://testserver/?limit={}'.format(max_limit)
- next_url = base_url + '&offset={}'.format(next_offset)
- prev_url = base_url + '&offset={}'.format(prev_offset)
+ base_url = f'http://testserver/?limit={max_limit}'
+ next_url = base_url + f'&offset={next_offset}'
+ prev_url = base_url + f'&offset={prev_offset}'
assert queryset == list(range(51, 66))
assert content.get('next') == next_url
assert content.get('previous') == prev_url
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index 39b7ed662..93fe7b941 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -353,7 +353,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
'delete': f('delete', model_name)
}
for perm in perms.values():
- perm = '{}.{}'.format(app_label, perm)
+ perm = f'{app_label}.{perm}'
assign_perm(perm, everyone)
everyone.user_set.add(*users.values())
@@ -624,7 +624,7 @@ class PermissionsCompositionTests(TestCase):
)
assert composed_perm().has_permission(request, None) is True
- def test_or_lazyness(self):
+ def test_or_laziness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
@@ -644,7 +644,7 @@ class PermissionsCompositionTests(TestCase):
assert mock_deny.call_count == 1
assert mock_allow.call_count == 1
- def test_object_or_lazyness(self):
+ def test_object_or_laziness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
@@ -664,7 +664,7 @@ class PermissionsCompositionTests(TestCase):
assert mock_deny.call_count == 0
assert mock_allow.call_count == 1
- def test_and_lazyness(self):
+ def test_and_laziness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
@@ -684,7 +684,7 @@ class PermissionsCompositionTests(TestCase):
assert mock_deny.call_count == 1
mock_allow.assert_not_called()
- def test_object_and_lazyness(self):
+ def test_object_and_laziness(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()
@@ -718,7 +718,7 @@ class PermissionsCompositionTests(TestCase):
assert hasperm is False
def test_operand_holder_is_hashable(self):
- assert hash((permissions.IsAuthenticated & permissions.IsAdminUser))
+ assert hash(permissions.IsAuthenticated & permissions.IsAdminUser)
def test_operand_holder_hash_same_for_same_operands_and_operator(self):
first_operand_holder = (
diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py
index b07087c97..12ecbf2e6 100644
--- a/tests/test_prefetch_related.py
+++ b/tests/test_prefetch_related.py
@@ -56,3 +56,17 @@ class TestPrefetchRelatedUpdates(TestCase):
'email': 'tom@example.com'
}
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'
diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py
index 7a4878a2b..14513f2bb 100644
--- a/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -586,7 +586,7 @@ class OneToOnePrimaryKeyTests(TestCase):
source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk})
# Then: The source is valid with the serializer
if not source.is_valid():
- self.fail("Expected OneToOnePKTargetSerializer to be valid but had errors: {}".format(source.errors))
+ self.fail(f"Expected OneToOnePKTargetSerializer to be valid but had errors: {source.errors}")
# Then: Saving the serializer creates a new object
new_source = source.save()
# Then: The new object has the same pk as the target object
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index d04ff300f..1b396575d 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -408,7 +408,7 @@ class UnicodeJSONRendererTests(TestCase):
obj = {'should_escape': '\u2028\u2029'}
renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"should_escape":"\\u2028\\u2029"}'.encode())
+ self.assertEqual(content, b'{"should_escape":"\\u2028\\u2029"}')
class AsciiJSONRendererTests(TestCase):
@@ -421,7 +421,7 @@ class AsciiJSONRendererTests(TestCase):
obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = AsciiJSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode())
+ self.assertEqual(content, b'{"countries":["United Kingdom","France","Espa\\u00f1a"]}')
# Tests for caching issue, #346
diff --git a/tests/test_request.py b/tests/test_request.py
index e37aa7dda..fe3efd96b 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -3,7 +3,6 @@ Tests for content parsing, and form-overloaded content parsing.
"""
import copy
import os.path
-import sys
import tempfile
import pytest
@@ -126,6 +125,25 @@ class TestContentParsing(TestCase):
request.parsers = (PlainTextParser(), )
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):
authentication_classes = (SessionAuthentication,)
@@ -356,9 +374,5 @@ class TestDeepcopy(TestCase):
class TestTyping(TestCase):
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_request_is_subscriptable(self):
assert Request is Request["foo"]
diff --git a/tests/test_response.py b/tests/test_response.py
index 00b5691b0..83f8a6717 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,6 +1,3 @@
-import sys
-
-import pytest
from django.test import TestCase, override_settings
from django.urls import include, path, re_path
@@ -270,7 +267,7 @@ class Issue807Tests(TestCase):
"""
headers = {"HTTP_ACCEPT": RendererC.media_type}
resp = self.client.get('/', **headers)
- expected = "{}; charset={}".format(RendererC.media_type, RendererC.charset)
+ expected = f"{RendererC.media_type}; charset={RendererC.charset}"
self.assertEqual(expected, resp['Content-Type'])
def test_content_type_set_explicitly_on_response(self):
@@ -289,9 +286,5 @@ class Issue807Tests(TestCase):
class TestTyping(TestCase):
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_response_is_subscriptable(self):
assert Response is Response["foo"]
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 887f601d5..91a6189d5 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -447,9 +447,9 @@ class TestDynamicListAndDetailRouter(TestCase):
url_path = endpoint.url_path
if method_name.startswith('list_'):
- assert route.url == '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path)
+ assert route.url == f'^{{prefix}}/{url_path}{{trailing_slash}}$'
else:
- assert route.url == '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path)
+ assert route.url == f'^{{prefix}}/{{lookup}}/{url_path}{{trailing_slash}}$'
# check method to function mapping
if method_name.endswith('_post'):
method_map = 'post'
@@ -488,14 +488,14 @@ class TestRegexUrlPath(URLPatternsTestCase, TestCase):
def test_regex_url_path_list(self):
kwarg = '1234'
- response = self.client.get('/regex/list/{}/'.format(kwarg))
+ response = self.client.get(f'/regex/list/{kwarg}/')
assert response.status_code == 200
assert json.loads(response.content.decode()) == {'kwarg': kwarg}
def test_regex_url_path_detail(self):
pk = '1'
kwarg = '1234'
- response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg))
+ response = self.client.get(f'/regex/{pk}/detail/{kwarg}/')
assert response.status_code == 200
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
@@ -557,14 +557,14 @@ class TestUrlPath(URLPatternsTestCase, TestCase):
def test_list_extra_action(self):
kwarg = 1234
- response = self.client.get('/path/list/{}/'.format(kwarg))
+ response = self.client.get(f'/path/list/{kwarg}/')
assert response.status_code == 200
assert json.loads(response.content.decode()) == {'kwarg': kwarg}
def test_detail_extra_action(self):
pk = '1'
kwarg = 1234
- response = self.client.get('/path/{}/detail/{}/'.format(pk, kwarg))
+ response = self.client.get(f'/path/{pk}/detail/{kwarg}/')
assert response.status_code == 200
assert json.loads(response.content.decode()) == {'pk': pk, 'kwarg': kwarg}
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 10fa8afb9..cefa2ee38 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,7 +1,6 @@
import inspect
import pickle
import re
-import sys
from collections import ChainMap
from collections.abc import Mapping
@@ -205,10 +204,6 @@ class TestSerializer:
exceptions.ErrorDetail(string='Raised error', code='invalid')
]}
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_serializer_is_subscriptable(self):
assert serializers.Serializer is serializers.Serializer["foo"]
@@ -743,10 +738,6 @@ class TestDeclaredFieldInheritance:
class Test8301Regression:
- @pytest.mark.skipif(
- sys.version_info < (3, 9),
- reason="dictionary union operator requires Python 3.9 or higher",
- )
def test_ReturnDict_merging(self):
# Serializer.data returns ReturnDict, this is essentially a test for that.
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
index 4070de7a5..42ebf4771 100644
--- a/tests/test_serializer_lists.py
+++ b/tests/test_serializer_lists.py
@@ -1,5 +1,3 @@
-import sys
-
import pytest
from django.http import QueryDict
from django.utils.datastructures import MultiValueDict
@@ -60,10 +58,6 @@ class TestListSerializer:
assert serializer.is_valid()
assert serializer.validated_data == expected_output
- @pytest.mark.skipif(
- sys.version_info < (3, 7),
- reason="subscriptable classes requires Python 3.7 or higher",
- )
def test_list_serializer_is_subscriptable(self):
assert serializers.ListSerializer is serializers.ListSerializer["foo"]
diff --git a/tests/test_testing.py b/tests/test_testing.py
index a7e00ab63..26a6e8ffb 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -8,9 +8,11 @@ from django.shortcuts import redirect
from django.test import TestCase, override_settings
from django.urls import path
-from rest_framework import fields, serializers
+from rest_framework import fields, parsers, renderers, serializers, status
from rest_framework.authtoken.models import Token
-from rest_framework.decorators import api_view
+from rest_framework.decorators import (
+ api_view, parser_classes, renderer_classes
+)
from rest_framework.response import Response
from rest_framework.test import (
APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate
@@ -50,6 +52,18 @@ class BasicSerializer(serializers.Serializer):
flag = fields.BooleanField(default=lambda: True)
+@api_view(['POST'])
+@parser_classes((parsers.JSONParser,))
+def post_json_view(request):
+ return Response(request.data)
+
+
+@api_view(['DELETE'])
+@renderer_classes((renderers.JSONRenderer, ))
+def delete_json_view(request):
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
@api_view(['POST'])
def post_view(request):
serializer = BasicSerializer(data=request.data)
@@ -62,7 +76,9 @@ urlpatterns = [
path('session-view/', session_view),
path('redirect-view/', redirect_view),
path('redirect-view//', redirect_307_308_view),
- path('post-view/', post_view)
+ path('post-json-view/', post_json_view),
+ path('delete-json-view/', delete_json_view),
+ path('post-view/', post_view),
]
@@ -236,6 +252,22 @@ class TestAPITestClient(TestCase):
assert response.status_code == 200
assert response.data == {"flag": True}
+ def test_post_encodes_data_based_on_json_content_type(self):
+ data = {'data': True}
+ response = self.client.post(
+ '/post-json-view/',
+ data=data,
+ content_type='application/json'
+ )
+
+ assert response.status_code == 200
+ assert response.data == data
+
+ def test_delete_based_on_format(self):
+ response = self.client.delete('/delete-json-view/', format='json')
+ assert response.status_code == status.HTTP_204_NO_CONTENT
+ assert response.data is None
+
class TestAPIRequestFactory(TestCase):
def test_csrf_exempt_by_default(self):
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 6e00b48c2..a2cdd1dcb 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -148,14 +148,14 @@ class TestMaxValueValidatorValidation(TestCase):
def test_max_value_validation_success(self):
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
- request = factory.patch('/{}'.format(obj.pk), {'number_value': 98}, format='json')
+ request = factory.patch(f'/{obj.pk}', {'number_value': 98}, format='json')
view = UpdateMaxValueValidationModel().as_view()
response = view(request, pk=obj.pk).render()
assert response.status_code == status.HTTP_200_OK
def test_max_value_validation_fail(self):
obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
- request = factory.patch('/{}'.format(obj.pk), {'number_value': 101}, format='json')
+ request = factory.patch(f'/{obj.pk}', {'number_value': 101}, format='json')
view = UpdateMaxValueValidationModel().as_view()
response = view(request, pk=obj.pk).render()
assert response.content == b'{"number_value":["Ensure this value is less than or equal to 100."]}'
diff --git a/tests/test_validators.py b/tests/test_validators.py
index c38dc1134..d19734d98 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -406,7 +406,7 @@ class TestUniquenessTogetherValidation(TestCase):
"with a `UniqueTogetherValidator` using the desired field names.")
assert str(excinfo.value) == expected
- def test_allow_explict_override(self):
+ def test_allow_explicit_override(self):
"""
Ensure validators can be explicitly removed..
"""
@@ -441,6 +441,14 @@ class TestUniquenessTogetherValidation(TestCase):
serializer = NullUniquenessTogetherSerializer(data=data)
assert serializer.is_valid()
+ def test_ignore_validation_for_missing_nullable_fields(self):
+ data = {
+ 'date': datetime.date(2000, 1, 1),
+ 'race_name': 'Paris Marathon',
+ }
+ serializer = NullUniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid(), serializer.errors
+
def test_do_not_ignore_validation_for_null_fields(self):
# None values that are not on fields part of the uniqueness constraint
# do not cause the instance to skip validation.
@@ -469,6 +477,28 @@ class TestUniquenessTogetherValidation(TestCase):
assert serializer.is_valid()
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):
"""
filter_queryset should add value from existing instance attribute
@@ -491,7 +521,7 @@ class UniqueConstraintModel(models.Model):
race_name = models.CharField(max_length=100)
position = models.IntegerField()
global_id = models.IntegerField()
- fancy_conditions = models.IntegerField(null=True)
+ fancy_conditions = models.IntegerField()
class Meta:
constraints = [
@@ -513,7 +543,24 @@ class UniqueConstraintModel(models.Model):
name="unique_constraint_model_together_uniq",
fields=('race_name', 'position'),
condition=models.Q(race_name='example'),
- )
+ ),
+ models.UniqueConstraint(
+ name='unique_constraint_model_together_uniq2',
+ fields=('race_name', 'position'),
+ condition=models.Q(fancy_conditions__gte=10),
+ ),
+ ]
+
+
+class UniqueConstraintNullableModel(models.Model):
+ title = models.CharField(max_length=100)
+ age = models.IntegerField(null=True)
+ tag = models.CharField(max_length=100, null=True)
+
+ class Meta:
+ constraints = [
+ # Unique constraint on 2 nullable fields
+ models.UniqueConstraint(name='unique_constraint', fields=('age', 'tag'))
]
@@ -523,22 +570,31 @@ class UniqueConstraintSerializer(serializers.ModelSerializer):
fields = '__all__'
+class UniqueConstraintNullableSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniqueConstraintNullableModel
+ fields = ('title', 'age', 'tag')
+
+
class TestUniqueConstraintValidation(TestCase):
def setUp(self):
self.instance = UniqueConstraintModel.objects.create(
race_name='example',
position=1,
- global_id=1
+ global_id=1,
+ fancy_conditions=1
)
UniqueConstraintModel.objects.create(
race_name='example',
position=2,
- global_id=2
+ global_id=2,
+ fancy_conditions=1
)
UniqueConstraintModel.objects.create(
race_name='other',
position=1,
- global_id=3
+ global_id=3,
+ fancy_conditions=1
)
def test_repr(self):
@@ -553,33 +609,65 @@ class TestUniqueConstraintValidation(TestCase):
position = IntegerField\(.*required=True\)
global_id = IntegerField\(.*validators=\[\]\)
class Meta:
- validators = \[, \]>, fields=\('race_name', 'position'\)\)>\]
+ validators = \[\)>\]
""")
assert re.search(expected, repr(serializer)) is not None
- def test_unique_together_field(self):
+ def test_unique_together_condition(self):
"""
- UniqueConstraint fields and condition attributes must be passed
- to UniqueTogetherValidator as fields and queryset
+ Fields used in UniqueConstraint's condition must be included
+ into queryset existence check
"""
- serializer = UniqueConstraintSerializer()
- assert len(serializer.validators) == 1
- validator = serializer.validators[0]
- assert validator.fields == ('race_name', 'position')
- assert set(validator.queryset.values_list(flat=True)) == set(
- UniqueConstraintModel.objects.filter(race_name='example').values_list(flat=True)
+ UniqueConstraintModel.objects.create(
+ race_name='condition',
+ position=1,
+ global_id=10,
+ fancy_conditions=10,
)
+ serializer = UniqueConstraintSerializer(data={
+ 'race_name': 'condition',
+ 'position': 1,
+ 'global_id': 11,
+ 'fancy_conditions': 9,
+ })
+ assert serializer.is_valid()
+ serializer = UniqueConstraintSerializer(data={
+ 'race_name': 'condition',
+ 'position': 1,
+ 'global_id': 11,
+ 'fancy_conditions': 11,
+ })
+ assert not serializer.is_valid()
+
+ def test_unique_together_condition_fields_required(self):
+ """
+ Fields used in UniqueConstraint's condition must be present in serializer
+ """
+ serializer = UniqueConstraintSerializer(data={
+ 'race_name': 'condition',
+ 'position': 1,
+ 'global_id': 11,
+ })
+ assert not serializer.is_valid()
+ assert serializer.errors == {'fancy_conditions': ['This field is required.']}
+
+ class NoFieldsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniqueConstraintModel
+ fields = ('race_name', 'position', 'global_id')
+
+ serializer = NoFieldsSerializer()
+ assert len(serializer.validators) == 1
def test_single_field_uniq_validators(self):
"""
UniqueConstraint with single field must be transformed into
field's UniqueValidator
"""
- # Django 5 includes Max and Min values validators for IntergerField
+ # Django 5 includes Max and Min values validators for IntegerField
extra_validators_qty = 2 if django_version[0] >= 5 else 0
- #
serializer = UniqueConstraintSerializer()
- assert len(serializer.validators) == 1
+ assert len(serializer.validators) == 2
validators = serializer.fields['global_id'].validators
assert len(validators) == 1 + extra_validators_qty
assert validators[0].queryset == UniqueConstraintModel.objects
@@ -589,6 +677,30 @@ class TestUniqueConstraintValidation(TestCase):
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
assert ids_in_qs == {frozenset([1]), frozenset([3])}
+ def test_nullable_unique_constraint_fields_are_not_required(self):
+ serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
+ self.assertTrue(serializer.is_valid(), serializer.errors)
+ result = serializer.save()
+ self.assertIsInstance(result, UniqueConstraintNullableModel)
+
+ def test_unique_constraint_source(self):
+ class SourceUniqueConstraintSerializer(serializers.ModelSerializer):
+ raceName = serializers.CharField(source="race_name")
+
+ class Meta:
+ model = UniqueConstraintModel
+ fields = ("raceName", "position", "global_id", "fancy_conditions")
+
+ serializer = SourceUniqueConstraintSerializer(
+ data={
+ "raceName": "example",
+ "position": 5,
+ "global_id": 11,
+ "fancy_conditions": 11,
+ }
+ )
+ assert serializer.is_valid()
+
# Tests for `UniqueForDateValidator`
# ----------------------------------
diff --git a/tests/test_views.py b/tests/test_views.py
index 2648c9fb3..f37cf4a16 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,5 +1,7 @@
import copy
+import unittest
+from django import VERSION as DJANGO_VERSION
from django.test import TestCase
from rest_framework import status
@@ -45,7 +47,7 @@ def custom_handler(exc, context):
return Response({'error': 'UnknownError'}, status=500)
-class OverridenSettingsView(APIView):
+class OverriddenSettingsView(APIView):
settings = APISettings({'EXCEPTION_HANDLER': custom_handler})
def get(self, request, *args, **kwargs):
@@ -129,10 +131,20 @@ class TestCustomExceptionHandler(TestCase):
class TestCustomSettings(TestCase):
def setUp(self):
- self.view = OverridenSettingsView.as_view()
+ self.view = OverriddenSettingsView.as_view()
def test_get_exception_handler(self):
request = factory.get('/', content_type='application/json')
response = self.view(request)
assert response.status_code == 400
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
diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py
index 8e439c86e..68b1207c3 100644
--- a/tests/test_viewsets.py
+++ b/tests/test_viewsets.py
@@ -1,6 +1,8 @@
+import unittest
from functools import wraps
import pytest
+from django import VERSION as DJANGO_VERSION
from django.db import models
from django.test import TestCase, override_settings
from django.urls import include, path
@@ -196,6 +198,11 @@ class InitializeViewSetsTestCase(TestCase):
assert get.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):
diff --git a/tox.ini b/tox.ini
index 16cc3f8f4..0adcee3a4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,10 @@
[tox]
envlist =
- {py38,py39}-{django42}
- {py310}-{django42,django50,djangomain}
- {py311}-{django42,django50,djangomain}
- {py312}-{django42,django50,djangomain}
+ {py39}-{django42}
+ {py310}-{django42,django51,django52,djangomain}
+ {py311}-{django42,django51,django52,djangomain}
+ {py312}-{django42,django51,django52,djangomain}
+ {py313}-{django51,django52,djangomain}
base
dist
docs
@@ -17,6 +18,8 @@ setenv =
deps =
django42: Django>=4.2,<5.0
django50: Django>=5.0,<5.1
+ django51: Django>=5.1,<5.2
+ django52: Django>=5.2,<6.0
djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
@@ -42,12 +45,6 @@ deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.txt
-[testenv:py38-djangomain]
-ignore_outcome = true
-
-[testenv:py39-djangomain]
-ignore_outcome = true
-
[testenv:py310-djangomain]
ignore_outcome = true
@@ -56,3 +53,6 @@ ignore_outcome = true
[testenv:py312-djangomain]
ignore_outcome = true
+
+[testenv:py313-djangomain]
+ignore_outcome = true