mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-30 15:37:50 +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. | ||||||
|  |  | ||||||
|  | @ -133,8 +133,6 @@ Okay, now let's wire up the API URLs.  On to `tutorial/urls.py`... | ||||||
|         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. | ||||||
| 
 | 
 | ||||||
| Again, if we need more control over the API URLs we can simply drop down to using regular class-based views, and writing the URL conf explicitly. | Again, if we need more control over the API URLs we can simply drop down to using regular class-based views, and writing the URL conf explicitly. | ||||||
|  |  | ||||||
|  | @ -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