mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-28 04:24:00 +03:00
Merge branch 'master' into migrate_setuppy_to_pryoject.toml
This commit is contained in:
commit
2770f5e17d
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Action updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
@ -62,9 +62,9 @@ jobs:
|
||||||
name: Test documentation links
|
name: Test documentation links
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
|
|
||||||
|
|
8
.github/workflows/pre-commit.yml
vendored
8
.github/workflows/pre-commit.yml
vendored
|
@ -11,14 +11,12 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
- uses: pre-commit/action@v3.0.0
|
- uses: pre-commit/action@v3.0.1
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
|
@ -25,3 +25,9 @@ repos:
|
||||||
exclude: ^(?!docs).*$
|
exclude: ^(?!docs).*$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- black==23.1.0
|
- black==23.1.0
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
# Configuration for codespell is in .codespellrc
|
||||||
|
rev: v2.2.6
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js
|
||||||
|
|
|
@ -56,7 +56,7 @@ There is a live example API for testing purposes, [available here][sandbox].
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python 3.6+
|
* Python 3.6+
|
||||||
* Django 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
|
* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
|
||||||
|
|
||||||
We **highly recommend** and only officially support the latest patch release of
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
|
|
@ -56,10 +56,11 @@ The following sections explain more.
|
||||||
|
|
||||||
### Install dependencies
|
### Install dependencies
|
||||||
|
|
||||||
pip install pyyaml uritemplate
|
pip install pyyaml uritemplate inflection
|
||||||
|
|
||||||
* `pyyaml` is used to generate schema into YAML-based OpenAPI format.
|
* `pyyaml` is used to generate schema into YAML-based OpenAPI format.
|
||||||
* `uritemplate` is used internally to get parameters in path.
|
* `uritemplate` is used internally to get parameters in path.
|
||||||
|
* `inflection` is used to pluralize operations more appropriately in the list endpoints.
|
||||||
|
|
||||||
### Generating a static schema with the `generateschema` management command
|
### Generating a static schema with the `generateschema` management command
|
||||||
|
|
||||||
|
|
58
docs/community/3.15-announcement.md
Normal file
58
docs/community/3.15-announcement.md
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<style>
|
||||||
|
.promo li a {
|
||||||
|
float: left;
|
||||||
|
width: 130px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 30px;
|
||||||
|
padding: 150px 0 0 0;
|
||||||
|
background-position: 0 50%;
|
||||||
|
background-size: 130px auto;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
font-size: 120%;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.promo li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
# Django REST framework 3.15
|
||||||
|
|
||||||
|
At the Internet, on March 15th, 2024, with 176 commits by 138 authors, we are happy to announce the release of Django REST framework 3.15.
|
||||||
|
|
||||||
|
## Django 5.0 and Python 3.12 support
|
||||||
|
|
||||||
|
The latest release now fully supports Django 5.0 and Python 3.12.
|
||||||
|
|
||||||
|
The current minimum versions of Django still is 3.0 and Python 3.6.
|
||||||
|
|
||||||
|
## Primary Support of UniqueConstraint
|
||||||
|
|
||||||
|
`ModelSerializer` generates validators for [UniqueConstraint](https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint) (both UniqueValidator and UniqueTogetherValidator)
|
||||||
|
|
||||||
|
## ValidationErrors improvements
|
||||||
|
|
||||||
|
The `ValidationError` has been aligned with Django's, currently supporting the same style (signature) and nesting.
|
||||||
|
|
||||||
|
## SimpleRouter non-regex matching support
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## ZoneInfo as the primary source of timezone data
|
||||||
|
|
||||||
|
Dependency on pytz has been removed and deprecation warnings have been added, Django will provide ZoneInfo instances as long as USE_DEPRECATED_PYTZ is not enabled. More info on the migration can be found [in this guide](https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html).
|
||||||
|
|
||||||
|
## Align `SearchFilter` behaviour to `django.contrib.admin` search
|
||||||
|
|
||||||
|
Searches now may contain _quoted phrases_ with spaces, each phrase is considered as a single search term, and it will raise a validation error if any null-character is provided in search. See the [Filtering API guide](../api-guide/filtering.md) for more information.
|
||||||
|
|
||||||
|
## Default values propagation
|
||||||
|
|
||||||
|
Model fields' default values are now propagated to serializer fields, for more information see the [Serializer fields API guide](../api-guide/fields.md#default).
|
||||||
|
|
||||||
|
## Other fixes and improvements
|
||||||
|
|
||||||
|
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.
|
|
@ -34,6 +34,90 @@ You can determine your currently installed version using `pip show`:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 3.15.x series
|
||||||
|
|
||||||
|
### 3.15.0
|
||||||
|
|
||||||
|
Date: 15th March 2024
|
||||||
|
|
||||||
|
* Django 5.0 and Python 3.12 support [[#9157](https://github.com/encode/django-rest-framework/pull/9157)]
|
||||||
|
* Use POST method instead of GET to perform logout in browsable API [[9208](https://github.com/encode/django-rest-framework/pull/9208)]
|
||||||
|
* Added jQuery 3.7.1 support & dropped previous version [[#9094](https://github.com/encode/django-rest-framework/pull/9094)]
|
||||||
|
* Use str as default path converter [[#9066](https://github.com/encode/django-rest-framework/pull/9066)]
|
||||||
|
* Document support for http.HTTPMethod in the @action decorator added in Python 3.11 [[#9067](https://github.com/encode/django-rest-framework/pull/9067)]
|
||||||
|
* Update exceptions.md [[#9071](https://github.com/encode/django-rest-framework/pull/9071)]
|
||||||
|
* Partial serializer should not have required fields [[#7563](https://github.com/encode/django-rest-framework/pull/7563)]
|
||||||
|
* Propagate 'default' from model field to serializer field. [[#9030](https://github.com/encode/django-rest-framework/pull/9030)]
|
||||||
|
* Allow to override child.run_validation call in ListSerializer [[#8035](https://github.com/encode/django-rest-framework/pull/8035)]
|
||||||
|
* Align SearchFilter behaviour to django.contrib.admin search [[#9017](https://github.com/encode/django-rest-framework/pull/9017)]
|
||||||
|
* Class name added to unknown field error [[#9019](https://github.com/encode/django-rest-framework/pull/9019)]
|
||||||
|
* Fix: Pagination response schemas. [[#9049](https://github.com/encode/django-rest-framework/pull/9049)]
|
||||||
|
* Fix choices in ChoiceField to support IntEnum [[#8955](https://github.com/encode/django-rest-framework/pull/8955)]
|
||||||
|
* Fix `SearchFilter` rendering search field with invalid value [[#9023](https://github.com/encode/django-rest-framework/pull/9023)]
|
||||||
|
* Fix OpenAPI Schema yaml rendering for `timedelta` [[#9007](https://github.com/encode/django-rest-framework/pull/9007)]
|
||||||
|
* Fix `NamespaceVersioning` ignoring `DEFAULT_VERSION` on non-None namespaces [[#7278](https://github.com/encode/django-rest-framework/pull/7278)]
|
||||||
|
* Added Deprecation Warnings for CoreAPI [[#7519](https://github.com/encode/django-rest-framework/pull/7519)]
|
||||||
|
* Removed usage of `field.choices` that triggered full table load [[#8950](https://github.com/encode/django-rest-framework/pull/8950)]
|
||||||
|
* Permit mixed casing of string values for `BooleanField` validation [[#8970](https://github.com/encode/django-rest-framework/pull/8970)]
|
||||||
|
* Fixes `BrowsableAPIRenderer` for usage with `ListSerializer`. [[#7530](https://github.com/encode/django-rest-framework/pull/7530)]
|
||||||
|
* Change semantic of `OR` of two permission classes [[#7522](https://github.com/encode/django-rest-framework/pull/7522)]
|
||||||
|
* Remove dependency on `pytz` [[#8984](https://github.com/encode/django-rest-framework/pull/8984)]
|
||||||
|
* Make set_value a method within `Serializer` [[#8001](https://github.com/encode/django-rest-framework/pull/8001)]
|
||||||
|
* Fix URLPathVersioning reverse fallback [[#7247](https://github.com/encode/django-rest-framework/pull/7247)]
|
||||||
|
* Warn about Decimal type in min_value and max_value arguments of DecimalField [[#8972](https://github.com/encode/django-rest-framework/pull/8972)]
|
||||||
|
* Fix mapping for choice values [[#8968](https://github.com/encode/django-rest-framework/pull/8968)]
|
||||||
|
* Refactor read function to use context manager for file handling [[#8967](https://github.com/encode/django-rest-framework/pull/8967)]
|
||||||
|
* Fix: fallback on CursorPagination ordering if unset on the view [[#8954](https://github.com/encode/django-rest-framework/pull/8954)]
|
||||||
|
* Replaced `OrderedDict` with `dict` [[#8964](https://github.com/encode/django-rest-framework/pull/8964)]
|
||||||
|
* Refactor get_field_info method to include max_digits and decimal_places attributes in SimpleMetadata class [[#8943](https://github.com/encode/django-rest-framework/pull/8943)]
|
||||||
|
* Implement `__eq__` for validators [[#8925](https://github.com/encode/django-rest-framework/pull/8925)]
|
||||||
|
* Ensure CursorPagination respects nulls in the ordering field [[#8912](https://github.com/encode/django-rest-framework/pull/8912)]
|
||||||
|
* Use ZoneInfo as primary source of timezone data [[#8924](https://github.com/encode/django-rest-framework/pull/8924)]
|
||||||
|
* Add username search field for TokenAdmin (#8927) [[#8934](https://github.com/encode/django-rest-framework/pull/8934)]
|
||||||
|
* Handle Nested Relation in SlugRelatedField when many=False [[#8922](https://github.com/encode/django-rest-framework/pull/8922)]
|
||||||
|
* Bump version of jQuery to 3.6.4 & updated ref links [[#8909](https://github.com/encode/django-rest-framework/pull/8909)]
|
||||||
|
* Support UniqueConstraint [[#7438](https://github.com/encode/django-rest-framework/pull/7438)]
|
||||||
|
* Allow Request, Response, Field, and GenericAPIView to be subscriptable. This allows the classes to be made generic for type checking. [[#8825](https://github.com/encode/django-rest-framework/pull/8825)]
|
||||||
|
* Feat: Add some changes to ValidationError to support django style validation errors [[#8863](https://github.com/encode/django-rest-framework/pull/8863)]
|
||||||
|
* Fix Respect `can_read_model` permission in DjangoModelPermissions [[#8009](https://github.com/encode/django-rest-framework/pull/8009)]
|
||||||
|
* Add SimplePathRouter [[#6789](https://github.com/encode/django-rest-framework/pull/6789)]
|
||||||
|
* Re-prefetch related objects after updating [[#8043](https://github.com/encode/django-rest-framework/pull/8043)]
|
||||||
|
* Fix FilePathField required argument [[#8805](https://github.com/encode/django-rest-framework/pull/8805)]
|
||||||
|
* Raise ImproperlyConfigured exception if `basename` is not unique [[#8438](https://github.com/encode/django-rest-framework/pull/8438)]
|
||||||
|
* Use PrimaryKeyRelatedField pkfield in openapi [[#8315](https://github.com/encode/django-rest-framework/pull/8315)]
|
||||||
|
* replace partition with split in BasicAuthentication [[#8790](https://github.com/encode/django-rest-framework/pull/8790)]
|
||||||
|
* Fix BooleanField's allow_null behavior [[#8614](https://github.com/encode/django-rest-framework/pull/8614)]
|
||||||
|
* Handle Django's ValidationErrors in ListField [[#6423](https://github.com/encode/django-rest-framework/pull/6423)]
|
||||||
|
* Remove a bit of inline CSS. Add CSP nonce where it might be required and is available [[#8783](https://github.com/encode/django-rest-framework/pull/8783)]
|
||||||
|
* Use autocomplete widget for user selection in Token admin [[#8534](https://github.com/encode/django-rest-framework/pull/8534)]
|
||||||
|
* Make browsable API compatible with strong CSP [[#8784](https://github.com/encode/django-rest-framework/pull/8784)]
|
||||||
|
* Avoid inline script execution for injecting CSRF token [[#7016](https://github.com/encode/django-rest-framework/pull/7016)]
|
||||||
|
* Mitigate global dependency on inflection [[#8017](https://github.com/encode/django-rest-framework/pull/8017)] [[#8781](https://github.com/encode/django-rest-framework/pull/8781)]
|
||||||
|
* Register Django urls [[#8778](https://github.com/encode/django-rest-framework/pull/8778)]
|
||||||
|
* Implemented Verbose Name Translation for TokenProxy [[#8713](https://github.com/encode/django-rest-framework/pull/8713)]
|
||||||
|
* Properly handle OverflowError in DurationField deserialization [[#8042](https://github.com/encode/django-rest-framework/pull/8042)]
|
||||||
|
* Fix OpenAPI operation name plural appropriately [[#8017](https://github.com/encode/django-rest-framework/pull/8017)]
|
||||||
|
* Represent SafeString as plain string on schema rendering [[#8429](https://github.com/encode/django-rest-framework/pull/8429)]
|
||||||
|
* Fix #8771 - Checking for authentication even if `_ignore_model_permissions = True` [[#8772](https://github.com/encode/django-rest-framework/pull/8772)]
|
||||||
|
* 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 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)]
|
||||||
|
* Add a method for getting serializer field name (OpenAPI) [[#7493](https://github.com/encode/django-rest-framework/pull/7493)]
|
||||||
|
* Add `__eq__` method for `OperandHolder` class [[#8710](https://github.com/encode/django-rest-framework/pull/8710)]
|
||||||
|
* Avoid importing `django.test` package when not testing [[#8699](https://github.com/encode/django-rest-framework/pull/8699)]
|
||||||
|
* Preserve exception messages for wrapped Django exceptions [[#8051](https://github.com/encode/django-rest-framework/pull/8051)]
|
||||||
|
* Include `examples` and `format` to OpenAPI schema of CursorPagination [[#8687](https://github.com/encode/django-rest-framework/pull/8687)] [[#8686](https://github.com/encode/django-rest-framework/pull/8686)]
|
||||||
|
* Fix infinite recursion with deepcopy on Request [[#8684](https://github.com/encode/django-rest-framework/pull/8684)]
|
||||||
|
* Refactor: Replace try/except with contextlib.suppress() [[#8676](https://github.com/encode/django-rest-framework/pull/8676)]
|
||||||
|
* Minor fix to SerializeMethodField docstring [[#8629](https://github.com/encode/django-rest-framework/pull/8629)]
|
||||||
|
* Minor refactor: Unnecessary use of list() function [[#8672](https://github.com/encode/django-rest-framework/pull/8672)]
|
||||||
|
* Unnecessary list comprehension [[#8670](https://github.com/encode/django-rest-framework/pull/8670)]
|
||||||
|
* Use correct class to indicate present deprecation [[#8665](https://github.com/encode/django-rest-framework/pull/8665)]
|
||||||
|
|
||||||
## 3.14.x series
|
## 3.14.x series
|
||||||
|
|
||||||
### 3.14.0
|
### 3.14.0
|
||||||
|
@ -946,7 +1030,7 @@ See the [release announcement][3.6-release].
|
||||||
* description.py codes and tests removal. ([#4153][gh4153])
|
* description.py codes and tests removal. ([#4153][gh4153])
|
||||||
* Wrap guardian.VERSION in tuple. ([#4149][gh4149])
|
* Wrap guardian.VERSION in tuple. ([#4149][gh4149])
|
||||||
* Refine validator for fields with <source=> kwargs. ([#4146][gh4146])
|
* Refine validator for fields with <source=> kwargs. ([#4146][gh4146])
|
||||||
* Fix None values representation in childs of ListField, DictField. ([#4118][gh4118])
|
* Fix None values representation in children of ListField, DictField. ([#4118][gh4118])
|
||||||
* Resolve TimeField representation for midnight value. ([#4107][gh4107])
|
* Resolve TimeField representation for midnight value. ([#4107][gh4107])
|
||||||
* Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. ([#4106][gh4106])
|
* Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. ([#4106][gh4106])
|
||||||
* TimeField render returns None instead of 00:00:00. ([#4105][gh4105])
|
* TimeField render returns None instead of 00:00:00. ([#4105][gh4105])
|
||||||
|
|
|
@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
REST framework requires the following:
|
REST framework requires the following:
|
||||||
|
|
||||||
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
|
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
|
||||||
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2)
|
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0)
|
||||||
|
|
||||||
We **highly recommend** and only officially support the latest patch release of
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
|
|
@ -132,8 +132,6 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += router.urls
|
|
||||||
|
|
||||||
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.
|
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ nav:
|
||||||
- 'Contributing to REST framework': 'community/contributing.md'
|
- 'Contributing to REST framework': 'community/contributing.md'
|
||||||
- 'Project management': 'community/project-management.md'
|
- 'Project management': 'community/project-management.md'
|
||||||
- 'Release Notes': 'community/release-notes.md'
|
- 'Release Notes': 'community/release-notes.md'
|
||||||
|
- '3.15 Announcement': 'community/3.15-announcement.md'
|
||||||
- '3.14 Announcement': 'community/3.14-announcement.md'
|
- '3.14 Announcement': 'community/3.14-announcement.md'
|
||||||
- '3.13 Announcement': 'community/3.13-announcement.md'
|
- '3.13 Announcement': 'community/3.13-announcement.md'
|
||||||
- '3.12 Announcement': 'community/3.12-announcement.md'
|
- '3.12 Announcement': 'community/3.12-announcement.md'
|
||||||
|
|
|
@ -18,6 +18,7 @@ classifiers = [
|
||||||
"Framework :: Django :: 4.0",
|
"Framework :: Django :: 4.0",
|
||||||
"Framework :: Django :: 4.1",
|
"Framework :: Django :: 4.1",
|
||||||
"Framework :: Django :: 4.2",
|
"Framework :: Django :: 4.2",
|
||||||
|
"Framework :: Django :: 5.0",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: BSD License",
|
"License :: OSI Approved :: BSD License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
|
|
|
@ -10,7 +10,7 @@ ______ _____ _____ _____ __
|
||||||
import django
|
import django
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.14.0'
|
__version__ = '3.15.0'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 3-Clause'
|
__license__ = 'BSD 3-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
||||||
|
|
|
@ -46,6 +46,12 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
|
# inflection is optional
|
||||||
|
try:
|
||||||
|
import inflection
|
||||||
|
except ImportError:
|
||||||
|
inflection = None
|
||||||
|
|
||||||
|
|
||||||
# requests is optional
|
# requests is optional
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -14,7 +14,7 @@ from django.utils.encoding import force_str
|
||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
RemovedInDRF315Warning, exceptions, renderers, serializers
|
RemovedInDRF315Warning, exceptions, renderers, serializers
|
||||||
)
|
)
|
||||||
from rest_framework.compat import uritemplate
|
from rest_framework.compat import inflection, uritemplate
|
||||||
from rest_framework.fields import _UnvalidatedField, empty
|
from rest_framework.fields import _UnvalidatedField, empty
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
@ -247,9 +247,8 @@ class AutoSchema(ViewInspector):
|
||||||
name = name[:-len(action)]
|
name = name[:-len(action)]
|
||||||
|
|
||||||
if action == 'list':
|
if action == 'list':
|
||||||
from inflection import pluralize
|
assert inflection, '`inflection` must be installed for OpenAPI schema support.'
|
||||||
|
name = inflection.pluralize(name)
|
||||||
name = pluralize(name)
|
|
||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
|
@ -603,12 +603,6 @@ class ListSerializer(BaseSerializer):
|
||||||
self.min_length = kwargs.pop('min_length', None)
|
self.min_length = kwargs.pop('min_length', None)
|
||||||
assert self.child is not None, '`child` is a required argument.'
|
assert self.child is not None, '`child` is a required argument.'
|
||||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||||
|
|
||||||
instance = kwargs.get('instance', [])
|
|
||||||
data = kwargs.get('data', [])
|
|
||||||
if instance and data:
|
|
||||||
assert len(data) == len(instance), 'Data and instance should have same length'
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.child.bind(field_name='', parent=self)
|
self.child.bind(field_name='', parent=self)
|
||||||
|
|
||||||
|
@ -694,13 +688,7 @@ class ListSerializer(BaseSerializer):
|
||||||
ret = []
|
ret = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for idx, item in enumerate(data):
|
for item in data:
|
||||||
if (
|
|
||||||
hasattr(self, 'instance')
|
|
||||||
and self.instance
|
|
||||||
and len(self.instance) > idx
|
|
||||||
):
|
|
||||||
self.child.instance = self.instance[idx]
|
|
||||||
try:
|
try:
|
||||||
validated = self.run_child_validation(item)
|
validated = self.run_child_validation(item)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
|
|
|
@ -3,6 +3,12 @@ function replaceDocument(docString) {
|
||||||
|
|
||||||
doc.write(docString);
|
doc.write(docString);
|
||||||
doc.close();
|
doc.close();
|
||||||
|
|
||||||
|
if (window.djdt) {
|
||||||
|
// If Django Debug Toolbar is available, reinitialize it so that
|
||||||
|
// it can show updated panels from new `docString`.
|
||||||
|
window.addEventListener("load", djdt.init);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doAjaxSubmit(e) {
|
function doAjaxSubmit(e) {
|
||||||
|
|
|
@ -27,8 +27,8 @@ def smart_repr(value):
|
||||||
if isinstance(value, models.Manager):
|
if isinstance(value, models.Manager):
|
||||||
return manager_repr(value)
|
return manager_repr(value)
|
||||||
|
|
||||||
if isinstance(value, Promise) and value._delegate_text:
|
if isinstance(value, Promise):
|
||||||
value = force_str(value)
|
value = force_str(value, strings_only=True)
|
||||||
|
|
||||||
value = repr(value)
|
value = repr(value)
|
||||||
|
|
||||||
|
|
|
@ -421,7 +421,7 @@ class APIView(View):
|
||||||
"""
|
"""
|
||||||
# Make the error obvious if a proper response is not returned
|
# Make the error obvious if a proper response is not returned
|
||||||
assert isinstance(response, HttpResponseBase), (
|
assert isinstance(response, HttpResponseBase), (
|
||||||
'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
|
'Expected a `Response`, `HttpResponse` or `StreamingHttpResponse` '
|
||||||
'to be returned from the view, but received a `%s`'
|
'to be returned from the view, but received a `%s`'
|
||||||
% type(response)
|
% type(response)
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,3 +26,8 @@ include = rest_framework/*,tests/*
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
pragma: no cover
|
pragma: no cover
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
[codespell]
|
||||||
|
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||||
|
skip = */kickstarter-announcement.md,*.js,*.map,*.po
|
||||||
|
ignore-words-list = fo,malcom,ser
|
||||||
|
|
|
@ -1538,7 +1538,8 @@ class TestNoOutputFormatDateTimeField(FieldValues):
|
||||||
field = serializers.DateTimeField(format=None)
|
field = serializers.DateTimeField(format=None)
|
||||||
|
|
||||||
|
|
||||||
class TestNaiveDateTimeField(FieldValues):
|
@override_settings(TIME_ZONE='UTC', USE_TZ=False)
|
||||||
|
class TestNaiveDateTimeField(FieldValues, TestCase):
|
||||||
"""
|
"""
|
||||||
Valid and invalid values for `DateTimeField` with naive datetimes.
|
Valid and invalid values for `DateTimeField` with naive datetimes.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json # noqa
|
import json # noqa
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
@ -169,33 +170,32 @@ class TestRegularFieldMappings(TestCase):
|
||||||
model = RegularFieldsModel
|
model = RegularFieldsModel
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
TestSerializer():
|
TestSerializer\(\):
|
||||||
auto_field = IntegerField(read_only=True)
|
auto_field = IntegerField\(read_only=True\)
|
||||||
big_integer_field = IntegerField()
|
big_integer_field = IntegerField\(.*\)
|
||||||
boolean_field = BooleanField(default=False, required=False)
|
boolean_field = BooleanField\(default=False, required=False\)
|
||||||
char_field = CharField(max_length=100)
|
char_field = CharField\(max_length=100\)
|
||||||
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
|
comma_separated_integer_field = CharField\(max_length=100, validators=\[<django.core.validators.RegexValidator object>\]\)
|
||||||
date_field = DateField()
|
date_field = DateField\(\)
|
||||||
datetime_field = DateTimeField()
|
datetime_field = DateTimeField\(\)
|
||||||
decimal_field = DecimalField(decimal_places=1, max_digits=3)
|
decimal_field = DecimalField\(decimal_places=1, max_digits=3\)
|
||||||
email_field = EmailField(max_length=100)
|
email_field = EmailField\(max_length=100\)
|
||||||
float_field = FloatField()
|
float_field = FloatField\(\)
|
||||||
integer_field = IntegerField()
|
integer_field = IntegerField\(.*\)
|
||||||
null_boolean_field = BooleanField(allow_null=True, default=False, required=False)
|
null_boolean_field = BooleanField\(allow_null=True, default=False, required=False\)
|
||||||
positive_integer_field = IntegerField()
|
positive_integer_field = IntegerField\(.*\)
|
||||||
positive_small_integer_field = IntegerField()
|
positive_small_integer_field = IntegerField\(.*\)
|
||||||
slug_field = SlugField(allow_unicode=False, max_length=100)
|
slug_field = SlugField\(allow_unicode=False, max_length=100\)
|
||||||
small_integer_field = IntegerField()
|
small_integer_field = IntegerField\(.*\)
|
||||||
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
|
text_field = CharField\(max_length=100, style={'base_template': 'textarea.html'}\)
|
||||||
file_field = FileField(max_length=100)
|
file_field = FileField\(max_length=100\)
|
||||||
time_field = TimeField()
|
time_field = TimeField\(\)
|
||||||
url_field = URLField(max_length=100)
|
url_field = URLField\(max_length=100\)
|
||||||
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
|
custom_field = ModelField\(model_field=<tests.test_model_serializer.CustomField: custom_field>\)
|
||||||
file_path_field = FilePathField(path=%r)
|
file_path_field = FilePathField\(path=%r\)
|
||||||
""" % tempfile.gettempdir())
|
""" % tempfile.gettempdir())
|
||||||
|
assert re.search(expected, repr(TestSerializer())) is not None
|
||||||
self.assertEqual(repr(TestSerializer()), expected)
|
|
||||||
|
|
||||||
def test_field_options(self):
|
def test_field_options(self):
|
||||||
class TestSerializer(serializers.ModelSerializer):
|
class TestSerializer(serializers.ModelSerializer):
|
||||||
|
@ -203,19 +203,19 @@ class TestRegularFieldMappings(TestCase):
|
||||||
model = FieldOptionsModel
|
model = FieldOptionsModel
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
TestSerializer():
|
TestSerializer\(\):
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField\(label='ID', read_only=True\)
|
||||||
value_limit_field = IntegerField(max_value=10, min_value=1)
|
value_limit_field = IntegerField\(max_value=10, min_value=1\)
|
||||||
length_limit_field = CharField(max_length=12, min_length=3)
|
length_limit_field = CharField\(max_length=12, min_length=3\)
|
||||||
blank_field = CharField(allow_blank=True, max_length=10, required=False)
|
blank_field = CharField\(allow_blank=True, max_length=10, required=False\)
|
||||||
null_field = IntegerField(allow_null=True, required=False)
|
null_field = IntegerField\(allow_null=True,.*required=False\)
|
||||||
default_field = IntegerField(default=0, required=False)
|
default_field = IntegerField\(default=0,.*required=False\)
|
||||||
descriptive_field = IntegerField(help_text='Some help text', label='A label')
|
descriptive_field = IntegerField\(help_text='Some help text', label='A label'.*\)
|
||||||
choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
|
choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
||||||
text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
|
text_choices_field = ChoiceField\(choices=(?:\[|\()\('red', 'Red'\), \('blue', 'Blue'\), \('green', 'Green'\)(?:\]|\))\)
|
||||||
""")
|
""")
|
||||||
self.assertEqual(repr(TestSerializer()), expected)
|
assert re.search(expected, repr(TestSerializer())) is not None
|
||||||
|
|
||||||
def test_nullable_boolean_field_choices(self):
|
def test_nullable_boolean_field_choices(self):
|
||||||
class NullableBooleanChoicesModel(models.Model):
|
class NullableBooleanChoicesModel(models.Model):
|
||||||
|
@ -1334,12 +1334,12 @@ class TestFieldSource(TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
TestSerializer():
|
TestSerializer\(\):
|
||||||
number_field = IntegerField(source='integer_field')
|
number_field = IntegerField\(.*source='integer_field'\)
|
||||||
""")
|
""")
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertEqual(repr(TestSerializer()), expected)
|
assert re.search(expected, repr(TestSerializer())) is not None
|
||||||
|
|
||||||
|
|
||||||
class Issue6110TestModel(models.Model):
|
class Issue6110TestModel(models.Model):
|
||||||
|
|
|
@ -2,7 +2,6 @@ import inspect
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
|
||||||
from collections import ChainMap
|
from collections import ChainMap
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
@ -784,63 +783,3 @@ class TestSetValueMethod:
|
||||||
ret = {'a': 1}
|
ret = {'a': 1}
|
||||||
self.s.set_value(ret, ['x', 'y'], 2)
|
self.s.set_value(ret, ['x', 'y'], 2)
|
||||||
assert ret == {'a': 1, 'x': {'y': 2}}
|
assert ret == {'a': 1, 'x': {'y': 2}}
|
||||||
|
|
||||||
|
|
||||||
class MyClass(models.Model):
|
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
value = models.CharField(max_length=100, blank=True)
|
|
||||||
|
|
||||||
app_label = "test"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_valid(self):
|
|
||||||
return self.name == 'valid'
|
|
||||||
|
|
||||||
|
|
||||||
class MyClassSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = MyClass
|
|
||||||
fields = ('id', 'name', 'value')
|
|
||||||
|
|
||||||
def validate_value(self, value):
|
|
||||||
if value and not self.instance.is_valid:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
'Status cannot be set for invalid instance')
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class TestMultipleObjectsValidation(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.objs = [
|
|
||||||
MyClass(name='valid'),
|
|
||||||
MyClass(name='invalid'),
|
|
||||||
MyClass(name='other'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_multiple_objects_are_validated_separately(self):
|
|
||||||
|
|
||||||
serializer = MyClassSerializer(
|
|
||||||
data=[{'value': 'set', 'id': instance.id} for instance in
|
|
||||||
self.objs],
|
|
||||||
instance=self.objs,
|
|
||||||
many=True,
|
|
||||||
partial=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert not serializer.is_valid()
|
|
||||||
assert serializer.errors == [
|
|
||||||
{},
|
|
||||||
{'value': ['Status cannot be set for invalid instance']},
|
|
||||||
{'value': ['Status cannot be set for invalid instance']}
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_exception_raised_when_data_and_instance_length_different(self):
|
|
||||||
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
MyClassSerializer(
|
|
||||||
data=[{'value': 'set', 'id': instance.id} for instance in
|
|
||||||
self.objs],
|
|
||||||
instance=self.objs[:-1],
|
|
||||||
many=True,
|
|
||||||
partial=True,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django import VERSION as django_version
|
||||||
from django.db import DataError, models
|
from django.db import DataError, models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -112,11 +114,15 @@ class TestUniquenessValidation(TestCase):
|
||||||
def test_doesnt_pollute_model(self):
|
def test_doesnt_pollute_model(self):
|
||||||
instance = AnotherUniquenessModel.objects.create(code='100')
|
instance = AnotherUniquenessModel.objects.create(code='100')
|
||||||
serializer = AnotherUniquenessSerializer(instance)
|
serializer = AnotherUniquenessSerializer(instance)
|
||||||
assert AnotherUniquenessModel._meta.get_field('code').validators == []
|
assert all(
|
||||||
|
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
|
||||||
|
)
|
||||||
|
|
||||||
# Accessing data shouldn't effect validators on the model
|
# Accessing data shouldn't effect validators on the model
|
||||||
serializer.data
|
serializer.data
|
||||||
assert AnotherUniquenessModel._meta.get_field('code').validators == []
|
assert all(
|
||||||
|
["Unique" not in repr(v) for v in AnotherUniquenessModel._meta.get_field('code').validators]
|
||||||
|
)
|
||||||
|
|
||||||
def test_related_model_is_unique(self):
|
def test_related_model_is_unique(self):
|
||||||
data = {'username': 'Existing', 'email': 'new-email@example.com'}
|
data = {'username': 'Existing', 'email': 'new-email@example.com'}
|
||||||
|
@ -193,15 +199,15 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
serializer = UniquenessTogetherSerializer()
|
serializer = UniquenessTogetherSerializer()
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
UniquenessTogetherSerializer():
|
UniquenessTogetherSerializer\(\):
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField\(label='ID', read_only=True\)
|
||||||
race_name = CharField(max_length=100, required=True)
|
race_name = CharField\(max_length=100, required=True\)
|
||||||
position = IntegerField(required=True)
|
position = IntegerField\(.*required=True\)
|
||||||
class Meta:
|
class Meta:
|
||||||
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
|
validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('race_name', 'position'\)\)>\]
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert re.search(expected, repr(serializer)) is not None
|
||||||
|
|
||||||
def test_is_not_unique_together(self):
|
def test_is_not_unique_together(self):
|
||||||
"""
|
"""
|
||||||
|
@ -282,13 +288,13 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
read_only_fields = ('race_name',)
|
read_only_fields = ('race_name',)
|
||||||
|
|
||||||
serializer = ReadOnlyFieldSerializer()
|
serializer = ReadOnlyFieldSerializer()
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
ReadOnlyFieldSerializer():
|
ReadOnlyFieldSerializer\(\):
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField\(label='ID', read_only=True\)
|
||||||
race_name = CharField(read_only=True)
|
race_name = CharField\(read_only=True\)
|
||||||
position = IntegerField(required=True)
|
position = IntegerField\(.*required=True\)
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert re.search(expected, repr(serializer)) is not None
|
||||||
|
|
||||||
def test_read_only_fields_with_default(self):
|
def test_read_only_fields_with_default(self):
|
||||||
"""
|
"""
|
||||||
|
@ -366,14 +372,14 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
fields = ['name', 'position']
|
fields = ['name', 'position']
|
||||||
|
|
||||||
serializer = TestSerializer()
|
serializer = TestSerializer()
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
TestSerializer():
|
TestSerializer\(\):
|
||||||
name = CharField(source='race_name')
|
name = CharField\(source='race_name'\)
|
||||||
position = IntegerField()
|
position = IntegerField\(.*\)
|
||||||
class Meta:
|
class Meta:
|
||||||
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('name', 'position'))>]
|
validators = \[<UniqueTogetherValidator\(queryset=UniquenessTogetherModel.objects.all\(\), fields=\('name', 'position'\)\)>\]
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert re.search(expected, repr(serializer)) is not None
|
||||||
|
|
||||||
def test_default_validator_with_multiple_fields_with_same_source(self):
|
def test_default_validator_with_multiple_fields_with_same_source(self):
|
||||||
class TestSerializer(serializers.ModelSerializer):
|
class TestSerializer(serializers.ModelSerializer):
|
||||||
|
@ -411,13 +417,13 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
serializer = NoValidatorsSerializer()
|
serializer = NoValidatorsSerializer()
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
NoValidatorsSerializer():
|
NoValidatorsSerializer\(\):
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField\(label='ID', read_only=True.*\)
|
||||||
race_name = CharField(max_length=100)
|
race_name = CharField\(max_length=100\)
|
||||||
position = IntegerField()
|
position = IntegerField\(.*\)
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert re.search(expected, repr(serializer)) is not None
|
||||||
|
|
||||||
def test_ignore_validation_for_null_fields(self):
|
def test_ignore_validation_for_null_fields(self):
|
||||||
# None values that are on fields which are part of the uniqueness
|
# None values that are on fields which are part of the uniqueness
|
||||||
|
@ -540,16 +546,16 @@ class TestUniqueConstraintValidation(TestCase):
|
||||||
# the order of validators isn't deterministic so delete
|
# the order of validators isn't deterministic so delete
|
||||||
# fancy_conditions field that has two of them
|
# fancy_conditions field that has two of them
|
||||||
del serializer.fields['fancy_conditions']
|
del serializer.fields['fancy_conditions']
|
||||||
expected = dedent("""
|
expected = dedent(r"""
|
||||||
UniqueConstraintSerializer():
|
UniqueConstraintSerializer\(\):
|
||||||
id = IntegerField(label='ID', read_only=True)
|
id = IntegerField\(label='ID', read_only=True\)
|
||||||
race_name = CharField(max_length=100, required=True)
|
race_name = CharField\(max_length=100, required=True\)
|
||||||
position = IntegerField(required=True)
|
position = IntegerField\(.*required=True\)
|
||||||
global_id = IntegerField(validators=[<UniqueValidator(queryset=UniqueConstraintModel.objects.all())>])
|
global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintModel.objects.all\(\)\)>\]\)
|
||||||
class Meta:
|
class Meta:
|
||||||
validators = [<UniqueTogetherValidator(queryset=<QuerySet [<UniqueConstraintModel: UniqueConstraintModel object (1)>, <UniqueConstraintModel: UniqueConstraintModel object (2)>]>, fields=('race_name', 'position'))>]
|
validators = \[<UniqueTogetherValidator\(queryset=<QuerySet \[<UniqueConstraintModel: UniqueConstraintModel object \(1\)>, <UniqueConstraintModel: UniqueConstraintModel object \(2\)>\]>, fields=\('race_name', 'position'\)\)>\]
|
||||||
""")
|
""")
|
||||||
assert repr(serializer) == expected
|
assert re.search(expected, repr(serializer)) is not None
|
||||||
|
|
||||||
def test_unique_together_field(self):
|
def test_unique_together_field(self):
|
||||||
"""
|
"""
|
||||||
|
@ -569,15 +575,18 @@ class TestUniqueConstraintValidation(TestCase):
|
||||||
UniqueConstraint with single field must be transformed into
|
UniqueConstraint with single field must be transformed into
|
||||||
field's UniqueValidator
|
field's UniqueValidator
|
||||||
"""
|
"""
|
||||||
|
# Django 5 includes Max and Min values validators for IntergerField
|
||||||
|
extra_validators_qty = 2 if django_version[0] >= 5 else 0
|
||||||
|
#
|
||||||
serializer = UniqueConstraintSerializer()
|
serializer = UniqueConstraintSerializer()
|
||||||
assert len(serializer.validators) == 1
|
assert len(serializer.validators) == 1
|
||||||
validators = serializer.fields['global_id'].validators
|
validators = serializer.fields['global_id'].validators
|
||||||
assert len(validators) == 1
|
assert len(validators) == 1 + extra_validators_qty
|
||||||
assert validators[0].queryset == UniqueConstraintModel.objects
|
assert validators[0].queryset == UniqueConstraintModel.objects
|
||||||
|
|
||||||
validators = serializer.fields['fancy_conditions'].validators
|
validators = serializer.fields['fancy_conditions'].validators
|
||||||
assert len(validators) == 2
|
assert len(validators) == 2 + extra_validators_qty
|
||||||
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators}
|
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])}
|
assert ids_in_qs == {frozenset([1]), frozenset([3])}
|
||||||
|
|
||||||
|
|
||||||
|
|
5
tox.ini
5
tox.ini
|
@ -4,8 +4,8 @@ envlist =
|
||||||
{py36,py37,py38,py39}-django31
|
{py36,py37,py38,py39}-django31
|
||||||
{py36,py37,py38,py39,py310}-django32
|
{py36,py37,py38,py39,py310}-django32
|
||||||
{py38,py39,py310}-{django40,django41,django42,djangomain}
|
{py38,py39,py310}-{django40,django41,django42,djangomain}
|
||||||
{py311}-{django41,django42,djangomain}
|
{py311}-{django41,django42,django50,djangomain}
|
||||||
{py312}-{django42,djangomain}
|
{py312}-{django42,djanggo50,djangomain}
|
||||||
base
|
base
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
@ -24,6 +24,7 @@ deps =
|
||||||
django40: Django>=4.0,<4.1
|
django40: Django>=4.0,<4.1
|
||||||
django41: Django>=4.1,<4.2
|
django41: Django>=4.1,<4.2
|
||||||
django42: Django>=4.2,<5.0
|
django42: Django>=4.2,<5.0
|
||||||
|
django50: Django>=5.0,<5.1
|
||||||
djangomain: https://github.com/django/django/archive/main.tar.gz
|
djangomain: https://github.com/django/django/archive/main.tar.gz
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-optionals.txt
|
-rrequirements/requirements-optionals.txt
|
||||||
|
|
Loading…
Reference in New Issue
Block a user