- {{ symbol }}
- {{ rates.personal1 }}
- /month{% if vat %} +VAT{% endif %}
-
-
Individual
-
-
- Support ongoing development
-
-
- Credited on the site
-
-
-
-
-
-
-
-
-*Billing is monthly and you can cancel at any time.*
-
----
-
-## Corporate plans
-
-These subscriptions are recommended for companies and organizations using REST framework either publicly or privately.
-
-In exchange for funding you'll also receive advertising space on our site, allowing you to **promote your company or product to many tens of thousands of developers worldwide**.
-
-Our professional and premium plans also include **priority support**. At any time your engineers can escalate an issue or discussion group thread, and we'll ensure it gets a guaranteed response within the next working day.
-
-
-
-
-
- {{ symbol }}
- {{ rates.corporate1 }}
- /month{% if vat %} +VAT{% endif %}
-
-
Basic
-
-
- Support ongoing development
-
-
- Funding page ad placement
-
-
-
-
-
-
-
-
- {{ symbol }}
- {{ rates.corporate2 }}
- /month{% if vat %} +VAT{% endif %}
-
-
Professional
-
-
- Support ongoing development
-
-
- Sidebar ad placement
-
-
- Priority support for your engineers
-
-
-
-
-
-
-
-
- {{ symbol }}
- {{ rates.corporate3 }}
- /month{% if vat %} +VAT{% endif %}
-
-
Premium
-
-
- Support ongoing development
-
-
- Homepage ad placement
-
-
- Sidebar ad placement
-
-
- Priority support for your engineers
-
-
-
-
-
-
-
-
-
-*Billing is monthly and you can cancel at any time.*
-
-Once you've signed up, we will contact you via email and arrange your ad placements on the site.
-
-For further enquires please contact funding@django-rest-framework.org.
-
----
-
-## Accountability
-
-In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018/) and regularly include financial reports and cost breakdowns.
-
-
-
-
-
+ {{ symbol }}
+ {{ rates.personal1 }}
+ /month{% if vat %} +VAT{% endif %}
+
+
Individual
+
+
+ Support ongoing development
+
+
+ Credited on the site
+
+
+
+
+
+
+
+
+*Billing is monthly and you can cancel at any time.*
+
+---
+
+## Corporate plans
+
+These subscriptions are recommended for companies and organizations using REST framework either publicly or privately.
+
+In exchange for funding you'll also receive advertising space on our site, allowing you to **promote your company or product to many tens of thousands of developers worldwide**.
+
+Our professional and premium plans also include **priority support**. At any time your engineers can escalate an issue or discussion group thread, and we'll ensure it gets a guaranteed response within the next working day.
+
+
+
+
+
+ {{ symbol }}
+ {{ rates.corporate1 }}
+ /month{% if vat %} +VAT{% endif %}
+
+
Basic
+
+
+ Support ongoing development
+
+
+ Funding page ad placement
+
+
+
+
+
+
+
+
+ {{ symbol }}
+ {{ rates.corporate2 }}
+ /month{% if vat %} +VAT{% endif %}
+
+
Professional
+
+
+ Support ongoing development
+
+
+ Sidebar ad placement
+
+
+ Priority support for your engineers
+
+
+
+
+
+
+
+
+ {{ symbol }}
+ {{ rates.corporate3 }}
+ /month{% if vat %} +VAT{% endif %}
+
+
Premium
+
+
+ Support ongoing development
+
+
+ Homepage ad placement
+
+
+ Sidebar ad placement
+
+
+ Priority support for your engineers
+
+
+
+
+
+
+
+
+
+*Billing is monthly and you can cancel at any time.*
+
+Once you've signed up, we will contact you via email and arrange your ad placements on the site.
+
+For further enquires please contact funding@django-rest-framework.org.
+
+---
+
+## Accountability
+
+In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](https://www.encode.io/reports/march-2018/) and regularly include financial reports and cost breakdowns.
+
+
+
+
+
+
+
+
+
+
+---
+
+## Frequently asked questions
+
+**Q: Can you issue monthly invoices?**
+A: Yes, we are happy to issue monthly invoices. Please just email us and let us know who to issue the invoice to (name and address) and which email address to send it to each month.
+
+**Q: Does sponsorship include VAT?**
+A: Sponsorship is VAT exempt.
+
+**Q: Do I have to sign up for a certain time period?**
+A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime.
+
+**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?**
+A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution.
+
+**Q: Are you only looking for corporate sponsors?**
+A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support.
+
+---
+
+## Our sponsors
+
+
+
+
diff --git a/docs/community/project-management.md b/docs/community/project-management.md
index 92132ae7e..4f203e13b 100644
--- a/docs/community/project-management.md
+++ b/docs/community/project-management.md
@@ -13,55 +13,13 @@ The aim is to ensure that the project has a high
## Maintenance team
-We have a quarterly maintenance cycle where new members may join the maintenance team. We currently cap the size of the team at 5 members, and may encourage folks to step out of the team for a cycle to allow new members to participate.
+[Participating actively in the REST framework project](contributing.md) **does not require being part of the maintenance team**. Almost every important part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository.
-#### Current team
+#### Composition
-The [maintenance team for Q4 2015](https://github.com/encode/django-rest-framework/issues/2190):
+The composition of the maintenance team is handled by [@tomchristie](https://github.com/encode/). Team members will be added as collaborators to the repository.
-* [@tomchristie](https://github.com/encode/)
-* [@xordoquy](https://github.com/xordoquy/) (Release manager.)
-* [@carltongibson](https://github.com/carltongibson/)
-* [@kevin-brown](https://github.com/kevin-brown/)
-* [@jpadilla](https://github.com/jpadilla/)
-
-#### Maintenance cycles
-
-Each maintenance cycle is initiated by an issue being opened with the `Process` label.
-
-* To be considered for a maintainer role simply comment against the issue.
-* Existing members must explicitly opt-in to the next cycle by check-marking their name.
-* The final decision on the incoming team will be made by `@tomchristie`.
-
-Members of the maintenance team will be added as collaborators to the repository.
-
-The following template should be used for the description of the issue, and serves as the formal process for selecting the team.
-
- This issue is for determining the maintenance team for the *** period.
-
- Please see the [Project management](https://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details.
-
- ---
-
- #### Renewing existing members.
-
- The following people are the current maintenance team. Please checkmark your name if you wish to continue to have write permission on the repository for the *** period.
-
- - [ ] @***
- - [ ] @***
- - [ ] @***
- - [ ] @***
- - [ ] @***
-
- ---
-
- #### New members.
-
- If you wish to be considered for this or a future date, please comment against this or subsequent issues.
-
- To modify this process for future maintenance cycles make a pull request to the [project management](https://www.django-rest-framework.org/topics/project-management/) documentation.
-
-#### Responsibilities of team members
+#### Responsibilities
Team members have the following responsibilities.
@@ -78,16 +36,12 @@ Further notes for maintainers:
* Each issue/pull request should have exactly one label once triaged.
* Search for un-triaged issues with [is:open no:label][un-triaged].
-It should be noted that participating actively in the REST framework project clearly **does not require being part of the maintenance team**. Almost every import part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository.
-
---
## Release process
-The release manager is selected on every quarterly maintenance cycle.
-
-* The manager should be selected by `@tomchristie`.
-* The manager will then have the maintainer role added to PyPI package.
+* The release manager is selected by `@tomchristie`.
+* The release manager will then have the maintainer role added to PyPI package.
* The previous manager will then have the maintainer role removed from the PyPI package.
Our PyPI releases will be handled by either the current release manager, or by `@tomchristie`. Every release should have an open issue tagged with the `Release` label and marked against the appropriate milestone.
@@ -198,8 +152,7 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons
The following issues still need to be addressed:
-* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.
-* Document ownership of the [live example][sandbox] API.
+* Ensure `@j4mie` has back-up access to the `django-rest-framework.org` domain setup and admin.
* Document ownership of the [mailing list][mailing-list] and IRC channel.
* Document ownership and management of the security mailing list.
@@ -208,5 +161,4 @@ The following issues still need to be addressed:
[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
-[sandbox]: https://restframework.herokuapp.com/
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md
index ab591db3b..3e5d3ebc5 100644
--- a/docs/community/release-notes.md
+++ b/docs/community/release-notes.md
@@ -2,11 +2,13 @@
## Versioning
-Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
+- **Minor** version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes.
-Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
+- **Medium** version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
-Major version numbers (x.0.0) are reserved for substantial project milestones.
+- **Major** version numbers (x.0.0) are reserved for substantial project milestones.
+
+As REST Framework is considered feature-complete, most releases are expected to be minor releases.
## Deprecation policy
@@ -36,12 +38,29 @@ You can determine your currently installed version using `pip show`:
## 3.15.x series
+### 3.15.2
+
+**Date**: 14th June 2024
+
+* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9435)
+* Revert "Ensure CursorPagination respects nulls in the ordering field". [#9381](https://github.com/encode/django-rest-framework/pull/9381)
+* Use warnings rather than logging a warning for DecimalField. [#9367](https://github.com/encode/django-rest-framework/pull/9367)
+* Remove unused code. [#9393](https://github.com/encode/django-rest-framework/pull/9393)
+* Django < 4.2 and Python < 3.8 no longer supported. [#9393](https://github.com/encode/django-rest-framework/pull/9393)
+
+### 3.15.1
+
+Date: 22nd March 2024
+
+* Fix `SearchFilter` handling of quoted and comma separated strings, when `.get_search_terms` is being called into by a custom class. See [[#9338](https://github.com/encode/django-rest-framework/issues/9338)]
+* Revert number of 3.15.0 issues which included unintended side-effects. See [[#9331](https://github.com/encode/django-rest-framework/issues/9331)]
+
### 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)]
+* 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)]
@@ -92,7 +111,7 @@ Date: 15th March 2024
* 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 [[#8017](https://github.com/encode/django-rest-framework/pull/8017)] [[#8781](https://github.com/encode/django-rest-framework/pull/8781)]
+* 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)]
@@ -110,7 +129,7 @@ Date: 15th March 2024
* 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)]
+* 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)]
diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md
index 3a4ba5848..593836411 100644
--- a/docs/community/third-party-packages.md
+++ b/docs/community/third-party-packages.md
@@ -46,6 +46,10 @@ Check out a grid detailing all the packages and ecosystem around Django REST Fra
To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr].
+## Async Support
+
+* [adrf](https://github.com/em1208/adrf) - Async support, provides async Views, ViewSets, and Serializers.
+
### Authentication
* [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support.
@@ -125,6 +129,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Misc
+* [drf-sendables][drf-sendables] - User messages for Django REST Framework
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serializer that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
@@ -157,6 +162,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [drf-redesign][drf-redesign] - A project that gives a fresh look to the browse-able API using Bootstrap 5.
* [drf-material][drf-material] - A project that gives a sleek and elegant look to the browsable API using Material Design.
+[drf-sendables]: https://github.com/amikrop/drf-sendables
[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
[new-repo]: https://github.com/new
diff --git a/docs/img/premium/zuplo-readme.png b/docs/img/premium/zuplo-readme.png
new file mode 100644
index 000000000..245ded35e
Binary files /dev/null and b/docs/img/premium/zuplo-readme.png differ
diff --git a/docs/index.md b/docs/index.md
index 07d233107..719eb6e6e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -75,10 +75,11 @@ continued development by **[signing up for a paid plan][funding]**.
-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), and [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship).*
+*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=DjangoRESTFramework&utm_medium=Webpage_Logo_Ad&utm_content=Developer&utm_campaign=DjangoRESTFramework_Jan2022_HomePage), [Spacinov](https://www.spacinov.com/), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [CryptAPI](https://cryptapi.io), [FEZTO](https://www.fezto.xyz/?utm_source=DjangoRESTFramework), [Svix](https://www.svix.com/?utm_source=django-REST&utm_medium=sponsorship), , and [Zuplo](https://zuplo.link/django-web).*
---
@@ -86,8 +87,8 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:
-* 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, 5.0)
+* Django (4.2, 5.0)
+* Python (3.8, 3.9, 3.10, 3.11, 3.12)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.
@@ -95,8 +96,8 @@ each Python and Django series.
The following packages are optional:
* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
-* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
-* [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing.
+* [Markdown][markdown] (3.3.0+) - Markdown support for the browsable API.
+* [Pygments][pygments] (2.7.0+) - Add syntax highlighting to Markdown processing.
* [django-filter][django-filter] (1.0.1+) - Filtering support.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
@@ -184,7 +185,7 @@ Can't wait to get started? The [quickstart guide][quickstart] is the fastest way
## Development
See the [Contribution guidelines][contributing] for information on how to clone
-the repository, run the test suite and contribute changes back to REST
+the repository, run the test suite and help maintain the code base of REST
Framework.
## Support
@@ -247,7 +248,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[serializer-section]: api-guide/serializers#serializers
[modelserializer-section]: api-guide/serializers#modelserializer
[functionview-section]: api-guide/views#function-based-views
-[sandbox]: https://restframework.herokuapp.com/
[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors
[quickstart]: tutorial/quickstart.md
diff --git a/docs/topics/html-and-forms.md b/docs/topics/html-and-forms.md
index 17c9e3314..c7e51c152 100644
--- a/docs/topics/html-and-forms.md
+++ b/docs/topics/html-and-forms.md
@@ -1,220 +1,220 @@
-# HTML & Forms
-
-REST framework is suitable for returning both API style responses, and regular HTML pages. Additionally, serializers can be used as HTML forms and rendered in templates.
-
-## Rendering HTML
-
-In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
-
-The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response.
-
-The `StaticHTMLRender` class expects the response to contain a string of the pre-rendered HTML content.
-
-Because static HTML pages typically have different behavior from API responses you'll probably need to write any HTML views explicitly, rather than relying on the built-in generic views.
-
-Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template:
-
-**views.py**:
-
- from my_project.example.models import Profile
- from rest_framework.renderers import TemplateHTMLRenderer
- from rest_framework.response import Response
- from rest_framework.views import APIView
-
-
- class ProfileList(APIView):
- renderer_classes = [TemplateHTMLRenderer]
- template_name = 'profile_list.html'
-
- def get(self, request):
- queryset = Profile.objects.all()
- return Response({'profiles': queryset})
-
-**profile_list.html**:
-
-
-
Profiles
-
- {% for profile in profiles %}
-
{{ profile.name }}
- {% endfor %}
-
-
-
-## Rendering Forms
-
-Serializers may be rendered as forms by using the `render_form` template tag, and including the serializer instance as context to the template.
-
-The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance:
-
-**views.py**:
-
- from django.shortcuts import get_object_or_404
- from my_project.example.models import Profile
- from rest_framework.renderers import TemplateHTMLRenderer
- from rest_framework.views import APIView
-
-
- class ProfileDetail(APIView):
- renderer_classes = [TemplateHTMLRenderer]
- template_name = 'profile_detail.html'
-
- def get(self, request, pk):
- profile = get_object_or_404(Profile, pk=pk)
- serializer = ProfileSerializer(profile)
- return Response({'serializer': serializer, 'profile': profile})
-
- def post(self, request, pk):
- profile = get_object_or_404(Profile, pk=pk)
- serializer = ProfileSerializer(profile, data=request.data)
- if not serializer.is_valid():
- return Response({'serializer': serializer, 'profile': profile})
- serializer.save()
- return redirect('profile-list')
-
-**profile_detail.html**:
-
- {% load rest_framework %}
-
-
-
-
Profile - {{ profile.name }}
-
-
-
-
-
-### Using template packs
-
-The `render_form` tag takes an optional `template_pack` argument, that specifies which template directory should be used for rendering the form and form fields.
-
-REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are `horizontal`, `vertical`, and `inline`. The default style is `horizontal`. To use any of these template packs you'll want to also include the Bootstrap 3 CSS.
-
-The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS:
-
-
- …
-
-
-
-Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates.
-
-Let's take a look at how to render each of the three available template packs. For these examples we'll use a single serializer class to present a "Login" form.
-
- class LoginSerializer(serializers.Serializer):
- email = serializers.EmailField(
- max_length=100,
- style={'placeholder': 'Email', 'autofocus': True}
- )
- password = serializers.CharField(
- max_length=100,
- style={'input_type': 'password', 'placeholder': 'Password'}
- )
- remember_me = serializers.BooleanField()
-
----
-
-#### `rest_framework/vertical`
-
-Presents form labels above their corresponding control inputs, using the standard Bootstrap layout.
-
-*This is the default template pack.*
-
- {% load rest_framework %}
-
- ...
-
-
-
-![Vertical form example](../img/vertical.png)
-
----
-
-#### `rest_framework/horizontal`
-
-Presents labels and controls alongside each other, using a 2/10 column split.
-
-*This is the form style used in the browsable API and admin renderers.*
-
- {% load rest_framework %}
-
- ...
-
-
-
-![Horizontal form example](../img/horizontal.png)
-
----
-
-#### `rest_framework/inline`
-
-A compact form style that presents all the controls inline.
-
- {% load rest_framework %}
-
- ...
-
-
-
-![Inline form example](../img/inline.png)
-
-## Field styles
-
-Serializer fields can have their rendering style customized by using the `style` keyword argument. This argument is a dictionary of options that control the template and layout used.
-
-The most common way to customize the field style is to use the `base_template` style keyword argument to select which template in the template pack should be use.
-
-For example, to render a `CharField` as an HTML textarea rather than the default HTML input, you would use something like this:
-
- details = serializers.CharField(
- max_length=1000,
- style={'base_template': 'textarea.html'}
- )
-
-If you instead want a field to be rendered using a custom template that is *not part of an included template pack*, you can instead use the `template` style option, to fully specify a template name:
-
- details = serializers.CharField(
- max_length=1000,
- style={'template': 'my-field-templates/custom-input.html'}
- )
-
-Field templates can also use additional style properties, depending on their type. For example, the `textarea.html` template also accepts a `rows` property that can be used to affect the sizing of the control.
-
- details = serializers.CharField(
- max_length=1000,
- style={'base_template': 'textarea.html', 'rows': 10}
- )
-
-The complete list of `base_template` options and their associated style options is listed below.
-
-base_template | Valid field types | Additional style options
------------------------|-------------------------------------------------------------|-----------------------------------------------
-input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus
-textarea.html | `CharField` | rows, placeholder, hide_label
-select.html | `ChoiceField` or relational field types | hide_label
-radio.html | `ChoiceField` or relational field types | inline, hide_label
-select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label
-checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label
-checkbox.html | `BooleanField` | hide_label
-fieldset.html | Nested serializer | hide_label
-list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
+# HTML & Forms
+
+REST framework is suitable for returning both API style responses, and regular HTML pages. Additionally, serializers can be used as HTML forms and rendered in templates.
+
+## Rendering HTML
+
+In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
+
+The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response.
+
+The `StaticHTMLRender` class expects the response to contain a string of the pre-rendered HTML content.
+
+Because static HTML pages typically have different behavior from API responses you'll probably need to write any HTML views explicitly, rather than relying on the built-in generic views.
+
+Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template:
+
+**views.py**:
+
+ from my_project.example.models import Profile
+ from rest_framework.renderers import TemplateHTMLRenderer
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
+
+ class ProfileList(APIView):
+ renderer_classes = [TemplateHTMLRenderer]
+ template_name = 'profile_list.html'
+
+ def get(self, request):
+ queryset = Profile.objects.all()
+ return Response({'profiles': queryset})
+
+**profile_list.html**:
+
+
+
Profiles
+
+ {% for profile in profiles %}
+
{{ profile.name }}
+ {% endfor %}
+
+
+
+## Rendering Forms
+
+Serializers may be rendered as forms by using the `render_form` template tag, and including the serializer instance as context to the template.
+
+The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance:
+
+**views.py**:
+
+ from django.shortcuts import get_object_or_404
+ from my_project.example.models import Profile
+ from rest_framework.renderers import TemplateHTMLRenderer
+ from rest_framework.views import APIView
+
+
+ class ProfileDetail(APIView):
+ renderer_classes = [TemplateHTMLRenderer]
+ template_name = 'profile_detail.html'
+
+ def get(self, request, pk):
+ profile = get_object_or_404(Profile, pk=pk)
+ serializer = ProfileSerializer(profile)
+ return Response({'serializer': serializer, 'profile': profile})
+
+ def post(self, request, pk):
+ profile = get_object_or_404(Profile, pk=pk)
+ serializer = ProfileSerializer(profile, data=request.data)
+ if not serializer.is_valid():
+ return Response({'serializer': serializer, 'profile': profile})
+ serializer.save()
+ return redirect('profile-list')
+
+**profile_detail.html**:
+
+ {% load rest_framework %}
+
+
+
+
Profile - {{ profile.name }}
+
+
+
+
+
+### Using template packs
+
+The `render_form` tag takes an optional `template_pack` argument, that specifies which template directory should be used for rendering the form and form fields.
+
+REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are `horizontal`, `vertical`, and `inline`. The default style is `horizontal`. To use any of these template packs you'll want to also include the Bootstrap 3 CSS.
+
+The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS:
+
+
+ …
+
+
+
+Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates.
+
+Let's take a look at how to render each of the three available template packs. For these examples we'll use a single serializer class to present a "Login" form.
+
+ class LoginSerializer(serializers.Serializer):
+ email = serializers.EmailField(
+ max_length=100,
+ style={'placeholder': 'Email', 'autofocus': True}
+ )
+ password = serializers.CharField(
+ max_length=100,
+ style={'input_type': 'password', 'placeholder': 'Password'}
+ )
+ remember_me = serializers.BooleanField()
+
+---
+
+#### `rest_framework/vertical`
+
+Presents form labels above their corresponding control inputs, using the standard Bootstrap layout.
+
+*This is the default template pack.*
+
+ {% load rest_framework %}
+
+ ...
+
+
+
+![Vertical form example](../img/vertical.png)
+
+---
+
+#### `rest_framework/horizontal`
+
+Presents labels and controls alongside each other, using a 2/10 column split.
+
+*This is the form style used in the browsable API and admin renderers.*
+
+ {% load rest_framework %}
+
+ ...
+
+
+
+![Horizontal form example](../img/horizontal.png)
+
+---
+
+#### `rest_framework/inline`
+
+A compact form style that presents all the controls inline.
+
+ {% load rest_framework %}
+
+ ...
+
+
+
+![Inline form example](../img/inline.png)
+
+## Field styles
+
+Serializer fields can have their rendering style customized by using the `style` keyword argument. This argument is a dictionary of options that control the template and layout used.
+
+The most common way to customize the field style is to use the `base_template` style keyword argument to select which template in the template pack should be use.
+
+For example, to render a `CharField` as an HTML textarea rather than the default HTML input, you would use something like this:
+
+ details = serializers.CharField(
+ max_length=1000,
+ style={'base_template': 'textarea.html'}
+ )
+
+If you instead want a field to be rendered using a custom template that is *not part of an included template pack*, you can instead use the `template` style option, to fully specify a template name:
+
+ details = serializers.CharField(
+ max_length=1000,
+ style={'template': 'my-field-templates/custom-input.html'}
+ )
+
+Field templates can also use additional style properties, depending on their type. For example, the `textarea.html` template also accepts a `rows` property that can be used to affect the sizing of the control.
+
+ details = serializers.CharField(
+ max_length=1000,
+ style={'base_template': 'textarea.html', 'rows': 10}
+ )
+
+The complete list of `base_template` options and their associated style options is listed below.
+
+base_template | Valid field types | Additional style options
+-----------------------|-------------------------------------------------------------|-----------------------------------------------
+input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus
+textarea.html | `CharField` | rows, placeholder, hide_label
+select.html | `ChoiceField` or relational field types | hide_label
+radio.html | `ChoiceField` or relational field types | inline, hide_label
+select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label
+checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label
+checkbox.html | `BooleanField` | hide_label
+fieldset.html | Nested serializer | hide_label
+list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 6db3ea282..1dac5e0d8 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
---
-**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox].
+**Note**: The code for this tutorial is available in the [encode/rest-framework-tutorial][repo] repository on GitHub. Feel free to clone the repository and see the code in action.
---
@@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes.
content = JSONRenderer().render(serializer.data)
content
- # b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}'
+ # b'{"id":2,"title":"","code":"print(\\"hello, world\\")\\n","linenos":false,"language":"python","style":"friendly"}'
Deserialization is similar. First we parse a stream into Python native datatypes...
@@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
serializer.is_valid()
# True
serializer.validated_data
- # OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
+ # {'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}
serializer.save()
#
@@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
- # [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])]
+ # [{'id': 1, 'title': '', 'code': 'foo = "bar"\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'}, {'id': 3, 'title': '', 'code': 'print("hello, world")', 'linenos': False, 'language': 'python', 'style': 'friendly'}]
## Using ModelSerializers
@@ -307,7 +307,7 @@ Quit out of the shell...
Validating models...
0 errors found
- Django version 4.0, using settings 'tutorial.settings'
+ Django version 5.0, using settings 'tutorial.settings'
Starting Development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
@@ -321,42 +321,50 @@ You can install httpie using pip:
Finally, we can get a list of all of the snippets:
- http http://127.0.0.1:8000/snippets/
+ http http://127.0.0.1:8000/snippets/ --unsorted
HTTP/1.1 200 OK
...
[
- {
- "id": 1,
- "title": "",
- "code": "foo = \"bar\"\n",
- "linenos": false,
- "language": "python",
- "style": "friendly"
- },
- {
+ {
+ "id": 1,
+ "title": "",
+ "code": "foo = \"bar\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 2,
+ "title": "",
+ "code": "print(\"hello, world\")\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 3,
+ "title": "",
+ "code": "print(\"hello, world\")",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
+ ]
+
+Or we can get a particular snippet by referencing its id:
+
+ http http://127.0.0.1:8000/snippets/2/ --unsorted
+
+ HTTP/1.1 200 OK
+ ...
+ {
"id": 2,
"title": "",
"code": "print(\"hello, world\")\n",
"linenos": false,
"language": "python",
"style": "friendly"
- }
- ]
-
-Or we can get a particular snippet by referencing its id:
-
- http http://127.0.0.1:8000/snippets/2/
-
- HTTP/1.1 200 OK
- ...
- {
- "id": 2,
- "title": "",
- "code": "print(\"hello, world\")\n",
- "linenos": false,
- "language": "python",
- "style": "friendly"
}
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
@@ -371,7 +379,6 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
[quickstart]: quickstart.md
[repo]: https://github.com/encode/rest-framework-tutorial
-[sandbox]: https://restframework.herokuapp.com/
[venv]: https://docs.python.org/3/library/venv.html
[tut-2]: 2-requests-and-responses.md
[httpie]: https://github.com/httpie/httpie#installation
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 7b46a44e6..a140dbce0 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -15,7 +15,6 @@ Create a new Django project named `tutorial`, then start a new app called `quick
source env/bin/activate # On Windows use `env\Scripts\activate`
# Install Django and Django REST framework into the virtual environment
- pip install django
pip install djangorestframework
# Set up a new project with a single application
diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css
index 7006f2a66..dfde26293 100644
--- a/docs_theme/css/default.css
+++ b/docs_theme/css/default.css
@@ -439,3 +439,17 @@ ul.sponsor {
display: inline-block !important;
}
+/* admonition */
+.admonition {
+ border: .075rem solid #448aff;
+ border-radius: .2rem;
+ margin: 1.5625em 0;
+ padding: 0 .6rem;
+}
+.admonition-title {
+ background: #448aff1a;
+ font-weight: 700;
+ margin: 0 -.6rem 1em;
+ padding: 0.4rem 0.6rem;
+}
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 79831fe95..a031dd69b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,6 +9,7 @@ theme:
custom_dir: docs_theme
markdown_extensions:
+ - admonition
- toc:
anchorlink: True
diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt
index 25f5121f2..2cf936ef3 100644
--- a/requirements/requirements-documentation.txt
+++ b/requirements/requirements-documentation.txt
@@ -1,6 +1,5 @@
# MkDocs to build our documentation.
-mkdocs==1.2.4
-jinja2>=2.10,<3.1.0 # contextfilter has been renamed
+mkdocs==1.6.0
# pylinkvalidator to check for broken links in documentation.
pylinkvalidator==0.3
diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
index e54100f52..bac597c95 100644
--- a/requirements/requirements-optionals.txt
+++ b/requirements/requirements-optionals.txt
@@ -6,5 +6,5 @@ django-guardian>=2.4.0,<2.5
inflection==0.5.1
markdown>=3.3.7
psycopg2-binary>=2.9.5,<2.10
-pygments>=2.12.0,<2.14.0
+pygments~=2.17.0
pyyaml>=5.3.1,<5.4
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 45ff90980..636f0c8ad 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -7,10 +7,8 @@ ______ _____ _____ _____ __
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
"""
-import django
-
__title__ = 'Django REST framework'
-__version__ = '3.15.0'
+__version__ = '3.15.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 3-Clause'
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
@@ -25,11 +23,7 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
ISO_8601 = 'iso-8601'
-if django.VERSION < (3, 2):
- default_app_config = 'rest_framework.apps.RestFrameworkConfig'
-
-
-class RemovedInDRF315Warning(DeprecationWarning):
+class RemovedInDRF316Warning(DeprecationWarning):
pass
diff --git a/rest_framework/authtoken/__init__.py b/rest_framework/authtoken/__init__.py
index 285fe15c6..e69de29bb 100644
--- a/rest_framework/authtoken/__init__.py
+++ b/rest_framework/authtoken/__init__.py
@@ -1,4 +0,0 @@
-import django
-
-if django.VERSION < (3, 2):
- default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig'
diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py
index 163328eb0..eabb8fca8 100644
--- a/rest_framework/authtoken/admin.py
+++ b/rest_framework/authtoken/admin.py
@@ -28,7 +28,6 @@ class TokenAdmin(admin.ModelAdmin):
search_help_text = _('Username')
ordering = ('-created',)
actions = None # Actions not compatible with mapped IDs.
- autocomplete_fields = ("user",)
def get_changelist(self, request, **kwargs):
return TokenChangeList
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 472b8ad24..27c5632be 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -46,6 +46,12 @@ try:
except ImportError:
yaml = None
+# inflection is optional
+try:
+ import inflection
+except ImportError:
+ inflection = None
+
# requests is optional
try:
@@ -145,30 +151,6 @@ else:
return False
-if django.VERSION >= (4, 2):
- # Django 4.2+: use the stock parse_header_parameters function
- # Note: Django 4.1 also has an implementation of parse_header_parameters
- # which is slightly different from the one in 4.2, it needs
- # the compatibility shim as well.
- from django.utils.http import parse_header_parameters
-else:
- # Django <= 4.1: create a compatibility shim for parse_header_parameters
- from django.http.multipartparser import parse_header
-
- def parse_header_parameters(line):
- # parse_header works with bytes, but parse_header_parameters
- # works with strings. Call encode to convert the line to bytes.
- main_value_pair, params = parse_header(line.encode())
- return main_value_pair, {
- # parse_header will convert *some* values to string.
- # parse_header_parameters converts *all* values to string.
- # Make sure all values are converted by calling decode on
- # any remaining non-string values.
- k: v if isinstance(v, str) else v.decode()
- for k, v in params.items()
- }
-
-
if django.VERSION >= (5, 1):
# Django 5.1+: use the stock ip_address_validators function
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index bc20fcaa3..09f111102 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -144,30 +144,17 @@ class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Invalid input.')
default_code = 'invalid'
- default_params = {}
- def __init__(self, detail=None, code=None, params=None):
+ def __init__(self, detail=None, code=None):
if detail is None:
detail = self.default_detail
if code is None:
code = self.default_code
- if params is None:
- params = self.default_params
# For validation failures, we may collect many errors together,
# so the details should always be coerced to a list if not already.
- if isinstance(detail, str):
- detail = [detail % params]
- elif isinstance(detail, ValidationError):
- detail = detail.detail
- elif isinstance(detail, (list, tuple)):
- final_detail = []
- for detail_item in detail:
- if isinstance(detail_item, ValidationError):
- final_detail += detail_item.detail
- else:
- final_detail += [detail_item % params if isinstance(detail_item, str) else detail_item]
- detail = final_detail
+ if isinstance(detail, tuple):
+ detail = list(detail)
elif not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index fda656507..cbc02e2c2 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -4,9 +4,9 @@ import datetime
import decimal
import functools
import inspect
-import logging
import re
import uuid
+import warnings
from collections.abc import Mapping
from enum import Enum
@@ -44,8 +44,6 @@ from rest_framework.utils.formatting import lazy_format
from rest_framework.utils.timezone import valid_datetime
from rest_framework.validators import ProhibitSurrogateCharactersValidator
-logger = logging.getLogger("rest_framework.fields")
-
class empty:
"""
@@ -989,9 +987,9 @@ class DecimalField(Field):
self.min_value = min_value
if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
- logger.warning("max_value in DecimalField should be Decimal type.")
+ warnings.warn("max_value should be a Decimal instance.")
if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal):
- logger.warning("min_value in DecimalField should be Decimal type.")
+ warnings.warn("min_value should be a Decimal instance.")
if self.max_digits is not None and self.decimal_places is not None:
self.max_whole_digits = self.max_digits - self.decimal_places
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 86effe24e..3f4730da8 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -21,18 +21,20 @@ from rest_framework.settings import api_settings
def search_smart_split(search_terms):
- """generator that first splits string by spaces, leaving quoted phrases together,
- then it splits non-quoted phrases by commas.
- """
+ """Returns sanitized search terms as a list."""
+ split_terms = []
for term in smart_split(search_terms):
# trim commas to avoid bad matching for quoted phrases
term = term.strip(',')
if term.startswith(('"', "'")) and term[0] == term[-1]:
# quoted phrases are kept together without any other split
- yield unescape_string_literal(term)
+ split_terms.append(unescape_string_literal(term))
else:
# non-quoted tokens are split by comma, keeping only non-empty ones
- yield from (sub_term.strip() for sub_term in term.split(',') if sub_term)
+ for sub_term in term.split(','):
+ if sub_term:
+ split_terms.append(sub_term.strip())
+ return split_terms
class BaseFilterBackend:
@@ -85,7 +87,8 @@ class SearchFilter(BaseFilterBackend):
"""
value = request.query_params.get(self.search_param, '')
field = CharField(trim_whitespace=False, allow_blank=True)
- return field.run_validation(value)
+ cleaned_value = field.run_validation(value)
+ return search_smart_split(cleaned_value)
def construct_search(self, field_name, queryset):
lookup = self.lookup_prefixes.get(field_name[0])
@@ -111,10 +114,6 @@ class SearchFilter(BaseFilterBackend):
if hasattr(field, "path_infos"):
# Update opts to follow the relation.
opts = field.path_infos[-1].to_opts
- # django < 4.1
- elif hasattr(field, 'get_path_info'):
- # Update opts to follow the relation.
- opts = field.get_path_info()[-1].to_opts
# Otherwise, use the field with icontains.
lookup = 'icontains'
return LOOKUP_SEP.join([field_name, lookup])
@@ -163,7 +162,7 @@ class SearchFilter(BaseFilterBackend):
reduce(
operator.or_,
(models.Q(**{orm_lookup: term}) for orm_lookup in orm_lookups)
- ) for term in search_smart_split(search_terms)
+ ) for term in search_terms
)
queryset = queryset.filter(reduce(operator.and_, conditions))
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index fd0f4e163..364ca5b14 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -11,7 +11,6 @@ from django.http import Http404
from django.utils.encoding import force_str
from rest_framework import exceptions, serializers
-from rest_framework.fields import empty
from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict
@@ -150,7 +149,4 @@ class SimpleMetadata(BaseMetadata):
for choice_value, choice_name in field.choices.items()
]
- if getattr(field, 'default', None) and field.default != empty and not callable(field.default):
- field_info['default'] = field.default
-
return field_info
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 6ac6366c7..7fa8947cb 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -4,8 +4,6 @@ Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
-from django.db.models.query import prefetch_related_objects
-
from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings
@@ -69,13 +67,10 @@ class UpdateModelMixin:
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
- queryset = self.filter_queryset(self.get_queryset())
- if queryset._prefetch_related_lookups:
+ if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
- # forcibly invalidate the prefetch cache on the instance,
- # and then re-prefetch related objects
+ # forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
- prefetch_related_objects([instance], *queryset._prefetch_related_lookups)
return Response(serializer.data)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 2b20e76af..a543ceeb5 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -11,7 +11,6 @@ from urllib import parse
from django.core.paginator import InvalidPage
from django.core.paginator import Paginator as DjangoPaginator
-from django.db.models import Q
from django.template import loader
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
@@ -631,7 +630,7 @@ class CursorPagination(BasePagination):
queryset = queryset.order_by(*self.ordering)
# If we have a cursor with a fixed position then filter by that.
- if str(current_position) != 'None':
+ if current_position is not None:
order = self.ordering[0]
is_reversed = order.startswith('-')
order_attr = order.lstrip('-')
@@ -642,12 +641,7 @@ class CursorPagination(BasePagination):
else:
kwargs = {order_attr + '__gt': current_position}
- filter_query = Q(**kwargs)
- # If some records contain a null for the ordering field, don't lose them.
- # When reverse ordering, nulls will come last and need to be included.
- if (reverse and not is_reversed) or is_reversed:
- filter_query |= Q(**{order_attr + '__isnull': True})
- queryset = queryset.filter(filter_query)
+ queryset = queryset.filter(**kwargs)
# If we have an offset cursor then offset the entire page by that amount.
# We also always fetch an extra item in order to determine if there is a
@@ -720,7 +714,7 @@ class CursorPagination(BasePagination):
# The item in this position and the item following it
# have different positions. We can use this position as
# our marker.
- has_item_with_unique_position = position is not None
+ has_item_with_unique_position = True
break
# The item in this position has the same position as the item
@@ -773,7 +767,7 @@ class CursorPagination(BasePagination):
# The item in this position and the item following it
# have different positions. We can use this position as
# our marker.
- has_item_with_unique_position = position is not None
+ has_item_with_unique_position = True
break
# The item in this position has the same position as the item
@@ -896,7 +890,7 @@ class CursorPagination(BasePagination):
attr = instance[field_name]
else:
attr = getattr(instance, field_name)
- return None if attr is None else str(attr)
+ return str(attr)
def get_paginated_response(self, data):
return Response({
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index f0fd2b884..0e8e4bcb8 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -15,9 +15,9 @@ from django.http.multipartparser import ChunkIter
from django.http.multipartparser import \
MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError
+from django.utils.http import parse_header_parameters
from rest_framework import renderers
-from rest_framework.compat import parse_header_parameters
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.utils import json
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 8fb4569cb..7c15eca58 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -54,6 +54,9 @@ class OperandHolder(OperationHolderMixin):
self.op2_class == other.op2_class
)
+ def __hash__(self):
+ return hash((self.operator_class, self.op1_class, self.op2_class))
+
class AND:
def __init__(self, op1, op2):
@@ -186,9 +189,9 @@ class DjangoModelPermissions(BasePermission):
# Override this if you need to also provide 'view' permissions,
# or if you want to provide custom permission codes.
perms_map = {
- 'GET': ['%(app_label)s.view_%(model_name)s'],
+ 'GET': [],
'OPTIONS': [],
- 'HEAD': ['%(app_label)s.view_%(model_name)s'],
+ 'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
@@ -239,13 +242,8 @@ class DjangoModelPermissions(BasePermission):
queryset = self._queryset(view)
perms = self.get_required_permissions(request.method, queryset.model)
- change_perm = self.get_required_permissions('PUT', queryset.model)
- user = request.user
- if request.method == 'GET':
- return user.has_perms(perms) or user.has_perms(change_perm)
-
- return user.has_perms(perms)
+ return request.user.has_perms(perms)
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index db1fdd128..ea73c6657 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -19,12 +19,13 @@ from django.core.paginator import Page
from django.template import engines, loader
from django.urls import NoReverseMatch
from django.utils.html import mark_safe
+from django.utils.http import parse_header_parameters
from django.utils.safestring import SafeString
from rest_framework import VERSION, exceptions, serializers, status
from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
- parse_header_parameters, pygments_css, yaml
+ pygments_css, yaml
)
from rest_framework.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 93109226d..b29e64d16 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -16,9 +16,9 @@ from django.conf import settings
from django.http import HttpRequest, QueryDict
from django.http.request import RawPostDataException
from django.utils.datastructures import MultiValueDict
+from django.utils.http import parse_header_parameters
from rest_framework import exceptions
-from rest_framework.compat import parse_header_parameters
from rest_framework.settings import api_settings
@@ -422,13 +422,6 @@ class Request:
except AttributeError:
return self.__getattribute__(attr)
- @property
- def DATA(self):
- raise NotImplementedError(
- '`request.DATA` has been deprecated in favor of `request.data` '
- 'since version 3.0, and has been fully removed as of version 3.2.'
- )
-
@property
def POST(self):
# Ensure that request.POST uses our request parsing.
@@ -447,13 +440,6 @@ class Request:
self._load_data_and_files()
return self._files
- @property
- def QUERY_PARAMS(self):
- raise NotImplementedError(
- '`request.QUERY_PARAMS` has been deprecated in favor of `request.query_params` '
- 'since version 3.0, and has been fully removed as of version 3.2.'
- )
-
def force_plaintext_errors(self, value):
# Hack to allow our exception handler to force choice of
# plaintext or html error responses.
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index c154494e2..f35106fe5 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -12,9 +12,9 @@ from django.db import models
from django.utils.encoding import force_str
from rest_framework import (
- RemovedInDRF315Warning, exceptions, renderers, serializers
+ RemovedInDRF316Warning, 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.settings import api_settings
@@ -247,9 +247,8 @@ class AutoSchema(ViewInspector):
name = name[:-len(action)]
if action == 'list':
- from inflection import pluralize
-
- name = pluralize(name)
+ assert inflection, '`inflection` must be installed for OpenAPI schema support.'
+ name = inflection.pluralize(name)
return name
@@ -726,7 +725,7 @@ class AutoSchema(ViewInspector):
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.15.",
- RemovedInDRF315Warning, stacklevel=2
+ "The old name will be removed in DRF v3.16.",
+ RemovedInDRF316Warning, stacklevel=2
)
return self.get_reference(serializer)
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index e01568cf2..dba8153b1 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -322,5 +322,5 @@ def break_long_headers(header):
when possible (are comma separated)
"""
if len(header) > 160 and ',' in header:
- header = mark_safe(' ' + ', '.join(header.split(',')))
+ header = mark_safe(' ' + ', '.join(escape(header).split(',')))
return header
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 04409f962..e939adcd7 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -3,7 +3,6 @@
import io
from importlib import import_module
-import django
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
@@ -394,19 +393,7 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
cls._override.enable()
- if django.VERSION > (4, 0):
- cls.addClassCleanup(cls._override.disable)
- cls.addClassCleanup(cleanup_url_patterns, cls)
+ cls.addClassCleanup(cls._override.disable)
+ cls.addClassCleanup(cleanup_url_patterns, cls)
super().setUpClass()
-
- if django.VERSION < (4, 0):
- @classmethod
- def tearDownClass(cls):
- super().tearDownClass()
- cls._override.disable()
-
- if hasattr(cls, '_module_urlpatterns'):
- cls._module.urlpatterns = cls._module_urlpatterns
- else:
- del cls._module.urlpatterns
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index 30bb65e0c..fc63f96fe 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -9,7 +9,6 @@ from django.db import models
from django.utils.text import capfirst
from rest_framework.compat import postgres_fields
-from rest_framework.fields import empty
from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = (
@@ -128,9 +127,6 @@ def get_field_kwargs(field_name, model_field):
kwargs['read_only'] = True
return kwargs
- if model_field.default is not None and model_field.default != empty and not callable(model_field.default):
- kwargs['default'] = model_field.default
-
if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index b9004d496..8641732f0 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -3,7 +3,7 @@ Handling of media types, as found in HTTP Content-Type and Accept headers.
See https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
"""
-from rest_framework.compat import parse_header_parameters
+from django.utils.http import parse_header_parameters
def media_type_matches(lhs, rhs):
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index a1c0ce4d7..c2764c7a4 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -119,16 +119,15 @@ class NamespaceVersioning(BaseVersioning):
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
- if resolver_match is not None and resolver_match.namespace:
- # Allow for possibly nested namespaces.
- possible_versions = resolver_match.namespace.split(':')
- for version in possible_versions:
- if self.is_allowed_version(version):
- return version
+ if resolver_match is None or not resolver_match.namespace:
+ return self.default_version
- if not self.is_allowed_version(self.default_version):
- raise exceptions.NotFound(self.invalid_version_message)
- return self.default_version
+ # Allow for possibly nested namespaces.
+ possible_versions = resolver_match.namespace.split(':')
+ for version in possible_versions:
+ if self.is_allowed_version(version):
+ return version
+ raise exceptions.NotFound(self.invalid_version_message)
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
diff --git a/setup.cfg b/setup.cfg
index e7e288816..459238836 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,7 +3,7 @@ license_files = LICENSE.md
[tool:pytest]
addopts=--tb=short --strict-markers -ra
-testspath = tests
+testpaths = tests
filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning
[flake8]
diff --git a/setup.py b/setup.py
index 40898b6c1..568909bbc 100755
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
import os
import re
import shutil
@@ -8,7 +7,7 @@ from io import open
from setuptools import find_packages, setup
CURRENT_PYTHON = sys.version_info[:2]
-REQUIRED_PYTHON = (3, 6)
+REQUIRED_PYTHON = (3, 8)
# This check and everything above must remain compatible with Python 2.7.
if CURRENT_PYTHON < REQUIRED_PYTHON:
@@ -83,18 +82,13 @@ setup(
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
packages=find_packages(exclude=['tests*']),
include_package_data=True,
- install_requires=["django>=3.0", 'backports.zoneinfo;python_version<"3.9"'],
- python_requires=">=3.6",
+ install_requires=["django>=4.2", 'backports.zoneinfo;python_version<"3.9"'],
+ python_requires=">=3.8",
zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
- 'Framework :: Django :: 3.0',
- 'Framework :: Django :: 3.1',
- 'Framework :: Django :: 3.2',
- 'Framework :: Django :: 4.0',
- 'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Intended Audience :: Developers',
@@ -102,8 +96,6 @@ setup(
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py
index 22e837ef4..2f05ce7d1 100644
--- a/tests/authentication/test_authentication.py
+++ b/tests/authentication/test_authentication.py
@@ -1,6 +1,5 @@
import base64
-import django
import pytest
from django.conf import settings
from django.contrib.auth.models import User
@@ -235,21 +234,13 @@ class SessionAuthTests(TestCase):
Ensure POSTing form over session authentication with CSRF token succeeds.
Regression test for #6088
"""
- # Remove this shim when dropping support for Django 3.0.
- if django.VERSION < (3, 1):
- from django.middleware.csrf import _get_new_csrf_token
- else:
- from django.middleware.csrf import (
- _get_new_csrf_string, _mask_cipher_secret
- )
-
- def _get_new_csrf_token():
- return _mask_cipher_secret(_get_new_csrf_string())
-
self.csrf_client.login(username=self.username, password=self.password)
# Set the csrf_token cookie so that CsrfViewMiddleware._get_token() works
- token = _get_new_csrf_token()
+ from django.middleware.csrf import (
+ _get_new_csrf_string, _mask_cipher_secret
+ )
+ token = _mask_cipher_secret(_get_new_csrf_string())
self.csrf_client.cookies[settings.CSRF_COOKIE_NAME] = token
# Post the token matching the cookie value
diff --git a/tests/conftest.py b/tests/conftest.py
index b67475d8a..01914ae77 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,8 +13,6 @@ def pytest_addoption(parser):
def pytest_configure(config):
from django.conf import settings
- # USE_L10N is deprecated, and will be removed in Django 5.0.
- use_l10n = {"USE_L10N": True} if django.VERSION < (4, 0) else {}
settings.configure(
DEBUG_PROPAGATE_EXCEPTIONS=True,
DATABASES={
@@ -64,7 +62,6 @@ def pytest_configure(config):
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.MD5PasswordHasher',
),
- **use_l10n,
)
# guardian is optional
@@ -87,10 +84,7 @@ def pytest_configure(config):
import rest_framework
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
- if django.VERSION < (4, 2):
- settings.STATICFILES_STORAGE = backend
- else:
- settings.STORAGES['staticfiles']['BACKEND'] = backend
+ settings.STORAGES['staticfiles']['BACKEND'] = backend
django.setup()
diff --git a/tests/test_description.py b/tests/test_description.py
index ecc6b9776..7fb93ed4e 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -1,155 +1,155 @@
-import pytest
-from django.test import TestCase
-
-from rest_framework.compat import apply_markdown
-from rest_framework.utils.formatting import dedent
-from rest_framework.views import APIView
-
-# We check that docstrings get nicely un-indented.
-DESCRIPTION = """an example docstring
-====================
-
-* list
-* list
-
-another header
---------------
-
- code block
-
-indented
-
-# hash style header #
-
-```json
-[{
- "alpha": 1,
- "beta": "this is a string"
-}]
-```"""
-
-
-# If markdown is installed we also test it's working
-# (and that our wrapped forces '=' to h2 and '-' to h3)
-MARKDOWN_DOCSTRING = """
an example docstring
-
-
list
-
list
-
-
another header
-
code block
-
-
indented
-
hash style header
-
[{ "alpha":1, "beta":"this is a string" }]
-
"""
-
-
-class TestViewNamesAndDescriptions(TestCase):
- def test_view_name_uses_class_name(self):
- """
- Ensure view names are based on the class name.
- """
- class MockView(APIView):
- pass
- assert MockView().get_view_name() == 'Mock'
-
- def test_view_name_uses_name_attribute(self):
- class MockView(APIView):
- name = 'Foo'
- assert MockView().get_view_name() == 'Foo'
-
- def test_view_name_uses_suffix_attribute(self):
- class MockView(APIView):
- suffix = 'List'
- assert MockView().get_view_name() == 'Mock List'
-
- def test_view_name_preferences_name_over_suffix(self):
- class MockView(APIView):
- name = 'Foo'
- suffix = 'List'
- assert MockView().get_view_name() == 'Foo'
-
- def test_view_description_uses_docstring(self):
- """Ensure view descriptions are based on the docstring."""
- class MockView(APIView):
- """an example docstring
- ====================
-
- * list
- * list
-
- another header
- --------------
-
- code block
-
- indented
-
- # hash style header #
-
- ```json
- [{
- "alpha": 1,
- "beta": "this is a string"
- }]
- ```"""
-
- assert MockView().get_view_description() == DESCRIPTION
-
- def test_view_description_uses_description_attribute(self):
- class MockView(APIView):
- description = 'Foo'
- assert MockView().get_view_description() == 'Foo'
-
- def test_view_description_allows_empty_description(self):
- class MockView(APIView):
- """Description."""
- description = ''
- assert MockView().get_view_description() == ''
-
- def test_view_description_can_be_empty(self):
- """
- Ensure that if a view has no docstring,
- then it's description is the empty string.
- """
- class MockView(APIView):
- pass
- assert MockView().get_view_description() == ''
-
- def test_view_description_can_be_promise(self):
- """
- Ensure a view may have a docstring that is actually a lazily evaluated
- class that can be converted to a string.
-
- See: https://github.com/encode/django-rest-framework/issues/1708
- """
- # use a mock object instead of gettext_lazy to ensure that we can't end
- # up with a test case string in our l10n catalog
-
- class MockLazyStr:
- def __init__(self, string):
- self.s = string
-
- def __str__(self):
- return self.s
-
- class MockView(APIView):
- __doc__ = MockLazyStr("a gettext string")
-
- assert MockView().get_view_description() == 'a gettext string'
-
- @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed")
- def test_markdown(self):
- """
- Ensure markdown to HTML works as expected.
- """
- assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
-
-
-def test_dedent_tabs():
- result = 'first string\n\nsecond string'
- assert dedent(" first string\n\n second string") == result
- assert dedent("first string\n\n second string") == result
- assert dedent("\tfirst string\n\n\tsecond string") == result
- assert dedent("first string\n\n\tsecond string") == result
+import pytest
+from django.test import TestCase
+
+from rest_framework.compat import apply_markdown
+from rest_framework.utils.formatting import dedent
+from rest_framework.views import APIView
+
+# We check that docstrings get nicely un-indented.
+DESCRIPTION = """an example docstring
+====================
+
+* list
+* list
+
+another header
+--------------
+
+ code block
+
+indented
+
+# hash style header #
+
+```json
+[{
+ "alpha": 1,
+ "beta": "this is a string"
+}]
+```"""
+
+
+# If markdown is installed we also test it's working
+# (and that our wrapped forces '=' to h2 and '-' to h3)
+MARKDOWN_DOCSTRING = """