diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md deleted file mode 100644 index 87fa57a89..000000000 --- a/.github/ISSUE_TEMPLATE/1-issue.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Issue -about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏 ---- - -## Checklist - - - -- [ ] Raised initially as discussion #... -- [ ] This is not a feature request suitable for implementation outside this project. Please elaborate what it is: - - [ ] compatibility fix for new Django/Python version ... - - [ ] other type of bug fix - - [ ] other type of improvement that does not touch existing code or change existing behavior (e.g. wrapper for new Django field) -- [ ] I have reduced the issue to the simplest possible case. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 382fc521a..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,6 +0,0 @@ -blank_issues_enabled: false -contact_links: -- name: Discussions - url: https://github.com/encode/django-rest-framework/discussions - about: > - The "Discussions" forum is where you want to start. đź’– diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d08655451..fe31e727a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,6 @@ jobs: strategy: matrix: python-version: - - '3.8' - '3.9' - '3.10' - '3.11' @@ -34,7 +33,7 @@ jobs: run: python -m pip install --upgrade pip setuptools virtualenv wheel - name: Install dependencies - run: python -m pip install --upgrade codecov tox + run: python -m pip install --upgrade tox - name: Run tox targets for ${{ matrix.python-version }} run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d . | cut -f 1 -d '-') @@ -45,8 +44,9 @@ jobs: tox -e base,dist,docs - name: Upload coverage - run: | - codecov -e TOXENV,DJANGO + uses: codecov/codecov-action@v5 + with: + env_vars: TOXENV,DJANGO test-docs: name: Test documentation links diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb01f8bf7..644a719c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,4 @@ At this point in its lifespan we consider Django REST framework to be essentially feature-complete. We may accept pull requests that track the continued development of Django versions, but would prefer not to accept new features or code formatting changes. -Apart from minor documentation changes, the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion. - The [Contributing guide in the documentation](https://www.django-rest-framework.org/community/contributing/) gives some more information on our process and code of conduct. diff --git a/README.md b/README.md index 6e62fb39a..be6619b4e 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Some reasons you might want to use REST framework: # Requirements -* Python 3.8+ -* Django 4.2, 5.0, 5.1 +* Python 3.9+ +* Django 4.2, 5.0, 5.1, 5.2 We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/SECURITY.md b/SECURITY.md index a92a1b0cf..88ff092a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,8 +2,6 @@ ## Reporting a Vulnerability -Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team). +**Please report security issues by emailing security@encode.io**. - **Please report security issues by emailing security@djangoproject.com**. - - The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. +The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 8409a83c8..84e58bf4b 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -454,6 +454,12 @@ There are currently two forks of this project. More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html). +## django-pyoidc + +[dango-pyoidc][django_pyoidc] adds support for OpenID Connect (OIDC) authentication. This allows you to delegate user management to an Identity Provider, which can be used to implement Single-Sign-On (SSO). It provides support for most uses-cases, such as customizing how token info are mapped to user models, using OIDC audiences for access control, etc. + +More information can be found in the [Documentation](https://django-pyoidc.readthedocs.io/latest/index.html). + [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -490,4 +496,5 @@ More information can be found in the [Documentation](https://django-rest-durin.r [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless [django-rest-authemail]: https://github.com/celiao/django-rest-authemail [django-rest-durin]: https://github.com/eshaan7/django-rest-durin -[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware \ No newline at end of file +[login-required-middleware]: https://docs.djangoproject.com/en/stable/ref/middleware/#django.contrib.auth.middleware.LoginRequiredMiddleware +[django-pyoidc] : https://github.com/makinacorpus/django_pyoidc diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 5cbedd964..3225191f1 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -552,7 +552,7 @@ For further examples on `HiddenField` see the [validators](validators.md) docume --- -**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). +**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). --- diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 56eb61e43..7c4eece4b 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -628,12 +628,16 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys. +The [rest-framework-gm2m-relations][drf-gm2m-relations] library provides read/write serialization for [django-gm2m][django-gm2m-field]. + [cite]: http://users.ece.utexas.edu/~adnan/pike.html [reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward [routers]: https://www.django-rest-framework.org/api-guide/routers#defaultrouter [generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations +[drf-gm2m-relations]: https://github.com/mojtabaakbari221b/rest-framework-gm2m-relations +[django-gm2m-field]: https://github.com/tkhyn/django-gm2m [django-intermediary-manytomany]: https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany [dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects [to_internal_value]: https://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index d48f785ab..7a6bd39f4 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -525,7 +525,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## LaTeX -[Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. +[Rest Framework Latex] provides a renderer that outputs PDFs using Lualatex. It is maintained by [Pebble (S/F Software)][mypebble]. [cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index eae79b62f..8d56d36f5 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -233,7 +233,7 @@ Serializer classes can also include reusable validators that are applied to the class EventSerializer(serializers.Serializer): name = serializers.CharField() - room_number = serializers.IntegerField(choices=[101, 102, 103, 201]) + room_number = serializers.ChoiceField(choices=[101, 102, 103, 201]) date = serializers.DateField() class Meta: diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 47e2ce993..7bee3166d 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -460,4 +460,4 @@ Default: `None` [cite]: https://www.python.org/dev/peps/pep-0020/ [rfc4627]: https://www.ietf.org/rfc/rfc4627.txt [heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses -[strftime]: https://docs.python.org/3/library/time.html#time.strftime +[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 261df80f2..ed585faf2 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -25,9 +25,12 @@ The `APIRequestFactory` class supports an almost identical API to Django's stand factory = APIRequestFactory() request = factory.post('/notes/', {'title': 'new idea'}) + # Using the standard RequestFactory API to encode JSON data + request = factory.post('/notes/', {'title': 'new idea'}, content_type='application/json') + #### Using the `format` argument -Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: +Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a wide set of request formats. When using this argument, the factory will select an appropriate renderer and its configured `content_type`. For example: # Create a JSON POST request factory = APIRequestFactory() @@ -41,7 +44,7 @@ To support a wider set of request formats, or change the default format, [see th If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example: - request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') + request = factory.post('/notes/', yaml.dump({'title': 'new idea'}), content_type='application/yaml') #### PUT and PATCH with form data diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 4c58fa713..e6d7774a6 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -45,7 +45,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C } } -The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. +The rates used in `DEFAULT_THROTTLE_RATES` can be specified over a period of second, minute, hour or day. The period must be specified after the `/` separator using `s`, `m`, `h` or `d`, respectively. For increased clarity, extended units such as `second`, `minute`, `hour`, `day` or even abbreviations like `sec`, `min`, `hr` are allowed, as only the first character is relevant to identify the rate. You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class-based views. @@ -110,7 +110,7 @@ You'll need to remember to also set your custom throttle class in the `'DEFAULT_ The built-in throttle implementations are open to [race conditions][race], so under high concurrency they may allow a few extra requests through. -If your project relies on guaranteeing the number of requests during concurrent requests, you will need to implement your own throttle class. See [issue #5181][gh5181] for more details. +If your project relies on guaranteeing the number of requests during concurrent requests, you will need to implement your own throttle class. --- @@ -220,5 +220,4 @@ The following is an example of a rate throttle, that will randomly throttle 1 in [identifying-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster [cache-setting]: https://docs.djangoproject.com/en/stable/ref/settings/#caches [cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache -[gh5181]: https://github.com/encode/django-rest-framework/issues/5181 [race]: https://en.wikipedia.org/wiki/Race_condition#Data_race diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e181d4c61..57bcb8628 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -48,7 +48,7 @@ If we open up the Django shell using `manage.py shell` we can now CustomerReportSerializer(): id = IntegerField(label='ID', read_only=True) time_raised = DateTimeField(read_only=True) - reference = CharField(max_length=20, validators=[]) + reference = CharField(max_length=20, validators=[UniqueValidator(queryset=CustomerReportRecord.objects.all())]) description = CharField(style={'type': 'textarea'}) The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field. @@ -166,7 +166,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden --- -**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). +**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). --- diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 43007e95d..22acfe327 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -128,6 +128,8 @@ You may inspect these attributes to adjust behavior based on the current action. permission_classes = [IsAdminUser] return [permission() for permission in permission_classes] +**Note**: the `action` attribute is not available in the `get_parsers`, `get_authenticators` and `get_content_negotiator` methods, as it is set _after_ they are called in the framework lifecycle. If you override one of these methods and try to access the `action` attribute in them, you will get an `AttributeError` error. + ## Marking extra actions for routing If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns. diff --git a/docs/community/3.16-announcement.md b/docs/community/3.16-announcement.md new file mode 100644 index 000000000..b8f460ae7 --- /dev/null +++ b/docs/community/3.16-announcement.md @@ -0,0 +1,42 @@ + + +# Django REST framework 3.16 + +At the Internet, on March 28th, 2025, we are happy to announce the release of Django REST framework 3.16. + +## Updated Django and Python support + +The latest release now fully supports Django 5.1 and the upcoming 5.2 LTS as well as Python 3.13. + +The current minimum versions of Django is now 4.2 and Python 3.9. + +## Django LoginRequiredMiddleware + +The new `LoginRequiredMiddleware` introduced by Django 5.1 can now be used alongside Django REST Framework, however it is not honored for API views as an equivalent behaviour can be configured via `DEFAULT_AUTHENTICATION_CLASSES`. See [our dedicated section](../api-guide/authentication.md#django-51-loginrequiredmiddleware) in the docs for more information. + +## Improved support for UniqueConstraint + +The generation of validators for [UniqueConstraint](https://docs.djangoproject.com/en/stable/ref/models/constraints/#uniqueconstraint) has been improved to support better nullable fields and constraints with conditions. + +## Other fixes and improvements + +There are a number of fixes and minor improvements in this release, ranging from documentation, internal infrastructure (typing, testing, requirements, deprecation, etc.), security and overall behaviour. + +See the [release notes](release-notes.md) page for a complete listing. diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 5dea6426d..5a9188943 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -4,8 +4,6 @@ > > — [Tim Berners-Lee][cite] -There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. - !!! note At this point in its lifespan we consider Django REST framework to be feature-complete. We focus on pull requests that track the continued development of Django versions, and generally do not accept new features or code formatting changes. @@ -30,24 +28,9 @@ The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines f # Issues -Our contribution process is that the [GitHub discussions page](https://github.com/encode/django-rest-framework/discussions) should generally be your starting point. Please only raise an issue or pull request if you've been recommended to do so after discussion. - -Some tips on good potential issue reporting: - * Django REST framework is considered feature-complete. Please do not file requests to change behavior, unless it is required for security reasons or to maintain compatibility with upcoming Django or Python versions. -* Search the GitHub project page for related items, and make sure you're running the latest version of REST framework before reporting an issue. * Feature requests will typically be closed with a recommendation that they be implemented outside the core REST framework library (e.g. as third-party libraries). This approach allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability and great documentation. -## Triaging issues - -Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to - -* Read through the ticket - does it make sense, is it missing any context that would help explain it better? -* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? -* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? -* If the ticket is a feature request, could the feature request instead be implemented as a third party package? -* If a ticket hasn't had much activity and addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. - # Development To start developing on Django REST framework, first create a Fork from the @@ -206,7 +189,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [so-filter]: https://stackexchange.com/filters/66475/rest-framework -[issues]: https://github.com/encode/django-rest-framework/issues?state=open [pep-8]: https://www.python.org/dev/peps/pep-0008/ [build-status]: ../img/build-status.png [pull-requests]: https://help.github.com/articles/using-pull-requests diff --git a/docs/community/project-management.md b/docs/community/project-management.md index 4f203e13b..daf2cda8d 100644 --- a/docs/community/project-management.md +++ b/docs/community/project-management.md @@ -34,7 +34,6 @@ Further notes for maintainers: * Code changes should come in the form of a pull request - do not push directly to master. * Maintainers should typically not merge their own pull requests. * Each issue/pull request should have exactly one label once triaged. -* Search for un-triaged issues with [is:open no:label][un-triaged]. --- @@ -157,7 +156,6 @@ The following issues still need to be addressed: * Document ownership and management of the security mailing list. [bus-factor]: https://en.wikipedia.org/wiki/Bus_factor -[un-triaged]: https://github.com/encode/django-rest-framework/issues?q=is%3Aopen+no%3Alabel [transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ [transifex-client]: https://pypi.org/project/transifex-client/ [translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 3e5d3ebc5..c7b82e985 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -36,6 +36,90 @@ You can determine your currently installed version using `pip show`: --- +## 3.16.x series + +### 3.16.0 + +**Date**: 28th March 2025 + +This release is considered a significant release to improve upstream support with Django and Python. Some of these may change the behaviour of existing features and pre-existing behaviour. Specifically, some fixes were added to around the support of `UniqueConstraint` with nullable fields which will improve built-in serializer validation. + +## Features + +* Add official support for Django 5.1 and its new `LoginRequiredMiddleware` in [#9514](https://github.com/encode/django-rest-framework/pull/9514) and [#9657](https://github.com/encode/django-rest-framework/pull/9657) +* Add official Django 5.2a1 support in [#9634](https://github.com/encode/django-rest-framework/pull/9634) +* Add support for Python 3.13 in [#9527](https://github.com/encode/django-rest-framework/pull/9527) and [#9556](https://github.com/encode/django-rest-framework/pull/9556) +* Support Django 2.1+ test client JSON data automatically serialized in [#6511](https://github.com/encode/django-rest-framework/pull/6511) and fix a regression in [#9615](https://github.com/encode/django-rest-framework/pull/9615) + +## Bug fixes + +* Fix unique together validator to respect condition's fields from `UniqueConstraint` in [#9360](https://github.com/encode/django-rest-framework/pull/9360) +* Fix raising on nullable fields part of `UniqueConstraint` in [#9531](https://github.com/encode/django-rest-framework/pull/9531) +* Fix `unique_together` validation with source in [#9482](https://github.com/encode/django-rest-framework/pull/9482) +* Added protections to `AttributeError` raised within properties in [#9455](https://github.com/encode/django-rest-framework/pull/9455) +* Fix `get_template_context` to handle also lists in [#9467](https://github.com/encode/django-rest-framework/pull/9467) +* Fix "Converter is already registered" deprecation warning. in [#9512](https://github.com/encode/django-rest-framework/pull/9512) +* Fix noisy warning and accept integers as min/max values of `DecimalField` in [#9515](https://github.com/encode/django-rest-framework/pull/9515) +* Fix usages of `open()` in `setup.py` in [#9661](https://github.com/encode/django-rest-framework/pull/9661) + +## Translations + +* Add some missing Chinese translations in [#9505](https://github.com/encode/django-rest-framework/pull/9505) +* Fix spelling mistakes in Farsi language were corrected in [#9521](https://github.com/encode/django-rest-framework/pull/9521) +* Fixing and adding missing Brazilian Portuguese translations in [#9535](https://github.com/encode/django-rest-framework/pull/9535) + +## Removals + +* Remove support for Python 3.8 in [#9670](https://github.com/encode/django-rest-framework/pull/9670) +* Remove long deprecated code from request wrapper in [#9441](https://github.com/encode/django-rest-framework/pull/9441) +* Remove deprecated `AutoSchema._get_reference` method in [#9525](https://github.com/encode/django-rest-framework/pull/9525) + +## Documentation and internal changes + +* Provide tests for hashing of `OperandHolder` in [#9437](https://github.com/encode/django-rest-framework/pull/9437) +* Update documentation: Add `adrf` third party package in [#9198](https://github.com/encode/django-rest-framework/pull/9198) +* Update tutorials links in Community contributions docs in [#9476](https://github.com/encode/django-rest-framework/pull/9476) +* Fix usage of deprecated Django function in example from docs in [#9509](https://github.com/encode/django-rest-framework/pull/9509) +* Move path converter docs into a separate section in [#9524](https://github.com/encode/django-rest-framework/pull/9524) +* Add test covering update view without `queryset` attribute in [#9528](https://github.com/encode/django-rest-framework/pull/9528) +* Fix Transifex link in [#9541](https://github.com/encode/django-rest-framework/pull/9541) +* Fix example `httpie` call in docs in [#9543](https://github.com/encode/django-rest-framework/pull/9543) +* Fix example for serializer field with choices in docs in [#9563](https://github.com/encode/django-rest-framework/pull/9563) +* Remove extra `<>` in validators example in [#9590](https://github.com/encode/django-rest-framework/pull/9590) +* Update `strftime` link in the docs in [#9624](https://github.com/encode/django-rest-framework/pull/9624) +* Switch to codecov GHA in [#9618](https://github.com/encode/django-rest-framework/pull/9618) +* Add note regarding availability of the `action` attribute in 'Introspecting ViewSet actions' docs section in [#9633](https://github.com/encode/django-rest-framework/pull/9633) +* Improved description of allowed throttling rates in documentation in [#9640](https://github.com/encode/django-rest-framework/pull/9640) +* Add `rest-framework-gm2m-relations` package to the list of 3rd party libraries in [#9063](https://github.com/encode/django-rest-framework/pull/9063) +* Fix a number of typos in the test suite in the docs in [#9662](https://github.com/encode/django-rest-framework/pull/9662) +* Add `django-pyoidc` as a third party authentication library in [#9667](https://github.com/encode/django-rest-framework/pull/9667) + +## New Contributors + +* [`@maerteijn`](https://github.com/maerteijn) made their first contribution in [#9198](https://github.com/encode/django-rest-framework/pull/9198) +* [`@FraCata00`](https://github.com/FraCata00) made their first contribution in [#9444](https://github.com/encode/django-rest-framework/pull/9444) +* [`@AlvaroVega`](https://github.com/AlvaroVega) made their first contribution in [#9451](https://github.com/encode/django-rest-framework/pull/9451) +* [`@james`](https://github.com/james)-mchugh made their first contribution in [#9455](https://github.com/encode/django-rest-framework/pull/9455) +* [`@ifeanyidavid`](https://github.com/ifeanyidavid) made their first contribution in [#9479](https://github.com/encode/django-rest-framework/pull/9479) +* [`@p`](https://github.com/p)-schlickmann made their first contribution in [#9480](https://github.com/encode/django-rest-framework/pull/9480) +* [`@akkuman`](https://github.com/akkuman) made their first contribution in [#9505](https://github.com/encode/django-rest-framework/pull/9505) +* [`@rafaelgramoschi`](https://github.com/rafaelgramoschi) made their first contribution in [#9509](https://github.com/encode/django-rest-framework/pull/9509) +* [`@Sinaatkd`](https://github.com/Sinaatkd) made their first contribution in [#9521](https://github.com/encode/django-rest-framework/pull/9521) +* [`@gtkacz`](https://github.com/gtkacz) made their first contribution in [#9535](https://github.com/encode/django-rest-framework/pull/9535) +* [`@sliverc`](https://github.com/sliverc) made their first contribution in [#9556](https://github.com/encode/django-rest-framework/pull/9556) +* [`@gabrielromagnoli1987`](https://github.com/gabrielromagnoli1987) made their first contribution in [#9543](https://github.com/encode/django-rest-framework/pull/9543) +* [`@cheehong1030`](https://github.com/cheehong1030) made their first contribution in [#9563](https://github.com/encode/django-rest-framework/pull/9563) +* [`@amansharma612`](https://github.com/amansharma612) made their first contribution in [#9590](https://github.com/encode/django-rest-framework/pull/9590) +* [`@Gluroda`](https://github.com/Gluroda) made their first contribution in [#9616](https://github.com/encode/django-rest-framework/pull/9616) +* [`@deepakangadi`](https://github.com/deepakangadi) made their first contribution in [#9624](https://github.com/encode/django-rest-framework/pull/9624) +* [`@EXG1O`](https://github.com/EXG1O) made their first contribution in [#9633](https://github.com/encode/django-rest-framework/pull/9633) +* [`@decadenza`](https://github.com/decadenza) made their first contribution in [#9640](https://github.com/encode/django-rest-framework/pull/9640) +* [`@mojtabaakbari221b`](https://github.com/mojtabaakbari221b) made their first contribution in [#9063](https://github.com/encode/django-rest-framework/pull/9063) +* [`@mikemanger`](https://github.com/mikemanger) made their first contribution in [#9661](https://github.com/encode/django-rest-framework/pull/9661) +* [`@gbip`](https://github.com/gbip) made their first contribution in [#9667](https://github.com/encode/django-rest-framework/pull/9667) + +**Full Changelog**: https://github.com/encode/django-rest-framework/compare/3.15.2...3.16.0 + ## 3.15.x series ### 3.15.2 @@ -121,7 +205,7 @@ Date: 15th March 2024 * Fix 404 when page query parameter is empty string [[#8578](https://github.com/encode/django-rest-framework/pull/8578)] * Fixes instance check in ListSerializer.to_representation [[#8726](https://github.com/encode/django-rest-framework/pull/8726)] [[#8727](https://github.com/encode/django-rest-framework/pull/8727)] * FloatField will crash if the input is a number that is too big [[#8725](https://github.com/encode/django-rest-framework/pull/8725)] -* Add missing DurationField to SimpleMetada label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)] +* Add missing DurationField to SimpleMetadata label_lookup [[#8702](https://github.com/encode/django-rest-framework/pull/8702)] * Add support for Python 3.11 [[#8752](https://github.com/encode/django-rest-framework/pull/8752)] * Make request consistently available in pagination classes [[#8764](https://github.com/encode/django-rest-framework/pull/9764)] * Possibility to remove trailing zeros on DecimalFields representation [[#6514](https://github.com/encode/django-rest-framework/pull/6514)] @@ -428,7 +512,7 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10. * Allow hashing of ErrorDetail. [#5932][gh5932] * Correct schema parsing for JSONField [#5878][gh5878] * Render descriptions (from help_text) using safe [#5869][gh5869] -* Removed input value from deault_error_message [#5881][gh5881] +* Removed input value from default_error_message [#5881][gh5881] * Added min_value/max_value support in DurationField [#5643][gh5643] * Fixed instance being overwritten in pk-only optimization try/except block [#5747][gh5747] * Fixed AttributeError from items filter when value is None [#5981][gh5981] diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 593836411..9d25665ad 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -32,7 +32,7 @@ We suggest adding your package to the [REST Framework][rest-framework-grid] grid #### Adding to the Django REST framework docs -Create a [Pull Request][drf-create-pr] or [Issue][drf-create-issue] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section. +Create a [Pull Request][drf-create-pr] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Packages][third-party-packages] section. #### Announce on the discussion group. @@ -44,7 +44,7 @@ Django REST Framework has a growing community of developers, packages, and resou Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages][rest-framework-grid]. -To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr]. +To submit new content, [create a pull request][drf-create-pr]. ## Async Support @@ -62,6 +62,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF. * [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers. * [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses. +* [dango-pyoidc][django-pyoidc] adds support for OpenID Connect (OIDC) authentication. ### Permissions @@ -173,7 +174,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-compat]: https://github.com/encode/django-rest-framework/blob/master/rest_framework/compat.py [rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/ [drf-create-pr]: https://github.com/encode/django-rest-framework/compare -[drf-create-issue]: https://github.com/encode/django-rest-framework/issues/new [authentication]: ../api-guide/authentication.md [permissions]: ../api-guide/permissions.md [third-party-packages]: ../topics/third-party-packages/#existing-third-party-packages @@ -256,3 +256,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-api-action]: https://github.com/Ori-Roza/drf-api-action [drf-redesign]: https://github.com/youzarsiph/drf-redesign [drf-material]: https://github.com/youzarsiph/drf-material +[django-pyoidc] : https://github.com/makinacorpus/django_pyoidc diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index b128160da..427bdd2d7 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -12,7 +12,7 @@ There are a wide range of resources available for learning and using Django REST - + @@ -28,7 +28,6 @@ There are a wide range of resources available for learning and using Django REST * [Beginner's Guide to the Django REST Framework][beginners-guide-to-the-django-rest-framework] * [Django REST Framework - An Introduction][drf-an-intro] * [Django REST Framework Tutorial][drf-tutorial] -* [Django REST Framework Course][django-rest-framework-course] * [Building a RESTful API with Django REST Framework][building-a-restful-api-with-drf] * [Getting Started with Django REST Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs] * [End to End Web App with Django REST Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs] @@ -41,8 +40,8 @@ There are a wide range of resources available for learning and using Django REST * [Creating a Production Ready API with Python and Django REST Framework – Part 2][creating-a-production-ready-api-with-python-and-drf-part2] * [Creating a Production Ready API with Python and Django REST Framework – Part 3][creating-a-production-ready-api-with-python-and-drf-part3] * [Creating a Production Ready API with Python and Django REST Framework – Part 4][creating-a-production-ready-api-with-python-and-drf-part4] -* [Django REST Framework Tutorial - Build a Blog API][django-rest-framework-tutorial-build-a-blog] -* [Django REST Framework & React Tutorial - Build a Todo List API][django-rest-framework-react-tutorial-build-a-todo-list] +* [Django Polls Tutorial API][django-polls-api] +* [Django REST Framework Tutorial: Todo API][django-rest-framework-todo-api] * [Tutorial: Django REST with React (Django 2.0)][django-rest-react-valentinog] @@ -51,11 +50,11 @@ There are a wide range of resources available for learning and using Django REST ### Talks * [Level Up! Rethinking the Web API Framework][pycon-us-2017] -* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit] +* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-toolkit] * [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy] * [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014] * [Django REST Framework: Schemas, Hypermedia & Client Libraries][pycon-uk-2016] - +* [Finally Understand Authentication in Django REST Framework][django-con-2018] ### Tutorials @@ -105,7 +104,6 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/ [cdrf.co]:http://www.cdrf.co [medium-django-rest-framework]: https://medium.com/django-rest-framework -[django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework [pycon-uk-2016]: https://www.youtube.com/watch?v=FjmiGh7OqVg [django-under-hood-2014]: https://www.youtube.com/watch?v=3cSsbe-tA0E [integrating-pandas-drf-and-bokeh]: https://web.archive.org/web/20180104205117/http://machinalis.com/blog/pandas-django-rest-framework-bokeh/ @@ -121,10 +119,10 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [creating-a-production-ready-api-with-python-and-drf-part2]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-2/ [creating-a-production-ready-api-with-python-and-drf-part3]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-3/ [creating-a-production-ready-api-with-python-and-drf-part4]: https://www.andreagrandi.it/posts/creating-a-production-ready-api-with-python-and-django-rest-framework-part-4/ -[django-rest-framework-tutorial-build-a-blog]: https://wsvincent.com/django-rest-framework-tutorial/ -[django-rest-framework-react-tutorial-build-a-todo-list]: https://wsvincent.com/django-rest-framework-react-tutorial/ +[django-polls-api]: https://learndjango.com/tutorials/django-polls-tutorial-api +[django-rest-framework-todo-api]: https://learndjango.com/tutorials/django-rest-framework-tutorial-todo-api [django-rest-api-so-easy]: https://www.youtube.com/watch?v=cqP758k1BaQ -[full-fledged-rest-api-with-django-oauth-tookit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk +[full-fledged-rest-api-with-django-oauth-toolkit]: https://www.youtube.com/watch?v=M6Ud3qC2tTk [drf-in-your-pjs]: https://www.youtube.com/watch?v=xMtHsWa72Ww [building-a-rest-api-using-django-and-drf]: https://www.youtube.com/watch?v=PwssEec3IRw [drf-tutorials]: https://www.youtube.com/watch?v=axRCBgbOJp8&list=PLJtp8Jm8EDzjgVg9vVyIUMoGyqtegj7FH @@ -139,3 +137,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/ [doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/ [developing-restful-apis-with-django-rest-framework]: https://testdriven.io/courses/django-rest-framework/ +[django-con-2018]: https://youtu.be/pY-oje5b5Qk?si=AOU6tLi0IL1_pVzq \ No newline at end of file diff --git a/docs/img/books/dfa-40-cover.jpg b/docs/img/books/dfa-40-cover.jpg new file mode 100644 index 000000000..cc47312c7 Binary files /dev/null and b/docs/img/books/dfa-40-cover.jpg differ diff --git a/docs/index.md b/docs/index.md index 24ae81672..d590d2c04 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Django (4.2, 5.0, 5.1) -* Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) +* Django (4.2, 5.0, 5.1, 5.2) +* Python (3.9, 3.10, 3.11, 3.12, 3.13) We **highly recommend** and only officially support the latest patch release of each Python and Django series. @@ -196,9 +196,7 @@ For priority support please sign up for a [professional or premium sponsorship p ## Security -Security issues are handled under the supervision of the [Django security team](https://www.djangoproject.com/foundation/teams/#security-team). - -**Please report security issues by emailing security@djangoproject.com**. +**Please report security issues by emailing security@encode.io**. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. diff --git a/mkdocs.yml b/mkdocs.yml index a031dd69b..010aaefe2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.16 Announcement': 'community/3.16-announcement.md' - '3.15 Announcement': 'community/3.15-announcement.md' - '3.14 Announcement': 'community/3.14-announcement.md' - '3.13 Announcement': 'community/3.13-announcement.md' diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 636f0c8ad..692ce9cb1 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.15.2' +__version__ = '3.16.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' @@ -23,9 +23,5 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' -class RemovedInDRF316Warning(DeprecationWarning): - pass - - class RemovedInDRF317Warning(PendingDeprecationWarning): pass diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 27c5632be..ff21bacff 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -3,6 +3,9 @@ The `compat` module provides support for backwards compatibility with older versions of Django/Python, and compatibility wrappers around optional packages. """ import django +from django.db import models +from django.db.models.constants import LOOKUP_SEP +from django.db.models.sql.query import Node from django.views.generic import View @@ -157,6 +160,10 @@ if django.VERSION >= (5, 1): # 1) the list of validators and 2) the error message. Starting from # Django 5.1 ip_address_validators only returns the list of validators from django.core.validators import ip_address_validators + + def get_referenced_base_fields_from_q(q): + return q.referenced_base_fields + else: # Django <= 5.1: create a compatibility shim for ip_address_validators from django.core.validators import \ @@ -165,6 +172,35 @@ else: def ip_address_validators(protocol, unpack_ipv4): return _ip_address_validators(protocol, unpack_ipv4)[0] + # Django < 5.1: create a compatibility shim for Q.referenced_base_fields + # https://github.com/django/django/blob/5.1a1/django/db/models/query_utils.py#L179 + def _get_paths_from_expression(expr): + if isinstance(expr, models.F): + yield expr.name + elif hasattr(expr, 'flatten'): + for child in expr.flatten(): + if isinstance(child, models.F): + yield child.name + elif isinstance(child, models.Q): + yield from _get_children_from_q(child) + + def _get_children_from_q(q): + for child in q.children: + if isinstance(child, Node): + yield from _get_children_from_q(child) + elif isinstance(child, tuple): + lhs, rhs = child + yield lhs + if hasattr(rhs, 'resolve_expression'): + yield from _get_paths_from_expression(rhs) + elif hasattr(child, 'resolve_expression'): + yield from _get_paths_from_expression(child) + + def get_referenced_base_fields_from_q(q): + return { + child.split(LOOKUP_SEP, 1)[0] for child in _get_children_from_q(q) + } + # `separators` argument to `json.dumps()` differs between 2.x and 3.x # See: https://bugs.python.org/issue22767 diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo index 5a6e3788e..03a0651fa 100644 Binary files a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo and b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po index 4d47ce3b9..bfd53ee0f 100644 --- a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po +++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Cloves Oliveira , 2020 # Craig Blaszczyk , 2015 @@ -10,6 +10,7 @@ # Hugo Leonardo Chalhoub Mendonça , 2015 # Jonatas Baldin , 2017 # Gabriel Mitelman Tkacz , 2024 +# Matheus Oliveira , 2025 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" @@ -141,7 +142,7 @@ msgstr "NĂŁo foi possĂ­vel satisfazer a requisição do cabeçalho Accept." #: exceptions.py:212 #, python-brace-format msgid "Unsupported media type \"{media_type}\" in request." -msgstr "Tipo de mĂ­dia \"{media_type}\" no pedido nĂŁo Ă© suportado." +msgstr "Tipo de mĂ­dia \"{media_type}\" no pedido nĂŁo Ă© suportado." #: exceptions.py:223 msgid "Request was throttled." @@ -200,17 +201,17 @@ msgstr "Este valor nĂŁo corresponde ao padrĂŁo exigido." msgid "" "Enter a valid \"slug\" consisting of letters, numbers, underscores or " "hyphens." -msgstr "Entrar um \"slug\" válido que consista de letras, nĂşmeros, sublinhados ou hĂ­fens." +msgstr "Insira um \"slug\" válido que consista de letras, nĂşmeros, sublinhados ou hĂ­fens." #: fields.py:839 msgid "" "Enter a valid \"slug\" consisting of Unicode letters, numbers, underscores, " "or hyphens." -msgstr "Digite um \"slug\" válido que consista de letras Unicode, nĂşmeros, sublinhados ou hĂ­fens." +msgstr "Insira um \"slug\" válido que consista de letras Unicode, nĂşmeros, sublinhados ou hĂ­fens." #: fields.py:854 msgid "Enter a valid URL." -msgstr "Entrar um URL válido." +msgstr "Insira um URL válido." #: fields.py:867 msgid "Must be a valid UUID." @@ -290,12 +291,12 @@ msgstr "Necessário uma data mas recebeu uma data e hora." #: fields.py:1303 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." -msgstr "Formato inválido para Tempo. Use um dos formatos a seguir: {format}." +msgstr "Formato inválido para tempo. Use um dos formatos a seguir: {format}." #: fields.py:1365 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." -msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format}." +msgstr "Formato inválido para duração. Use um dos formatos a seguir: {format}." #: fields.py:1399 fields.py:1456 #, python-brace-format @@ -348,7 +349,7 @@ msgstr "Certifique-se de que o nome do arquivo tem menos de {max_length} caracte msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." -msgstr "Fazer upload de uma imagem válida. O arquivo enviado nĂŁo Ă© um arquivo de imagem ou está corrompido." +msgstr "Faça upload de uma imagem válida. O arquivo enviado nĂŁo Ă© um arquivo de imagem ou está corrompido." #: fields.py:1604 relations.py:486 serializers.py:571 msgid "This list may not be empty." @@ -375,7 +376,7 @@ msgstr "Este dicionário nĂŁo pode estar vazio." #: fields.py:1755 msgid "Value must be valid JSON." -msgstr "Valor devo ser JSON válido." +msgstr "Valor deve ser JSON válido." #: filters.py:49 templates/rest_framework/filters/search.html:2 msgid "Search" @@ -395,11 +396,11 @@ msgstr "Qual campo usar ao ordenar os resultados." #: filters.py:287 msgid "ascending" -msgstr "ascendente" +msgstr "crescente" #: filters.py:288 msgid "descending" -msgstr "descendente" +msgstr "decrescente" #: pagination.py:174 msgid "A page number within the paginated result set." @@ -531,7 +532,7 @@ msgstr "Nenhum item para escholher." #: validators.py:39 msgid "This field must be unique." -msgstr "Esse campo deve ser Ăşnico." +msgstr "Esse campo deve ser Ăşnico." #: validators.py:89 #, python-brace-format diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index f35106fe5..019eeb33e 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -11,9 +11,7 @@ from django.core.validators import ( from django.db import models from django.utils.encoding import force_str -from rest_framework import ( - RemovedInDRF316Warning, exceptions, renderers, serializers -) +from rest_framework import exceptions, renderers, serializers from rest_framework.compat import inflection, uritemplate from rest_framework.fields import _UnvalidatedField, empty from rest_framework.settings import api_settings @@ -721,11 +719,3 @@ class AutoSchema(ViewInspector): path = path[1:] return [path.split('/')[0].replace('_', '-')] - - def _get_reference(self, serializer): - warnings.warn( - "Method `_get_reference()` has been renamed to `get_reference()`. " - "The old name will be removed in DRF v3.16.", - RemovedInDRF316Warning, stacklevel=2 - ) - return self.get_reference(serializer) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b1b7b6477..0b87aa8fc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -26,7 +26,9 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from rest_framework.compat import postgres_fields +from rest_framework.compat import ( + get_referenced_base_fields_from_q, postgres_fields +) from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.fields import get_error_detail from rest_framework.settings import api_settings @@ -1425,20 +1427,20 @@ class ModelSerializer(Serializer): def get_unique_together_constraints(self, model): """ - Returns iterator of (fields, queryset), each entry describes an unique together - constraint on `fields` in `queryset`. + Returns iterator of (fields, queryset, condition_fields, condition), + each entry describes an unique together constraint on `fields` in `queryset` + with respect of constraint's `condition`. """ for parent_class in [model] + list(model._meta.parents): for unique_together in parent_class._meta.unique_together: - yield unique_together, model._default_manager + yield unique_together, model._default_manager, [], None for constraint in parent_class._meta.constraints: if isinstance(constraint, models.UniqueConstraint) and len(constraint.fields) > 1: - yield ( - constraint.fields, - model._default_manager - if constraint.condition is None - else model._default_manager.filter(constraint.condition) - ) + if constraint.condition is None: + condition_fields = [] + else: + condition_fields = list(get_referenced_base_fields_from_q(constraint.condition)) + yield (constraint.fields, model._default_manager, condition_fields, constraint.condition) def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs): """ @@ -1470,9 +1472,10 @@ class ModelSerializer(Serializer): # Include each of the `unique_together` and `UniqueConstraint` field names, # so long as all the field names are included on the serializer. - for unique_together_list, queryset in self.get_unique_together_constraints(model): - if set(field_names).issuperset(unique_together_list): - unique_constraint_names |= set(unique_together_list) + for unique_together_list, queryset, condition_fields, condition in self.get_unique_together_constraints(model): + unique_together_list_and_condition_fields = set(unique_together_list) | set(condition_fields) + if set(field_names).issuperset(unique_together_list_and_condition_fields): + unique_constraint_names |= unique_together_list_and_condition_fields # Now we have all the field names that have uniqueness constraints # applied, we can add the extra 'required=...' or 'default=...' @@ -1490,6 +1493,8 @@ class ModelSerializer(Serializer): default = timezone.now elif unique_constraint_field.has_default(): default = unique_constraint_field.default + elif unique_constraint_field.null: + default = None else: default = empty @@ -1592,12 +1597,13 @@ class ModelSerializer(Serializer): # Note that we make sure to check `unique_together` both on the # base model class, but also on any parent classes. validators = [] - for unique_together, queryset in self.get_unique_together_constraints(self.Meta.model): + for unique_together, queryset, condition_fields, condition in self.get_unique_together_constraints(self.Meta.model): # Skip if serializer does not map to all unique together sources - if not set(source_map).issuperset(unique_together): + unique_together_and_condition_fields = set(unique_together) | set(condition_fields) + if not set(source_map).issuperset(unique_together_and_condition_fields): continue - for source in unique_together: + for source in unique_together_and_condition_fields: assert len(source_map[source]) == 1, ( "Unable to create `UniqueTogetherValidator` for " "`{model}.{field}` as `{serializer}` has multiple " @@ -1616,7 +1622,9 @@ class ModelSerializer(Serializer): field_names = tuple(source_map[f][0] for f in unique_together) validator = UniqueTogetherValidator( queryset=queryset, - fields=field_names + fields=field_names, + condition_fields=tuple(source_map[f][0] for f in condition_fields), + condition=condition, ) validators.append(validator) return validators diff --git a/rest_framework/test.py b/rest_framework/test.py index e939adcd7..c273724b9 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -150,15 +150,19 @@ class APIRequestFactory(DjangoRequestFactory): """ Encode the data returning a two tuple of (bytes, content_type) """ - if data is None: - return ('', content_type) + return (b'', content_type) assert format is None or content_type is None, ( 'You may not set both `format` and `content_type`.' ) if content_type: + try: + data = self._encode_json(data, content_type) + except AttributeError: + pass + # Content type specified explicitly, treat data as a raw bytestring ret = force_bytes(data, settings.DEFAULT_CHARSET) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 71ebc2ca9..a152c6362 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -6,7 +6,9 @@ This gives us better separation of concerns, allows us to use single-step object creation, and makes it possible to switch between using the implicit `ModelSerializer` class and an equivalent explicit `Serializer` class. """ +from django.core.exceptions import FieldError from django.db import DataError +from django.db.models import Exists from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ValidationError @@ -23,6 +25,17 @@ def qs_exists(queryset): return False +def qs_exists_with_condition(queryset, condition, against): + if condition is None: + return qs_exists(queryset) + try: + # use the same query as UniqueConstraint.validate + # https://github.com/django/django/blob/7ba2a0db20c37a5b1500434ca4ed48022311c171/django/db/models/constraints.py#L672 + return (condition & Exists(queryset.filter(condition))).check(against) + except (TypeError, ValueError, DataError, FieldError): + return False + + def qs_filter(queryset, **kwargs): try: return queryset.filter(**kwargs) @@ -99,10 +112,12 @@ class UniqueTogetherValidator: missing_message = _('This field is required.') requires_context = True - def __init__(self, queryset, fields, message=None): + def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None): self.queryset = queryset self.fields = fields self.message = message or self.message + self.condition_fields = [] if condition_fields is None else condition_fields + self.condition = condition def enforce_required_fields(self, attrs, serializer): """ @@ -114,7 +129,7 @@ class UniqueTogetherValidator: missing_items = { field_name: self.missing_message - for field_name in self.fields + for field_name in (*self.fields, *self.condition_fields) if serializer.fields[field_name].source not in attrs } if missing_items: @@ -173,16 +188,19 @@ class UniqueTogetherValidator: if attrs[field_name] != getattr(serializer.instance, field_name) ] - if checked_values and None not in checked_values and qs_exists(queryset): + condition_kwargs = {source: attrs[source] for source in self.condition_fields} + if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs): field_names = ', '.join(self.fields) message = self.message.format(field_names=field_names) raise ValidationError(message, code='unique') def __repr__(self): - return '<%s(queryset=%s, fields=%s)>' % ( + return '<{}({})>'.format( self.__class__.__name__, - smart_repr(self.queryset), - smart_repr(self.fields) + ', '.join( + f'{attr}={smart_repr(getattr(self, attr))}' + for attr in ('queryset', 'fields', 'condition') + if getattr(self, attr) is not None) ) def __eq__(self, other): diff --git a/setup.py b/setup.py index 67904ec61..952b5f50a 100755 --- a/setup.py +++ b/setup.py @@ -2,12 +2,11 @@ import os import re import shutil import sys -from io import open from setuptools import find_packages, setup CURRENT_PYTHON = sys.version_info[:2] -REQUIRED_PYTHON = (3, 8) +REQUIRED_PYTHON = (3, 9) # This check and everything above must remain compatible with Python 2.7. if CURRENT_PYTHON < REQUIRED_PYTHON: @@ -36,7 +35,7 @@ an older version of Django REST Framework: def read(f): - with open(f, 'r', encoding='utf-8') as file: + with open(f, encoding='utf-8') as file: return file.read() @@ -83,7 +82,7 @@ setup( packages=find_packages(exclude=['tests*']), include_package_data=True, install_requires=["django>=4.2", 'backports.zoneinfo;python_version<"3.9"'], - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -92,12 +91,12 @@ setup( 'Framework :: Django :: 4.2', 'Framework :: Django :: 5.0', 'Framework :: Django :: 5.1', + 'Framework :: Django :: 5.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py index 98fd46f9f..171f08646 100644 --- a/tests/schemas/test_coreapi.py +++ b/tests/schemas/test_coreapi.py @@ -1177,7 +1177,7 @@ class NamingCollisionViewSet(GenericViewSet): """ Example via: https://stackoverflow.com/questions/43778668/django-rest-framwork-occured-typeerror-link-object-does-not-support-item-ass/ """ - permision_class = () + permission_classes = () @action(detail=False) def detail(self, request): diff --git a/tests/test_filters.py b/tests/test_filters.py index 6db0c3deb..9fc8ad1a9 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -19,7 +19,7 @@ factory = APIRequestFactory() class SearchSplitTests(SimpleTestCase): - def test_keep_quoted_togheter_regardless_of_commas(self): + def test_keep_quoted_together_regardless_of_commas(self): assert ['hello, world'] == list(filters.search_smart_split('"hello, world"')) def test_strips_commas_around_quoted(self): @@ -516,7 +516,7 @@ class OrderingFilterModel(models.Model): class OrderingFilterRelatedModel(models.Model): - related_object = models.ForeignKey(OrderingFilterModel, related_name="relateds", on_delete=models.CASCADE) + related_object = models.ForeignKey(OrderingFilterModel, related_name="related", on_delete=models.CASCADE) index = models.SmallIntegerField(help_text="A non-related field to test with", default=0) @@ -725,9 +725,9 @@ class OrderingFilterTests(TestCase): def test_ordering_by_aggregate_field(self): # create some related models to aggregate order by num_objs = [2, 5, 3] - for obj, num_relateds in zip(OrderingFilterModel.objects.all(), - num_objs): - for _ in range(num_relateds): + for obj, num_related in zip(OrderingFilterModel.objects.all(), + num_objs): + for _ in range(num_related): new_related = OrderingFilterRelatedModel( related_object=obj ) @@ -739,10 +739,10 @@ class OrderingFilterTests(TestCase): ordering = 'title' ordering_fields = '__all__' queryset = OrderingFilterModel.objects.all().annotate( - models.Count("relateds")) + models.Count("related")) view = OrderingListView.as_view() - request = factory.get('/', {'ordering': 'relateds__count'}) + request = factory.get('/', {'ordering': 'related__count'}) response = view(request) assert response.data == [ {'id': 1, 'title': 'zyx', 'text': 'abc'}, diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index ae1a2b0fa..f1f9518cb 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -797,7 +797,7 @@ class TestIntegration(TestCase): ) self.instance.many_to_many.set(self.many_to_many_targets) - def test_pk_retrival(self): + def test_pk_retrieval(self): class TestSerializer(serializers.ModelSerializer): class Meta: model = RelationalModel diff --git a/tests/test_negotiation.py b/tests/test_negotiation.py index 089a86c62..1fe659095 100644 --- a/tests/test_negotiation.py +++ b/tests/test_negotiation.py @@ -61,8 +61,8 @@ class TestAcceptedMediaType(TestCase): def test_match_is_false_if_main_types_not_match(self): mediatype = _MediaType('test_1') - anoter_mediatype = _MediaType('test_2') - assert mediatype.match(anoter_mediatype) is False + another_mediatype = _MediaType('test_2') + assert mediatype.match(another_mediatype) is False def test_mediatype_match_is_false_if_keys_not_match(self): mediatype = _MediaType(';test_param=foo') diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 02d443ade..d8feae52b 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -513,7 +513,7 @@ class TestLimitOffset: ] } - def test_erronous_offset(self): + def test_erroneous_offset(self): request = Request(factory.get('/', {'limit': 5, 'offset': 1000})) queryset = self.paginate_queryset(request) self.get_paginated_content(queryset) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 39b7ed662..2c908ba3f 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -624,7 +624,7 @@ class PermissionsCompositionTests(TestCase): ) assert composed_perm().has_permission(request, None) is True - def test_or_lazyness(self): + def test_or_laziness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -644,7 +644,7 @@ class PermissionsCompositionTests(TestCase): assert mock_deny.call_count == 1 assert mock_allow.call_count == 1 - def test_object_or_lazyness(self): + def test_object_or_laziness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -664,7 +664,7 @@ class PermissionsCompositionTests(TestCase): assert mock_deny.call_count == 0 assert mock_allow.call_count == 1 - def test_and_lazyness(self): + def test_and_laziness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() @@ -684,7 +684,7 @@ class PermissionsCompositionTests(TestCase): assert mock_deny.call_count == 1 mock_allow.assert_not_called() - def test_object_and_lazyness(self): + def test_object_and_laziness(self): request = factory.get('/1', format='json') request.user = AnonymousUser() diff --git a/tests/test_testing.py b/tests/test_testing.py index a7e00ab63..26a6e8ffb 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,9 +8,11 @@ from django.shortcuts import redirect from django.test import TestCase, override_settings from django.urls import path -from rest_framework import fields, serializers +from rest_framework import fields, parsers, renderers, serializers, status from rest_framework.authtoken.models import Token -from rest_framework.decorators import api_view +from rest_framework.decorators import ( + api_view, parser_classes, renderer_classes +) from rest_framework.response import Response from rest_framework.test import ( APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate @@ -50,6 +52,18 @@ class BasicSerializer(serializers.Serializer): flag = fields.BooleanField(default=lambda: True) +@api_view(['POST']) +@parser_classes((parsers.JSONParser,)) +def post_json_view(request): + return Response(request.data) + + +@api_view(['DELETE']) +@renderer_classes((renderers.JSONRenderer, )) +def delete_json_view(request): + return Response(status=status.HTTP_204_NO_CONTENT) + + @api_view(['POST']) def post_view(request): serializer = BasicSerializer(data=request.data) @@ -62,7 +76,9 @@ urlpatterns = [ path('session-view/', session_view), path('redirect-view/', redirect_view), path('redirect-view//', redirect_307_308_view), - path('post-view/', post_view) + path('post-json-view/', post_json_view), + path('delete-json-view/', delete_json_view), + path('post-view/', post_view), ] @@ -236,6 +252,22 @@ class TestAPITestClient(TestCase): assert response.status_code == 200 assert response.data == {"flag": True} + def test_post_encodes_data_based_on_json_content_type(self): + data = {'data': True} + response = self.client.post( + '/post-json-view/', + data=data, + content_type='application/json' + ) + + assert response.status_code == 200 + assert response.data == data + + def test_delete_based_on_format(self): + response = self.client.delete('/delete-json-view/', format='json') + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.data is None + class TestAPIRequestFactory(TestCase): def test_csrf_exempt_by_default(self): diff --git a/tests/test_validators.py b/tests/test_validators.py index 4bb8658d5..29b097ef3 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -406,7 +406,7 @@ class TestUniquenessTogetherValidation(TestCase): "with a `UniqueTogetherValidator` using the desired field names.") assert str(excinfo.value) == expected - def test_allow_explict_override(self): + def test_allow_explicit_override(self): """ Ensure validators can be explicitly removed.. """ @@ -441,6 +441,14 @@ class TestUniquenessTogetherValidation(TestCase): serializer = NullUniquenessTogetherSerializer(data=data) assert serializer.is_valid() + def test_ignore_validation_for_missing_nullable_fields(self): + data = { + 'date': datetime.date(2000, 1, 1), + 'race_name': 'Paris Marathon', + } + serializer = NullUniquenessTogetherSerializer(data=data) + assert serializer.is_valid(), serializer.errors + def test_do_not_ignore_validation_for_null_fields(self): # None values that are not on fields part of the uniqueness constraint # do not cause the instance to skip validation. @@ -513,7 +521,7 @@ class UniqueConstraintModel(models.Model): race_name = models.CharField(max_length=100) position = models.IntegerField() global_id = models.IntegerField() - fancy_conditions = models.IntegerField(null=True) + fancy_conditions = models.IntegerField() class Meta: constraints = [ @@ -535,7 +543,24 @@ class UniqueConstraintModel(models.Model): name="unique_constraint_model_together_uniq", fields=('race_name', 'position'), condition=models.Q(race_name='example'), - ) + ), + models.UniqueConstraint( + name='unique_constraint_model_together_uniq2', + fields=('race_name', 'position'), + condition=models.Q(fancy_conditions__gte=10), + ), + ] + + +class UniqueConstraintNullableModel(models.Model): + title = models.CharField(max_length=100) + age = models.IntegerField(null=True) + tag = models.CharField(max_length=100, null=True) + + class Meta: + constraints = [ + # Unique constraint on 2 nullable fields + models.UniqueConstraint(name='unique_constraint', fields=('age', 'tag')) ] @@ -545,22 +570,31 @@ class UniqueConstraintSerializer(serializers.ModelSerializer): fields = '__all__' +class UniqueConstraintNullableSerializer(serializers.ModelSerializer): + class Meta: + model = UniqueConstraintNullableModel + fields = ('title', 'age', 'tag') + + class TestUniqueConstraintValidation(TestCase): def setUp(self): self.instance = UniqueConstraintModel.objects.create( race_name='example', position=1, - global_id=1 + global_id=1, + fancy_conditions=1 ) UniqueConstraintModel.objects.create( race_name='example', position=2, - global_id=2 + global_id=2, + fancy_conditions=1 ) UniqueConstraintModel.objects.create( race_name='other', position=1, - global_id=3 + global_id=3, + fancy_conditions=1 ) def test_repr(self): @@ -575,33 +609,65 @@ class TestUniqueConstraintValidation(TestCase): position = IntegerField\(.*required=True\) global_id = IntegerField\(.*validators=\[\]\) class Meta: - validators = \[, \]>, fields=\('race_name', 'position'\)\)>\] + validators = \[\)>\] """) assert re.search(expected, repr(serializer)) is not None - def test_unique_together_field(self): + def test_unique_together_condition(self): """ - UniqueConstraint fields and condition attributes must be passed - to UniqueTogetherValidator as fields and queryset + Fields used in UniqueConstraint's condition must be included + into queryset existence check """ - serializer = UniqueConstraintSerializer() - assert len(serializer.validators) == 1 - validator = serializer.validators[0] - assert validator.fields == ('race_name', 'position') - assert set(validator.queryset.values_list(flat=True)) == set( - UniqueConstraintModel.objects.filter(race_name='example').values_list(flat=True) + UniqueConstraintModel.objects.create( + race_name='condition', + position=1, + global_id=10, + fancy_conditions=10, ) + serializer = UniqueConstraintSerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + 'fancy_conditions': 9, + }) + assert serializer.is_valid() + serializer = UniqueConstraintSerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + 'fancy_conditions': 11, + }) + assert not serializer.is_valid() + + def test_unique_together_condition_fields_required(self): + """ + Fields used in UniqueConstraint's condition must be present in serializer + """ + serializer = UniqueConstraintSerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + }) + assert not serializer.is_valid() + assert serializer.errors == {'fancy_conditions': ['This field is required.']} + + class NoFieldsSerializer(serializers.ModelSerializer): + class Meta: + model = UniqueConstraintModel + fields = ('race_name', 'position', 'global_id') + + serializer = NoFieldsSerializer() + assert len(serializer.validators) == 1 def test_single_field_uniq_validators(self): """ UniqueConstraint with single field must be transformed into field's UniqueValidator """ - # Django 5 includes Max and Min values validators for IntergerField + # Django 5 includes Max and Min values validators for IntegerField extra_validators_qty = 2 if django_version[0] >= 5 else 0 - # serializer = UniqueConstraintSerializer() - assert len(serializer.validators) == 1 + assert len(serializer.validators) == 2 validators = serializer.fields['global_id'].validators assert len(validators) == 1 + extra_validators_qty assert validators[0].queryset == UniqueConstraintModel.objects @@ -611,6 +677,12 @@ class TestUniqueConstraintValidation(TestCase): ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")} assert ids_in_qs == {frozenset([1]), frozenset([3])} + def test_nullable_unique_constraint_fields_are_not_required(self): + serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'}) + self.assertTrue(serializer.is_valid(), serializer.errors) + result = serializer.save() + self.assertIsInstance(result, UniqueConstraintNullableModel) + # Tests for `UniqueForDateValidator` # ---------------------------------- diff --git a/tests/test_views.py b/tests/test_views.py index 11f24906e..f37cf4a16 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -47,7 +47,7 @@ def custom_handler(exc, context): return Response({'error': 'UnknownError'}, status=500) -class OverridenSettingsView(APIView): +class OverriddenSettingsView(APIView): settings = APISettings({'EXCEPTION_HANDLER': custom_handler}) def get(self, request, *args, **kwargs): @@ -131,7 +131,7 @@ class TestCustomExceptionHandler(TestCase): class TestCustomSettings(TestCase): def setUp(self): - self.view = OverridenSettingsView.as_view() + self.view = OverriddenSettingsView.as_view() def test_get_exception_handler(self): request = factory.get('/', content_type='application/json') diff --git a/tox.ini b/tox.ini index f565a1281..032d4a18e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = - {py38,py39}-{django42} - {py310}-{django42,django50,django51,djangomain} - {py311}-{django42,django50,django51,djangomain} - {py312}-{django42,django50,django51,djangomain} - {py313}-{django51,djangomain} + {py39}-{django42} + {py310}-{django42,django51,django52,djangomain} + {py311}-{django42,django51,django52,djangomain} + {py312}-{django42,django51,django52,djangomain} + {py313}-{django51,django52,djangomain} base dist docs @@ -19,6 +19,7 @@ deps = django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 + django52: Django>=5.2b1,<6.0 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt @@ -52,3 +53,6 @@ ignore_outcome = true [testenv:py312-djangomain] ignore_outcome = true + +[testenv:py313-djangomain] +ignore_outcome = true