mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 01:26:53 +03:00
Merge branch 'master' into drop-break-long-headers
This commit is contained in:
commit
2445421821
9
.github/ISSUE_TEMPLATE/1-issue.md
vendored
9
.github/ISSUE_TEMPLATE/1-issue.md
vendored
|
@ -5,6 +5,13 @@ about: Please only raise an issue if you've been advised to do so after discussi
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note: REST framework is considered feature-complete. New functionality should be implemented outside the core REST framework. For details, please check the docs: https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages
|
||||||
|
-->
|
||||||
|
|
||||||
- [ ] Raised initially as discussion #...
|
- [ ] Raised initially as discussion #...
|
||||||
- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
|
- [ ] 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.
|
- [ ] I have reduced the issue to the simplest possible case.
|
||||||
|
|
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
|
@ -14,8 +14,6 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- '3.6'
|
|
||||||
- '3.7'
|
|
||||||
- '3.8'
|
- '3.8'
|
||||||
- '3.9'
|
- '3.9'
|
||||||
- '3.10'
|
- '3.10'
|
||||||
|
@ -37,18 +35,9 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: python -m pip install --upgrade codecov tox
|
run: python -m pip install --upgrade codecov tox
|
||||||
|
|
||||||
- name: Install tox-py
|
|
||||||
if: ${{ matrix.python-version == '3.6' }}
|
|
||||||
run: python -m pip install --upgrade tox-py
|
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
- name: Run tox targets for ${{ matrix.python-version }}
|
||||||
if: ${{ matrix.python-version != '3.6' }}
|
|
||||||
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .)
|
||||||
|
|
||||||
- name: Run tox targets for ${{ matrix.python-version }}
|
|
||||||
if: ${{ matrix.python-version == '3.6' }}
|
|
||||||
run: tox --py current
|
|
||||||
|
|
||||||
- name: Run extra tox targets
|
- name: Run extra tox targets
|
||||||
if: ${{ matrix.python-version == '3.9' }}
|
if: ${{ matrix.python-version == '3.9' }}
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
*Note*: Before submitting this pull request, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests).
|
*Note*: Before submitting a code change, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests).
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
|
|
@ -53,8 +53,8 @@ Some reasons you might want to use REST framework:
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
* Python 3.6+
|
* Python 3.8+
|
||||||
* Django 5.0, 4.2, 4.1, 4.0, 3.2, 3.1, 3.0
|
* Django 5.0, 4.2
|
||||||
|
|
||||||
We **highly recommend** and only officially support the latest patch release of
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
@ -172,8 +172,6 @@ Full documentation for the project is available at [https://www.django-rest-fram
|
||||||
|
|
||||||
For questions and support, use the [REST framework discussion group][group], or `#restframework` on libera.chat IRC.
|
For questions and support, use the [REST framework discussion group][group], or `#restframework` on libera.chat IRC.
|
||||||
|
|
||||||
You may also want to [follow the author on Twitter][twitter].
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
|
|
||||||
Please see the [security policy][security-policy].
|
Please see the [security policy][security-policy].
|
||||||
|
@ -184,7 +182,6 @@ Please see the [security policy][security-policy].
|
||||||
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
|
[codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master
|
||||||
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
|
[pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg
|
||||||
[pypi]: https://pypi.org/project/djangorestframework/
|
[pypi]: https://pypi.org/project/djangorestframework/
|
||||||
[twitter]: https://twitter.com/starletdreaming
|
|
||||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||||
|
|
||||||
[funding]: https://fund.django-rest-framework.org/topics/funding/
|
[funding]: https://fund.django-rest-framework.org/topics/funding/
|
||||||
|
|
|
@ -845,8 +845,6 @@ Here's an example of how you might choose to implement multiple updates:
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = BookListSerializer
|
list_serializer_class = BookListSerializer
|
||||||
|
|
||||||
It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2.
|
|
||||||
|
|
||||||
#### Customizing ListSerializer initialization
|
#### Customizing ListSerializer initialization
|
||||||
|
|
||||||
When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
|
When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
|
|
||||||
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.
|
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
|
||||||
|
|
||||||
**Note**: At this point in it's 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.
|
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.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
@ -36,10 +34,9 @@ Our contribution process is that the [GitHub discussions page](https://github.co
|
||||||
|
|
||||||
Some tips on good potential issue reporting:
|
Some tips on good potential issue reporting:
|
||||||
|
|
||||||
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
|
* 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.
|
* 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 often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. At this point in it's lifespan we consider Django REST framework to be essentially feature-complete.
|
* 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.
|
||||||
* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
|
|
||||||
|
|
||||||
## Triaging issues
|
## Triaging issues
|
||||||
|
|
||||||
|
@ -48,8 +45,8 @@ Getting involved in triaging incoming issues is a good way to start contributing
|
||||||
* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
|
* 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?
|
* 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 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, do you agree with it, and could the feature request instead be implemented as a third party package?
|
* 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 it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
|
* 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
|
# Development
|
||||||
|
|
||||||
|
|
|
@ -1,398 +1,398 @@
|
||||||
<script>
|
<script>
|
||||||
// Imperfect, but easier to fit in with the existing docs build.
|
// Imperfect, but easier to fit in with the existing docs build.
|
||||||
// Hyperlinks should point directly to the "fund." subdomain, but this'll
|
// Hyperlinks should point directly to the "fund." subdomain, but this'll
|
||||||
// handle the nav bar links without requiring any docs build changes for the moment.
|
// handle the nav bar links without requiring any docs build changes for the moment.
|
||||||
if (window.location.hostname == "www.django-rest-framework.org") {
|
if (window.location.hostname == "www.django-rest-framework.org") {
|
||||||
window.location.replace("https://fund.django-rest-framework.org/topics/funding/");
|
window.location.replace("https://fund.django-rest-framework.org/topics/funding/");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.promo li a {
|
.promo li a {
|
||||||
float: left;
|
float: left;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 10px 30px;
|
margin: 10px 30px;
|
||||||
padding: 150px 0 0 0;
|
padding: 150px 0 0 0;
|
||||||
background-position: 0 50%;
|
background-position: 0 50%;
|
||||||
background-size: 130px auto;
|
background-size: 130px auto;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.promo li {
|
.promo li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.chart {
|
.chart {
|
||||||
background-color: #e3e3e3;
|
background-color: #e3e3e3;
|
||||||
background: -webkit-linear-gradient(top, #fff 0, #e3e3e3 100%);
|
background: -webkit-linear-gradient(top, #fff 0, #e3e3e3 100%);
|
||||||
border: 1px solid #E6E6E6;
|
border: 1px solid #E6E6E6;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);
|
box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);
|
||||||
padding: 40px 0px 5px;
|
padding: 40px 0px 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 97%;
|
width: 97%;
|
||||||
min-height: 255px;
|
min-height: 255px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 37px;
|
top: 37px;
|
||||||
margin-bottom: 20px
|
margin-bottom: 20px
|
||||||
}
|
}
|
||||||
.quantity {
|
.quantity {
|
||||||
text-align: center
|
text-align: center
|
||||||
}
|
}
|
||||||
.dollar {
|
.dollar {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -18px;
|
top: -18px;
|
||||||
}
|
}
|
||||||
.price {
|
.price {
|
||||||
font-size: 49px;
|
font-size: 49px;
|
||||||
}
|
}
|
||||||
.period {
|
.period {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -8px;
|
top: -8px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.plan-name {
|
.plan-name {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #777;
|
color: #777;
|
||||||
border-bottom: 1px solid #d5d5d5;
|
border-bottom: 1px solid #d5d5d5;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.specs {
|
.specs {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
}
|
}
|
||||||
.specs.freelancer {
|
.specs.freelancer {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
}
|
}
|
||||||
.spec {
|
.spec {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #474747;
|
color: #474747;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin-bottom: 13px;
|
margin-bottom: 13px;
|
||||||
}
|
}
|
||||||
.variable {
|
.variable {
|
||||||
color: #1FBEE7;
|
color: #1FBEE7;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
form.signup {
|
form.signup {
|
||||||
margin-top: 35px
|
margin-top: 35px
|
||||||
}
|
}
|
||||||
.clear-promo {
|
.clear-promo {
|
||||||
padding-top: 30px
|
padding-top: 30px
|
||||||
}
|
}
|
||||||
#main-content h1:first-of-type {
|
#main-content h1:first-of-type {
|
||||||
margin: 0 0 50px;
|
margin: 0 0 50px;
|
||||||
font-size: 60px;
|
font-size: 60px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
text-align: center
|
text-align: center
|
||||||
}
|
}
|
||||||
#main-content {
|
#main-content {
|
||||||
padding-top: 10px; line-height: 23px
|
padding-top: 10px; line-height: 23px
|
||||||
}
|
}
|
||||||
#main-content li {
|
#main-content li {
|
||||||
line-height: 23px
|
line-height: 23px
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
# Funding
|
# Funding
|
||||||
|
|
||||||
If you use REST framework commercially we strongly encourage you to invest in its continued development by signing up for a paid plan.
|
If you use REST framework commercially we strongly encourage you to invest in its continued development by signing up for a paid plan.
|
||||||
|
|
||||||
**We believe that collaboratively funded software can offer outstanding returns on investment, by encouraging our users to collectively share the cost of development.**
|
**We believe that collaboratively funded software can offer outstanding returns on investment, by encouraging our users to collectively share the cost of development.**
|
||||||
|
|
||||||
Signing up for a paid plan will:
|
Signing up for a paid plan will:
|
||||||
|
|
||||||
* Directly contribute to faster releases, more features, and higher quality software.
|
* Directly contribute to faster releases, more features, and higher quality software.
|
||||||
* Allow more time to be invested in documentation, issue triage, and community support.
|
* Allow more time to be invested in documentation, issue triage, and community support.
|
||||||
* Safeguard the future development of REST framework.
|
* Safeguard the future development of REST framework.
|
||||||
|
|
||||||
REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development.
|
REST framework continues to be open-source and permissively licensed, but we firmly believe it is in the commercial best-interest for users of the project to invest in its ongoing development.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What funding has enabled so far
|
## What funding has enabled so far
|
||||||
|
|
||||||
* The [3.4](https://www.django-rest-framework.org/community/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/community/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
|
* The [3.4](https://www.django-rest-framework.org/community/3.4-announcement/) and [3.5](https://www.django-rest-framework.org/community/3.5-announcement/) releases, including schema generation for both Swagger and RAML, a Python client library, a Command Line client, and addressing of a large number of outstanding issues.
|
||||||
* The [3.6](https://www.django-rest-framework.org/community/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
|
* The [3.6](https://www.django-rest-framework.org/community/3.6-announcement/) release, including JavaScript client library, and API documentation, complete with auto-generated code samples.
|
||||||
* The [3.7 release](https://www.django-rest-framework.org/community/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
|
* The [3.7 release](https://www.django-rest-framework.org/community/3.7-announcement/), made possible due to our collaborative funding model, focuses on improvements to schema generation and the interactive API documentation.
|
||||||
* The recent [3.8 release](https://www.django-rest-framework.org/community/3.8-announcement/).
|
* The recent [3.8 release](https://www.django-rest-framework.org/community/3.8-announcement/).
|
||||||
* Tom Christie, the creator of Django REST framework, working on the project full-time.
|
* Tom Christie, the creator of Django REST framework, working on the project full-time.
|
||||||
* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time.
|
* Around 80-90 issues and pull requests closed per month since Tom Christie started working on the project full-time.
|
||||||
* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship.
|
* A community & operations manager position part-time for 4 months, helping mature the business and grow sponsorship.
|
||||||
* Contracting development time for the work on the JavaScript client library and API documentation tooling.
|
* Contracting development time for the work on the JavaScript client library and API documentation tooling.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What future funding will enable
|
## What future funding will enable
|
||||||
|
|
||||||
* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries.
|
* Realtime API support, using WebSockets. This will consist of documentation and support for using REST framework together with Django Channels, plus integrating WebSocket support into the client libraries.
|
||||||
* Better authentication defaults, possibly bringing JWT & CORS support into the core package.
|
* Better authentication defaults, possibly bringing JWT & CORS support into the core package.
|
||||||
* Securing the community & operations manager position long-term.
|
* Securing the community & operations manager position long-term.
|
||||||
* Opening up and securing a part-time position to focus on ticket triage and resolution.
|
* Opening up and securing a part-time position to focus on ticket triage and resolution.
|
||||||
* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation.
|
* Paying for development time on building API client libraries in a range of programming languages. These would be integrated directly into the upcoming API documentation.
|
||||||
|
|
||||||
Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project.
|
Sign up for a paid plan today, and help ensure that REST framework becomes a sustainable, full-time funded project.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What our sponsors and users say
|
## What our sponsors and users say
|
||||||
|
|
||||||
> As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem.
|
> As a developer, Django REST framework feels like an obvious and natural extension to all the great things that make up Django and it's community. Getting started is easy while providing simple abstractions which makes it flexible and customizable. Contributing and supporting Django REST framework helps ensure its future and one way or another it also helps Django, and the Python ecosystem.
|
||||||
>
|
>
|
||||||
> — José Padilla, Django REST framework contributor
|
> — José Padilla, Django REST framework contributor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> The number one feature of the Python programming language is its community. Such a community is only possible because of the Open Source nature of the language and all the culture that comes from it. Building great Open Source projects require great minds. Given that, we at Vinta are not only proud to sponsor the team behind DRF but we also recognize the ROI that comes from it.
|
> The number one feature of the Python programming language is its community. Such a community is only possible because of the Open Source nature of the language and all the culture that comes from it. Building great Open Source projects require great minds. Given that, we at Vinta are not only proud to sponsor the team behind DRF but we also recognize the ROI that comes from it.
|
||||||
>
|
>
|
||||||
> — Filipe Ximenes, Vinta Software
|
> — Filipe Ximenes, Vinta Software
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> It's really awesome that this project continues to endure. The code base is top notch and the maintainers are committed to the highest level of quality.
|
> It's really awesome that this project continues to endure. The code base is top notch and the maintainers are committed to the highest level of quality.
|
||||||
DRF is one of the core reasons why Django is top choice among web frameworks today. In my opinion, it sets the standard for rest frameworks for the development community at large.
|
DRF is one of the core reasons why Django is top choice among web frameworks today. In my opinion, it sets the standard for rest frameworks for the development community at large.
|
||||||
>
|
>
|
||||||
> — Andrew Conti, Django REST framework user
|
> — Andrew Conti, Django REST framework user
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Individual plan
|
## Individual plan
|
||||||
|
|
||||||
This subscription is recommended for individuals with an interest in seeing REST framework continue to improve.
|
This subscription is recommended for individuals with an interest in seeing REST framework continue to improve.
|
||||||
|
|
||||||
If you are using REST framework as a full-time employee, consider recommending that your company takes out a [corporate plan](#corporate-plans).
|
If you are using REST framework as a full-time employee, consider recommending that your company takes out a [corporate plan](#corporate-plans).
|
||||||
|
|
||||||
<div class="pricing">
|
<div class="pricing">
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="chart first">
|
<div class="chart first">
|
||||||
<div class="quantity">
|
<div class="quantity">
|
||||||
<span class="dollar">{{ symbol }}</span>
|
<span class="dollar">{{ symbol }}</span>
|
||||||
<span class="price">{{ rates.personal1 }}</span>
|
<span class="price">{{ rates.personal1 }}</span>
|
||||||
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-name">Individual</div>
|
<div class="plan-name">Individual</div>
|
||||||
<div class="specs freelancer">
|
<div class="specs freelancer">
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
Support ongoing development
|
Support ongoing development
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
Credited on the site
|
Credited on the site
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="signup" action="/signup/{{ currency }}-{{ rates.personal1 }}/" method="POST">
|
<form class="signup" action="/signup/{{ currency }}-{{ rates.personal1 }}/" method="POST">
|
||||||
<script
|
<script
|
||||||
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||||
data-key="{{ stripe_public }}"
|
data-key="{{ stripe_public }}"
|
||||||
data-amount="{{ stripe_amounts.personal1 }}"
|
data-amount="{{ stripe_amounts.personal1 }}"
|
||||||
data-name="Django REST framework"
|
data-name="Django REST framework"
|
||||||
data-description="Individual"
|
data-description="Individual"
|
||||||
data-currency="{{ currency }}"
|
data-currency="{{ currency }}"
|
||||||
data-allow-remember-me=false
|
data-allow-remember-me=false
|
||||||
data-billing-address=true
|
data-billing-address=true
|
||||||
data-label='Sign up'
|
data-label='Sign up'
|
||||||
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="clear: both; padding-top: 50px"></div>
|
<div style="clear: both; padding-top: 50px"></div>
|
||||||
|
|
||||||
*Billing is monthly and you can cancel at any time.*
|
*Billing is monthly and you can cancel at any time.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Corporate plans
|
## Corporate plans
|
||||||
|
|
||||||
These subscriptions are recommended for companies and organizations using REST framework either publicly or privately.
|
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**.
|
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.
|
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.
|
||||||
|
|
||||||
<div class="pricing">
|
<div class="pricing">
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="chart first">
|
<div class="chart first">
|
||||||
<div class="quantity">
|
<div class="quantity">
|
||||||
<span class="dollar">{{ symbol }}</span>
|
<span class="dollar">{{ symbol }}</span>
|
||||||
<span class="price">{{ rates.corporate1 }}</span>
|
<span class="price">{{ rates.corporate1 }}</span>
|
||||||
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-name">Basic</div>
|
<div class="plan-name">Basic</div>
|
||||||
<div class="specs startup">
|
<div class="specs startup">
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
Support ongoing development
|
Support ongoing development
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Funding page</span> ad placement
|
<span class="variable">Funding page</span> ad placement
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate1 }}/" method="POST">
|
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate1 }}/" method="POST">
|
||||||
<script
|
<script
|
||||||
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||||
data-key="{{ stripe_public }}"
|
data-key="{{ stripe_public }}"
|
||||||
data-amount="{{ stripe_amounts.corporate1 }}"
|
data-amount="{{ stripe_amounts.corporate1 }}"
|
||||||
data-name="Django REST framework"
|
data-name="Django REST framework"
|
||||||
data-description="Basic"
|
data-description="Basic"
|
||||||
data-currency="{{ currency }}"
|
data-currency="{{ currency }}"
|
||||||
data-allow-remember-me=false
|
data-allow-remember-me=false
|
||||||
data-billing-address=true
|
data-billing-address=true
|
||||||
data-label='Sign up'
|
data-label='Sign up'
|
||||||
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<div class="quantity">
|
<div class="quantity">
|
||||||
<span class="dollar">{{ symbol }}</span>
|
<span class="dollar">{{ symbol }}</span>
|
||||||
<span class="price">{{ rates.corporate2 }}</span>
|
<span class="price">{{ rates.corporate2 }}</span>
|
||||||
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-name">Professional</div>
|
<div class="plan-name">Professional</div>
|
||||||
<div class="specs">
|
<div class="specs">
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
Support ongoing development
|
Support ongoing development
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Sidebar</span> ad placement
|
<span class="variable">Sidebar</span> ad placement
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Priority support</span> for your engineers
|
<span class="variable">Priority support</span> for your engineers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate2 }}/" method="POST">
|
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate2 }}/" method="POST">
|
||||||
<script
|
<script
|
||||||
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||||
data-key="{{ stripe_public }}"
|
data-key="{{ stripe_public }}"
|
||||||
data-amount="{{ stripe_amounts.corporate2 }}"
|
data-amount="{{ stripe_amounts.corporate2 }}"
|
||||||
data-name="Django REST framework"
|
data-name="Django REST framework"
|
||||||
data-description="Professional"
|
data-description="Professional"
|
||||||
data-currency="{{ currency }}"
|
data-currency="{{ currency }}"
|
||||||
data-allow-remember-me=false
|
data-allow-remember-me=false
|
||||||
data-billing-address=true
|
data-billing-address=true
|
||||||
data-label='Sign up'
|
data-label='Sign up'
|
||||||
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="chart last">
|
<div class="chart last">
|
||||||
<div class="quantity">
|
<div class="quantity">
|
||||||
<span class="dollar">{{ symbol }}</span>
|
<span class="dollar">{{ symbol }}</span>
|
||||||
<span class="price">{{ rates.corporate3 }}</span>
|
<span class="price">{{ rates.corporate3 }}</span>
|
||||||
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
<span class="period">/month{% if vat %} +VAT{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-name">Premium</div>
|
<div class="plan-name">Premium</div>
|
||||||
<div class="specs">
|
<div class="specs">
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
Support ongoing development
|
Support ongoing development
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Homepage</span> ad placement
|
<span class="variable">Homepage</span> ad placement
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Sidebar</span> ad placement
|
<span class="variable">Sidebar</span> ad placement
|
||||||
</div>
|
</div>
|
||||||
<div class="spec">
|
<div class="spec">
|
||||||
<span class="variable">Priority support</span> for your engineers
|
<span class="variable">Priority support</span> for your engineers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate3 }}/" method="POST">
|
<form class="signup" action="/signup/{{ currency }}-{{ rates.corporate3 }}/" method="POST">
|
||||||
<script
|
<script
|
||||||
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
||||||
data-key="{{ stripe_public }}"
|
data-key="{{ stripe_public }}"
|
||||||
data-amount="{{ stripe_amounts.corporate3 }}"
|
data-amount="{{ stripe_amounts.corporate3 }}"
|
||||||
data-name="Django REST framework"
|
data-name="Django REST framework"
|
||||||
data-description="Premium"
|
data-description="Premium"
|
||||||
data-currency="{{ currency }}"
|
data-currency="{{ currency }}"
|
||||||
data-allow-remember-me=false
|
data-allow-remember-me=false
|
||||||
data-billing-address=true
|
data-billing-address=true
|
||||||
data-label='Sign up'
|
data-label='Sign up'
|
||||||
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
data-panel-label='Sign up - {% verbatim %}{{amount}}{% endverbatim %}/mo'>
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="clear: both; padding-top: 50px"></div>
|
<div style="clear: both; padding-top: 50px"></div>
|
||||||
|
|
||||||
*Billing is monthly and you can cancel at any time.*
|
*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.
|
Once you've signed up, we will contact you via email and arrange your ad placements on the site.
|
||||||
|
|
||||||
For further enquires please contact <a href=mailto:funding@django-rest-framework.org>funding@django-rest-framework.org</a>.
|
For further enquires please contact <a href=mailto:funding@django-rest-framework.org>funding@django-rest-framework.org</a>.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Accountability
|
## 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.
|
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.
|
||||||
|
|
||||||
<!-- Begin MailChimp Signup Form -->
|
<!-- Begin MailChimp Signup Form -->
|
||||||
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
|
#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; }
|
||||||
/* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
|
/* Add your own MailChimp form style overrides in your site stylesheet or in this style block.
|
||||||
We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
|
We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */
|
||||||
</style>
|
</style>
|
||||||
<div id="mc_embed_signup">
|
<div id="mc_embed_signup">
|
||||||
<form action="//encode.us13.list-manage.com/subscribe/post?u=b6b66bb5e4c7cb484a85c8dd7&id=e382ef68ef" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
|
<form action="//encode.us13.list-manage.com/subscribe/post?u=b6b66bb5e4c7cb484a85c8dd7&id=e382ef68ef" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
|
||||||
<div id="mc_embed_signup_scroll">
|
<div id="mc_embed_signup_scroll">
|
||||||
<h2>Stay up to date, with our monthly progress reports...</h2>
|
<h2>Stay up to date, with our monthly progress reports...</h2>
|
||||||
<div class="mc-field-group">
|
<div class="mc-field-group">
|
||||||
<label for="mce-EMAIL">Email Address </label>
|
<label for="mce-EMAIL">Email Address </label>
|
||||||
<input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL">
|
<input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL">
|
||||||
</div>
|
</div>
|
||||||
<div id="mce-responses" class="clear">
|
<div id="mce-responses" class="clear">
|
||||||
<div class="response" id="mce-error-response" style="display:none"></div>
|
<div class="response" id="mce-error-response" style="display:none"></div>
|
||||||
<div class="response" id="mce-success-response" style="display:none"></div>
|
<div class="response" id="mce-success-response" style="display:none"></div>
|
||||||
</div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
|
</div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
|
||||||
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_b6b66bb5e4c7cb484a85c8dd7_e382ef68ef" tabindex="-1" value=""></div>
|
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_b6b66bb5e4c7cb484a85c8dd7_e382ef68ef" tabindex="-1" value=""></div>
|
||||||
<div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
|
<div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script type='text/javascript' src='//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js'></script><script type='text/javascript'>(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[0]='EMAIL';ftypes[0]='email';fnames[1]='FNAME';ftypes[1]='text';fnames[2]='LNAME';ftypes[2]='text';}(jQuery));var $mcj = jQuery.noConflict(true);</script>
|
<script type='text/javascript' src='//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js'></script><script type='text/javascript'>(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[0]='EMAIL';ftypes[0]='email';fnames[1]='FNAME';ftypes[1]='text';fnames[2]='LNAME';ftypes[2]='text';}(jQuery));var $mcj = jQuery.noConflict(true);</script>
|
||||||
<!--End mc_embed_signup-->
|
<!--End mc_embed_signup-->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frequently asked questions
|
## Frequently asked questions
|
||||||
|
|
||||||
**Q: Can you issue monthly invoices?**
|
**Q: Can you issue monthly invoices?**
|
||||||
A: Yes, we are happy to issue monthly invoices. Please just <a href=mailto:funding@django-rest-framework.org>email us</a> and let us know who to issue the invoice to (name and address) and which email address to send it to each month.
|
A: Yes, we are happy to issue monthly invoices. Please just <a href=mailto:funding@django-rest-framework.org>email us</a> 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?**
|
**Q: Does sponsorship include VAT?**
|
||||||
A: Sponsorship is VAT exempt.
|
A: Sponsorship is VAT exempt.
|
||||||
|
|
||||||
**Q: Do I have to sign up for a certain time period?**
|
**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.
|
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?**
|
**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.
|
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?**
|
**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.
|
A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Our sponsors
|
## Our sponsors
|
||||||
|
|
||||||
<div id="fundingInclude"></div>
|
<div id="fundingInclude"></div>
|
||||||
|
|
||||||
<script src="https://fund.django-rest-framework.org/funding_include.js"></script>
|
<script src="https://fund.django-rest-framework.org/funding_include.js"></script>
|
||||||
|
|
|
@ -13,55 +13,13 @@ The aim is to ensure that the project has a high
|
||||||
|
|
||||||
## Maintenance team
|
## 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/)
|
#### Responsibilities
|
||||||
* [@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
|
|
||||||
|
|
||||||
Team members have the following 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.
|
* Each issue/pull request should have exactly one label once triaged.
|
||||||
* Search for un-triaged issues with [is:open no:label][un-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
|
## Release process
|
||||||
|
|
||||||
The release manager is selected on every quarterly maintenance cycle.
|
* The release manager is selected by `@tomchristie`.
|
||||||
|
* The release manager will then have the maintainer role added to PyPI package.
|
||||||
* The manager should be selected by `@tomchristie`.
|
|
||||||
* The 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.
|
* 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.
|
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,7 +152,7 @@ If `@tomchristie` ceases to participate in the project then `@j4mie` has respons
|
||||||
|
|
||||||
The following issues still need to be addressed:
|
The following issues still need to be addressed:
|
||||||
|
|
||||||
* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.
|
* 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 of the [mailing list][mailing-list] and IRC channel.
|
||||||
* Document ownership and management of the security mailing list.
|
* Document ownership and management of the security mailing list.
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
## Versioning
|
## 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
|
## Deprecation policy
|
||||||
|
|
||||||
|
@ -36,6 +38,15 @@ You can determine your currently installed version using `pip show`:
|
||||||
|
|
||||||
## 3.15.x series
|
## 3.15.x series
|
||||||
|
|
||||||
|
### 3.15.2
|
||||||
|
|
||||||
|
**Date**: 14th June 2024
|
||||||
|
|
||||||
|
* Fix potential XSS vulnerability in browsable API. [#9435](https://github.com/encode/django-rest-framework/pull/9157)
|
||||||
|
* 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)
|
||||||
|
|
||||||
### 3.15.1
|
### 3.15.1
|
||||||
|
|
||||||
Date: 22nd March 2024
|
Date: 22nd March 2024
|
||||||
|
|
|
@ -86,8 +86,8 @@ continued development by **[signing up for a paid plan][funding]**.
|
||||||
|
|
||||||
REST framework requires the following:
|
REST framework requires the following:
|
||||||
|
|
||||||
* Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11)
|
* Django (4.2, 5.0)
|
||||||
* Django (3.0, 3.1, 3.2, 4.0, 4.1, 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
|
We **highly recommend** and only officially support the latest patch release of
|
||||||
each Python and Django series.
|
each Python and Django series.
|
||||||
|
@ -95,8 +95,8 @@ each Python and Django series.
|
||||||
The following packages are optional:
|
The following packages are optional:
|
||||||
|
|
||||||
* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
|
* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
|
||||||
* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
|
* [Markdown][markdown] (3.3.0+) - Markdown support for the browsable API.
|
||||||
* [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing.
|
* [Pygments][pygments] (2.7.0+) - Add syntax highlighting to Markdown processing.
|
||||||
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
* [django-filter][django-filter] (1.0.1+) - Filtering support.
|
||||||
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ Can't wait to get started? The [quickstart guide][quickstart] is the fastest way
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See the [Contribution guidelines][contributing] for information on how to clone
|
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.
|
Framework.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
|
@ -1,220 +1,220 @@
|
||||||
# HTML & Forms
|
# 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.
|
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
|
## Rendering HTML
|
||||||
|
|
||||||
In order to return HTML responses you'll need to use either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
|
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 `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.
|
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.
|
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:
|
Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template:
|
||||||
|
|
||||||
**views.py**:
|
**views.py**:
|
||||||
|
|
||||||
from my_project.example.models import Profile
|
from my_project.example.models import Profile
|
||||||
from rest_framework.renderers import TemplateHTMLRenderer
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
class ProfileList(APIView):
|
class ProfileList(APIView):
|
||||||
renderer_classes = [TemplateHTMLRenderer]
|
renderer_classes = [TemplateHTMLRenderer]
|
||||||
template_name = 'profile_list.html'
|
template_name = 'profile_list.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
queryset = Profile.objects.all()
|
queryset = Profile.objects.all()
|
||||||
return Response({'profiles': queryset})
|
return Response({'profiles': queryset})
|
||||||
|
|
||||||
**profile_list.html**:
|
**profile_list.html**:
|
||||||
|
|
||||||
<html><body>
|
<html><body>
|
||||||
<h1>Profiles</h1>
|
<h1>Profiles</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for profile in profiles %}
|
{% for profile in profiles %}
|
||||||
<li>{{ profile.name }}</li>
|
<li>{{ profile.name }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|
||||||
## Rendering Forms
|
## 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.
|
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:
|
The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance:
|
||||||
|
|
||||||
**views.py**:
|
**views.py**:
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from my_project.example.models import Profile
|
from my_project.example.models import Profile
|
||||||
from rest_framework.renderers import TemplateHTMLRenderer
|
from rest_framework.renderers import TemplateHTMLRenderer
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
class ProfileDetail(APIView):
|
class ProfileDetail(APIView):
|
||||||
renderer_classes = [TemplateHTMLRenderer]
|
renderer_classes = [TemplateHTMLRenderer]
|
||||||
template_name = 'profile_detail.html'
|
template_name = 'profile_detail.html'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
profile = get_object_or_404(Profile, pk=pk)
|
profile = get_object_or_404(Profile, pk=pk)
|
||||||
serializer = ProfileSerializer(profile)
|
serializer = ProfileSerializer(profile)
|
||||||
return Response({'serializer': serializer, 'profile': profile})
|
return Response({'serializer': serializer, 'profile': profile})
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
profile = get_object_or_404(Profile, pk=pk)
|
profile = get_object_or_404(Profile, pk=pk)
|
||||||
serializer = ProfileSerializer(profile, data=request.data)
|
serializer = ProfileSerializer(profile, data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response({'serializer': serializer, 'profile': profile})
|
return Response({'serializer': serializer, 'profile': profile})
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return redirect('profile-list')
|
return redirect('profile-list')
|
||||||
|
|
||||||
**profile_detail.html**:
|
**profile_detail.html**:
|
||||||
|
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
|
|
||||||
<html><body>
|
<html><body>
|
||||||
|
|
||||||
<h1>Profile - {{ profile.name }}</h1>
|
<h1>Profile - {{ profile.name }}</h1>
|
||||||
|
|
||||||
<form action="{% url 'profile-detail' pk=profile.pk %}" method="POST">
|
<form action="{% url 'profile-detail' pk=profile.pk %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form serializer %}
|
{% render_form serializer %}
|
||||||
<input type="submit" value="Save">
|
<input type="submit" value="Save">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|
||||||
### Using template packs
|
### 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.
|
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.
|
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:
|
The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS:
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
…
|
…
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates.
|
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.
|
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):
|
class LoginSerializer(serializers.Serializer):
|
||||||
email = serializers.EmailField(
|
email = serializers.EmailField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
style={'placeholder': 'Email', 'autofocus': True}
|
style={'placeholder': 'Email', 'autofocus': True}
|
||||||
)
|
)
|
||||||
password = serializers.CharField(
|
password = serializers.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
style={'input_type': 'password', 'placeholder': 'Password'}
|
style={'input_type': 'password', 'placeholder': 'Password'}
|
||||||
)
|
)
|
||||||
remember_me = serializers.BooleanField()
|
remember_me = serializers.BooleanField()
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `rest_framework/vertical`
|
#### `rest_framework/vertical`
|
||||||
|
|
||||||
Presents form labels above their corresponding control inputs, using the standard Bootstrap layout.
|
Presents form labels above their corresponding control inputs, using the standard Bootstrap layout.
|
||||||
|
|
||||||
*This is the default template pack.*
|
*This is the default template pack.*
|
||||||
|
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
<form action="{% url 'login' %}" method="post" novalidate>
|
<form action="{% url 'login' %}" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form serializer template_pack='rest_framework/vertical' %}
|
{% render_form serializer template_pack='rest_framework/vertical' %}
|
||||||
<button type="submit" class="btn btn-default">Sign in</button>
|
<button type="submit" class="btn btn-default">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
![Vertical form example](../img/vertical.png)
|
![Vertical form example](../img/vertical.png)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `rest_framework/horizontal`
|
#### `rest_framework/horizontal`
|
||||||
|
|
||||||
Presents labels and controls alongside each other, using a 2/10 column split.
|
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.*
|
*This is the form style used in the browsable API and admin renderers.*
|
||||||
|
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
<form class="form-horizontal" action="{% url 'login' %}" method="post" novalidate>
|
<form class="form-horizontal" action="{% url 'login' %}" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form serializer %}
|
{% render_form serializer %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" class="btn btn-default">Sign in</button>
|
<button type="submit" class="btn btn-default">Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
![Horizontal form example](../img/horizontal.png)
|
![Horizontal form example](../img/horizontal.png)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `rest_framework/inline`
|
#### `rest_framework/inline`
|
||||||
|
|
||||||
A compact form style that presents all the controls inline.
|
A compact form style that presents all the controls inline.
|
||||||
|
|
||||||
{% load rest_framework %}
|
{% load rest_framework %}
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
<form class="form-inline" action="{% url 'login' %}" method="post" novalidate>
|
<form class="form-inline" action="{% url 'login' %}" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form serializer template_pack='rest_framework/inline' %}
|
{% render_form serializer template_pack='rest_framework/inline' %}
|
||||||
<button type="submit" class="btn btn-default">Sign in</button>
|
<button type="submit" class="btn btn-default">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
![Inline form example](../img/inline.png)
|
![Inline form example](../img/inline.png)
|
||||||
|
|
||||||
## Field styles
|
## 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.
|
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.
|
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:
|
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(
|
details = serializers.CharField(
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
style={'base_template': 'textarea.html'}
|
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:
|
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(
|
details = serializers.CharField(
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
style={'template': 'my-field-templates/custom-input.html'}
|
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.
|
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(
|
details = serializers.CharField(
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
style={'base_template': 'textarea.html', 'rows': 10}
|
style={'base_template': 'textarea.html', 'rows': 10}
|
||||||
)
|
)
|
||||||
|
|
||||||
The complete list of `base_template` options and their associated style options is listed below.
|
The complete list of `base_template` options and their associated style options is listed below.
|
||||||
|
|
||||||
base_template | Valid field types | Additional style options
|
base_template | Valid field types | Additional style options
|
||||||
-----------------------|-------------------------------------------------------------|-----------------------------------------------
|
-----------------------|-------------------------------------------------------------|-----------------------------------------------
|
||||||
input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus
|
input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus
|
||||||
textarea.html | `CharField` | rows, placeholder, hide_label
|
textarea.html | `CharField` | rows, placeholder, hide_label
|
||||||
select.html | `ChoiceField` or relational field types | hide_label
|
select.html | `ChoiceField` or relational field types | hide_label
|
||||||
radio.html | `ChoiceField` or relational field types | inline, hide_label
|
radio.html | `ChoiceField` or relational field types | inline, hide_label
|
||||||
select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | 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_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label
|
||||||
checkbox.html | `BooleanField` | hide_label
|
checkbox.html | `BooleanField` | hide_label
|
||||||
fieldset.html | Nested serializer | hide_label
|
fieldset.html | Nested serializer | hide_label
|
||||||
list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
|
list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
|
||||||
|
|
|
@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes.
|
||||||
|
|
||||||
content = JSONRenderer().render(serializer.data)
|
content = JSONRenderer().render(serializer.data)
|
||||||
content
|
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...
|
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()
|
serializer.is_valid()
|
||||||
# True
|
# True
|
||||||
serializer.validated_data
|
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()
|
serializer.save()
|
||||||
# <Snippet: Snippet object>
|
# <Snippet: Snippet object>
|
||||||
|
|
||||||
|
@ -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 = SnippetSerializer(Snippet.objects.all(), many=True)
|
||||||
serializer.data
|
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
|
## Using ModelSerializers
|
||||||
|
|
||||||
|
@ -321,42 +321,50 @@ You can install httpie using pip:
|
||||||
|
|
||||||
Finally, we can get a list of all of the snippets:
|
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
|
HTTP/1.1 200 OK
|
||||||
...
|
...
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "",
|
"title": "",
|
||||||
"code": "foo = \"bar\"\n",
|
"code": "foo = \"bar\"\n",
|
||||||
"linenos": false,
|
"linenos": false,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"style": "friendly"
|
"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,
|
"id": 2,
|
||||||
"title": "",
|
"title": "",
|
||||||
"code": "print(\"hello, world\")\n",
|
"code": "print(\"hello, world\")\n",
|
||||||
"linenos": false,
|
"linenos": false,
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"style": "friendly"
|
"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.
|
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
|
||||||
|
|
|
@ -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`
|
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||||
|
|
||||||
# Install Django and Django REST framework into the virtual environment
|
# Install Django and Django REST framework into the virtual environment
|
||||||
pip install django
|
|
||||||
pip install djangorestframework
|
pip install djangorestframework
|
||||||
|
|
||||||
# Set up a new project with a single application
|
# Set up a new project with a single application
|
||||||
|
|
|
@ -439,3 +439,17 @@ ul.sponsor {
|
||||||
display: inline-block !important;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ theme:
|
||||||
custom_dir: docs_theme
|
custom_dir: docs_theme
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
- toc:
|
- toc:
|
||||||
anchorlink: True
|
anchorlink: True
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# MkDocs to build our documentation.
|
# MkDocs to build our documentation.
|
||||||
mkdocs==1.2.4
|
mkdocs==1.6.0
|
||||||
jinja2>=2.10,<3.1.0 # contextfilter has been renamed
|
|
||||||
|
|
||||||
# pylinkvalidator to check for broken links in documentation.
|
# pylinkvalidator to check for broken links in documentation.
|
||||||
pylinkvalidator==0.3
|
pylinkvalidator==0.3
|
||||||
|
|
|
@ -6,5 +6,5 @@ django-guardian>=2.4.0,<2.5
|
||||||
inflection==0.5.1
|
inflection==0.5.1
|
||||||
markdown>=3.3.7
|
markdown>=3.3.7
|
||||||
psycopg2-binary>=2.9.5,<2.10
|
psycopg2-binary>=2.9.5,<2.10
|
||||||
pygments>=2.12.0,<2.14.0
|
pygments~=2.17.0
|
||||||
pyyaml>=5.3.1,<5.4
|
pyyaml>=5.3.1,<5.4
|
||||||
|
|
|
@ -7,10 +7,8 @@ ______ _____ _____ _____ __
|
||||||
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import django
|
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.15.1'
|
__version__ = '3.15.2'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 3-Clause'
|
__license__ = 'BSD 3-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd'
|
||||||
|
@ -25,11 +23,7 @@ HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||||
ISO_8601 = 'iso-8601'
|
ISO_8601 = 'iso-8601'
|
||||||
|
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
class RemovedInDRF316Warning(DeprecationWarning):
|
||||||
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
|
|
||||||
|
|
||||||
|
|
||||||
class RemovedInDRF315Warning(DeprecationWarning):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import django
|
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
|
||||||
default_app_config = 'rest_framework.authtoken.apps.AuthTokenConfig'
|
|
|
@ -151,30 +151,6 @@ else:
|
||||||
return False
|
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):
|
if django.VERSION >= (5, 1):
|
||||||
# Django 5.1+: use the stock ip_address_validators function
|
# Django 5.1+: use the stock ip_address_validators function
|
||||||
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
|
# Note: Before Django 5.1, ip_address_validators returns a tuple containing
|
||||||
|
|
|
@ -4,9 +4,9 @@ import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from enum import Enum
|
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.utils.timezone import valid_datetime
|
||||||
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
||||||
|
|
||||||
logger = logging.getLogger("rest_framework.fields")
|
|
||||||
|
|
||||||
|
|
||||||
class empty:
|
class empty:
|
||||||
"""
|
"""
|
||||||
|
@ -989,9 +987,9 @@ class DecimalField(Field):
|
||||||
self.min_value = min_value
|
self.min_value = min_value
|
||||||
|
|
||||||
if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal):
|
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):
|
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:
|
if self.max_digits is not None and self.decimal_places is not None:
|
||||||
self.max_whole_digits = self.max_digits - self.decimal_places
|
self.max_whole_digits = self.max_digits - self.decimal_places
|
||||||
|
|
|
@ -114,10 +114,6 @@ class SearchFilter(BaseFilterBackend):
|
||||||
if hasattr(field, "path_infos"):
|
if hasattr(field, "path_infos"):
|
||||||
# Update opts to follow the relation.
|
# Update opts to follow the relation.
|
||||||
opts = field.path_infos[-1].to_opts
|
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.
|
# Otherwise, use the field with icontains.
|
||||||
lookup = 'icontains'
|
lookup = 'icontains'
|
||||||
return LOOKUP_SEP.join([field_name, lookup])
|
return LOOKUP_SEP.join([field_name, lookup])
|
||||||
|
|
|
@ -11,7 +11,6 @@ from urllib import parse
|
||||||
|
|
||||||
from django.core.paginator import InvalidPage
|
from django.core.paginator import InvalidPage
|
||||||
from django.core.paginator import Paginator as DjangoPaginator
|
from django.core.paginator import Paginator as DjangoPaginator
|
||||||
from django.db.models import Q
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -631,7 +630,7 @@ class CursorPagination(BasePagination):
|
||||||
queryset = queryset.order_by(*self.ordering)
|
queryset = queryset.order_by(*self.ordering)
|
||||||
|
|
||||||
# If we have a cursor with a fixed position then filter by that.
|
# 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]
|
order = self.ordering[0]
|
||||||
is_reversed = order.startswith('-')
|
is_reversed = order.startswith('-')
|
||||||
order_attr = order.lstrip('-')
|
order_attr = order.lstrip('-')
|
||||||
|
@ -642,12 +641,7 @@ class CursorPagination(BasePagination):
|
||||||
else:
|
else:
|
||||||
kwargs = {order_attr + '__gt': current_position}
|
kwargs = {order_attr + '__gt': current_position}
|
||||||
|
|
||||||
filter_query = Q(**kwargs)
|
queryset = queryset.filter(**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)
|
|
||||||
|
|
||||||
# If we have an offset cursor then offset the entire page by that amount.
|
# 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
|
# 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
|
# The item in this position and the item following it
|
||||||
# have different positions. We can use this position as
|
# have different positions. We can use this position as
|
||||||
# our marker.
|
# our marker.
|
||||||
has_item_with_unique_position = position is not None
|
has_item_with_unique_position = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# The item in this position has the same position as the item
|
# 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
|
# The item in this position and the item following it
|
||||||
# have different positions. We can use this position as
|
# have different positions. We can use this position as
|
||||||
# our marker.
|
# our marker.
|
||||||
has_item_with_unique_position = position is not None
|
has_item_with_unique_position = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# The item in this position has the same position as the item
|
# The item in this position has the same position as the item
|
||||||
|
@ -896,7 +890,7 @@ class CursorPagination(BasePagination):
|
||||||
attr = instance[field_name]
|
attr = instance[field_name]
|
||||||
else:
|
else:
|
||||||
attr = getattr(instance, field_name)
|
attr = getattr(instance, field_name)
|
||||||
return None if attr is None else str(attr)
|
return str(attr)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
return Response({
|
return Response({
|
||||||
|
|
|
@ -15,9 +15,9 @@ from django.http.multipartparser import ChunkIter
|
||||||
from django.http.multipartparser import \
|
from django.http.multipartparser import \
|
||||||
MultiPartParser as DjangoMultiPartParser
|
MultiPartParser as DjangoMultiPartParser
|
||||||
from django.http.multipartparser import MultiPartParserError
|
from django.http.multipartparser import MultiPartParserError
|
||||||
|
from django.utils.http import parse_header_parameters
|
||||||
|
|
||||||
from rest_framework import renderers
|
from rest_framework import renderers
|
||||||
from rest_framework.compat import parse_header_parameters
|
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.utils import json
|
from rest_framework.utils import json
|
||||||
|
|
|
@ -54,6 +54,9 @@ class OperandHolder(OperationHolderMixin):
|
||||||
self.op2_class == other.op2_class
|
self.op2_class == other.op2_class
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.operator_class, self.op1_class, self.op2_class))
|
||||||
|
|
||||||
|
|
||||||
class AND:
|
class AND:
|
||||||
def __init__(self, op1, op2):
|
def __init__(self, op1, op2):
|
||||||
|
|
|
@ -19,12 +19,13 @@ from django.core.paginator import Page
|
||||||
from django.template import engines, loader
|
from django.template import engines, loader
|
||||||
from django.urls import NoReverseMatch
|
from django.urls import NoReverseMatch
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
|
from django.utils.http import parse_header_parameters
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
from rest_framework import VERSION, exceptions, serializers, status
|
from rest_framework import VERSION, exceptions, serializers, status
|
||||||
from rest_framework.compat import (
|
from rest_framework.compat import (
|
||||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
|
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.exceptions import ParseError
|
||||||
from rest_framework.request import is_form_media_type, override_method
|
from rest_framework.request import is_form_media_type, override_method
|
||||||
|
|
|
@ -16,9 +16,9 @@ from django.conf import settings
|
||||||
from django.http import HttpRequest, QueryDict
|
from django.http import HttpRequest, QueryDict
|
||||||
from django.http.request import RawPostDataException
|
from django.http.request import RawPostDataException
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
from django.utils.http import parse_header_parameters
|
||||||
|
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework.compat import parse_header_parameters
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.db import models
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from rest_framework import (
|
from rest_framework import (
|
||||||
RemovedInDRF315Warning, exceptions, renderers, serializers
|
RemovedInDRF316Warning, exceptions, renderers, serializers
|
||||||
)
|
)
|
||||||
from rest_framework.compat import inflection, uritemplate
|
from rest_framework.compat import inflection, uritemplate
|
||||||
from rest_framework.fields import _UnvalidatedField, empty
|
from rest_framework.fields import _UnvalidatedField, empty
|
||||||
|
@ -725,7 +725,7 @@ class AutoSchema(ViewInspector):
|
||||||
def _get_reference(self, serializer):
|
def _get_reference(self, serializer):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Method `_get_reference()` has been renamed to `get_reference()`. "
|
"Method `_get_reference()` has been renamed to `get_reference()`. "
|
||||||
"The old name will be removed in DRF v3.15.",
|
"The old name will be removed in DRF v3.16.",
|
||||||
RemovedInDRF315Warning, stacklevel=2
|
RemovedInDRF316Warning, stacklevel=2
|
||||||
)
|
)
|
||||||
return self.get_reference(serializer)
|
return self.get_reference(serializer)
|
||||||
|
|
|
@ -313,3 +313,4 @@ def smart_urlquote_wrapper(matched_url):
|
||||||
return smart_urlquote(matched_url)
|
return smart_urlquote(matched_url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import io
|
import io
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import django
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.handlers.wsgi import WSGIHandler
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
|
@ -394,19 +393,7 @@ class URLPatternsTestCase(testcases.SimpleTestCase):
|
||||||
|
|
||||||
cls._override.enable()
|
cls._override.enable()
|
||||||
|
|
||||||
if django.VERSION > (4, 0):
|
cls.addClassCleanup(cls._override.disable)
|
||||||
cls.addClassCleanup(cls._override.disable)
|
cls.addClassCleanup(cleanup_url_patterns, cls)
|
||||||
cls.addClassCleanup(cleanup_url_patterns, cls)
|
|
||||||
|
|
||||||
super().setUpClass()
|
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
|
|
||||||
|
|
|
@ -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
|
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):
|
def media_type_matches(lhs, rhs):
|
||||||
|
|
|
@ -3,7 +3,7 @@ license_files = LICENSE.md
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts=--tb=short --strict-markers -ra
|
addopts=--tb=short --strict-markers -ra
|
||||||
testspath = tests
|
testpaths = tests
|
||||||
filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning
|
filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
|
|
13
setup.py
13
setup.py
|
@ -8,7 +8,7 @@ from io import open
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
CURRENT_PYTHON = sys.version_info[:2]
|
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.
|
# This check and everything above must remain compatible with Python 2.7.
|
||||||
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
||||||
|
@ -83,18 +83,13 @@ setup(
|
||||||
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
|
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
|
||||||
packages=find_packages(exclude=['tests*']),
|
packages=find_packages(exclude=['tests*']),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=["django>=3.0", 'backports.zoneinfo;python_version<"3.9"'],
|
install_requires=["django>=4.2", 'backports.zoneinfo;python_version<"3.9"'],
|
||||||
python_requires=">=3.6",
|
python_requires=">=3.8",
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Framework :: Django',
|
'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 :: 4.2',
|
||||||
'Framework :: Django :: 5.0',
|
'Framework :: Django :: 5.0',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
@ -102,8 +97,6 @@ setup(
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.7',
|
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.8',
|
||||||
'Programming Language :: Python :: 3.9',
|
'Programming Language :: Python :: 3.9',
|
||||||
'Programming Language :: Python :: 3.10',
|
'Programming Language :: Python :: 3.10',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import django
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -235,21 +234,13 @@ class SessionAuthTests(TestCase):
|
||||||
Ensure POSTing form over session authentication with CSRF token succeeds.
|
Ensure POSTing form over session authentication with CSRF token succeeds.
|
||||||
Regression test for #6088
|
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)
|
self.csrf_client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
# Set the csrf_token cookie so that CsrfViewMiddleware._get_token() works
|
# 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
|
self.csrf_client.cookies[settings.CSRF_COOKIE_NAME] = token
|
||||||
|
|
||||||
# Post the token matching the cookie value
|
# Post the token matching the cookie value
|
||||||
|
|
|
@ -13,8 +13,6 @@ def pytest_addoption(parser):
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
from django.conf import settings
|
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(
|
settings.configure(
|
||||||
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
DEBUG_PROPAGATE_EXCEPTIONS=True,
|
||||||
DATABASES={
|
DATABASES={
|
||||||
|
@ -64,7 +62,6 @@ def pytest_configure(config):
|
||||||
PASSWORD_HASHERS=(
|
PASSWORD_HASHERS=(
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
),
|
),
|
||||||
**use_l10n,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# guardian is optional
|
# guardian is optional
|
||||||
|
@ -87,10 +84,7 @@ def pytest_configure(config):
|
||||||
import rest_framework
|
import rest_framework
|
||||||
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
|
settings.STATIC_ROOT = os.path.join(os.path.dirname(rest_framework.__file__), 'static-root')
|
||||||
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
backend = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||||
if django.VERSION < (4, 2):
|
settings.STORAGES['staticfiles']['BACKEND'] = backend
|
||||||
settings.STATICFILES_STORAGE = backend
|
|
||||||
else:
|
|
||||||
settings.STORAGES['staticfiles']['BACKEND'] = backend
|
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
|
|
|
@ -1,155 +1,155 @@
|
||||||
import pytest
|
import pytest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.compat import apply_markdown
|
from rest_framework.compat import apply_markdown
|
||||||
from rest_framework.utils.formatting import dedent
|
from rest_framework.utils.formatting import dedent
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
# We check that docstrings get nicely un-indented.
|
# We check that docstrings get nicely un-indented.
|
||||||
DESCRIPTION = """an example docstring
|
DESCRIPTION = """an example docstring
|
||||||
====================
|
====================
|
||||||
|
|
||||||
* list
|
* list
|
||||||
* list
|
* list
|
||||||
|
|
||||||
another header
|
another header
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
code block
|
code block
|
||||||
|
|
||||||
indented
|
indented
|
||||||
|
|
||||||
# hash style header #
|
# hash style header #
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[{
|
[{
|
||||||
"alpha": 1,
|
"alpha": 1,
|
||||||
"beta": "this is a string"
|
"beta": "this is a string"
|
||||||
}]
|
}]
|
||||||
```"""
|
```"""
|
||||||
|
|
||||||
|
|
||||||
# If markdown is installed we also test it's working
|
# If markdown is installed we also test it's working
|
||||||
# (and that our wrapped forces '=' to h2 and '-' to h3)
|
# (and that our wrapped forces '=' to h2 and '-' to h3)
|
||||||
MARKDOWN_DOCSTRING = """<h2 id="an-example-docstring">an example docstring</h2>
|
MARKDOWN_DOCSTRING = """<h2 id="an-example-docstring">an example docstring</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>list</li>
|
<li>list</li>
|
||||||
<li>list</li>
|
<li>list</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="another-header">another header</h3>
|
<h3 id="another-header">another header</h3>
|
||||||
<pre><code>code block
|
<pre><code>code block
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>indented</p>
|
<p>indented</p>
|
||||||
<h2 id="hash-style-header">hash style header</h2>
|
<h2 id="hash-style-header">hash style header</h2>
|
||||||
<div class="highlight"><pre><span></span><span class="p">[{</span><span class="w"></span><br /><span class="w"> </span><span class="nt">"alpha"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"></span><br /><span class="w"> </span><span class="nt">"beta"</span><span class="p">:</span><span class="w"> </span><span class="s2">"this is a string"</span><span class="w"></span><br /><span class="p">}]</span><span class="w"></span><br /></pre></div>
|
<div class="highlight"><pre><span></span><span class="p">[{</span><br /><span class="w"> </span><span class="nt">"alpha"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><br /><span class="w"> </span><span class="nt">"beta"</span><span class="p">:</span><span class="w"> </span><span class="s2">"this is a string"</span><br /><span class="p">}]</span><br /></pre></div>
|
||||||
<p><br /></p>"""
|
<p><br /></p>"""
|
||||||
|
|
||||||
|
|
||||||
class TestViewNamesAndDescriptions(TestCase):
|
class TestViewNamesAndDescriptions(TestCase):
|
||||||
def test_view_name_uses_class_name(self):
|
def test_view_name_uses_class_name(self):
|
||||||
"""
|
"""
|
||||||
Ensure view names are based on the class name.
|
Ensure view names are based on the class name.
|
||||||
"""
|
"""
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
pass
|
pass
|
||||||
assert MockView().get_view_name() == 'Mock'
|
assert MockView().get_view_name() == 'Mock'
|
||||||
|
|
||||||
def test_view_name_uses_name_attribute(self):
|
def test_view_name_uses_name_attribute(self):
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
name = 'Foo'
|
name = 'Foo'
|
||||||
assert MockView().get_view_name() == 'Foo'
|
assert MockView().get_view_name() == 'Foo'
|
||||||
|
|
||||||
def test_view_name_uses_suffix_attribute(self):
|
def test_view_name_uses_suffix_attribute(self):
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
suffix = 'List'
|
suffix = 'List'
|
||||||
assert MockView().get_view_name() == 'Mock List'
|
assert MockView().get_view_name() == 'Mock List'
|
||||||
|
|
||||||
def test_view_name_preferences_name_over_suffix(self):
|
def test_view_name_preferences_name_over_suffix(self):
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
name = 'Foo'
|
name = 'Foo'
|
||||||
suffix = 'List'
|
suffix = 'List'
|
||||||
assert MockView().get_view_name() == 'Foo'
|
assert MockView().get_view_name() == 'Foo'
|
||||||
|
|
||||||
def test_view_description_uses_docstring(self):
|
def test_view_description_uses_docstring(self):
|
||||||
"""Ensure view descriptions are based on the docstring."""
|
"""Ensure view descriptions are based on the docstring."""
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
"""an example docstring
|
"""an example docstring
|
||||||
====================
|
====================
|
||||||
|
|
||||||
* list
|
* list
|
||||||
* list
|
* list
|
||||||
|
|
||||||
another header
|
another header
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
code block
|
code block
|
||||||
|
|
||||||
indented
|
indented
|
||||||
|
|
||||||
# hash style header #
|
# hash style header #
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[{
|
[{
|
||||||
"alpha": 1,
|
"alpha": 1,
|
||||||
"beta": "this is a string"
|
"beta": "this is a string"
|
||||||
}]
|
}]
|
||||||
```"""
|
```"""
|
||||||
|
|
||||||
assert MockView().get_view_description() == DESCRIPTION
|
assert MockView().get_view_description() == DESCRIPTION
|
||||||
|
|
||||||
def test_view_description_uses_description_attribute(self):
|
def test_view_description_uses_description_attribute(self):
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
description = 'Foo'
|
description = 'Foo'
|
||||||
assert MockView().get_view_description() == 'Foo'
|
assert MockView().get_view_description() == 'Foo'
|
||||||
|
|
||||||
def test_view_description_allows_empty_description(self):
|
def test_view_description_allows_empty_description(self):
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
"""Description."""
|
"""Description."""
|
||||||
description = ''
|
description = ''
|
||||||
assert MockView().get_view_description() == ''
|
assert MockView().get_view_description() == ''
|
||||||
|
|
||||||
def test_view_description_can_be_empty(self):
|
def test_view_description_can_be_empty(self):
|
||||||
"""
|
"""
|
||||||
Ensure that if a view has no docstring,
|
Ensure that if a view has no docstring,
|
||||||
then it's description is the empty string.
|
then it's description is the empty string.
|
||||||
"""
|
"""
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
pass
|
pass
|
||||||
assert MockView().get_view_description() == ''
|
assert MockView().get_view_description() == ''
|
||||||
|
|
||||||
def test_view_description_can_be_promise(self):
|
def test_view_description_can_be_promise(self):
|
||||||
"""
|
"""
|
||||||
Ensure a view may have a docstring that is actually a lazily evaluated
|
Ensure a view may have a docstring that is actually a lazily evaluated
|
||||||
class that can be converted to a string.
|
class that can be converted to a string.
|
||||||
|
|
||||||
See: https://github.com/encode/django-rest-framework/issues/1708
|
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
|
# 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
|
# up with a test case string in our l10n catalog
|
||||||
|
|
||||||
class MockLazyStr:
|
class MockLazyStr:
|
||||||
def __init__(self, string):
|
def __init__(self, string):
|
||||||
self.s = string
|
self.s = string
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.s
|
return self.s
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
__doc__ = MockLazyStr("a gettext string")
|
__doc__ = MockLazyStr("a gettext string")
|
||||||
|
|
||||||
assert MockView().get_view_description() == 'a gettext string'
|
assert MockView().get_view_description() == 'a gettext string'
|
||||||
|
|
||||||
@pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed")
|
@pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed")
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""
|
"""
|
||||||
Ensure markdown to HTML works as expected.
|
Ensure markdown to HTML works as expected.
|
||||||
"""
|
"""
|
||||||
assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
|
assert apply_markdown(DESCRIPTION) == MARKDOWN_DOCSTRING
|
||||||
|
|
||||||
|
|
||||||
def test_dedent_tabs():
|
def test_dedent_tabs():
|
||||||
result = 'first string\n\nsecond string'
|
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("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("\tfirst string\n\n\tsecond string") == result
|
||||||
assert dedent("first string\n\n\tsecond string") == result
|
assert dedent("first string\n\n\tsecond string") == result
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
||||||
from enum import auto
|
from enum import auto
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
@ -1254,15 +1255,19 @@ class TestMinMaxDecimalField(FieldValues):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_warning_when_not_decimal_types(self, caplog):
|
def test_warning_when_not_decimal_types(self, caplog):
|
||||||
import logging
|
with warnings.catch_warnings(record=True) as w:
|
||||||
serializers.DecimalField(
|
warnings.simplefilter('always')
|
||||||
max_digits=3, decimal_places=1,
|
|
||||||
min_value=10, max_value=20
|
serializers.DecimalField(
|
||||||
)
|
max_digits=3, decimal_places=1,
|
||||||
assert caplog.record_tuples == [
|
min_value=10, max_value=20
|
||||||
("rest_framework.fields", logging.WARNING, "max_value in DecimalField should be Decimal type."),
|
)
|
||||||
("rest_framework.fields", logging.WARNING, "min_value in DecimalField should be Decimal type.")
|
|
||||||
]
|
assert len(w) == 2
|
||||||
|
assert all(issubclass(i.category, UserWarning) for i in w)
|
||||||
|
|
||||||
|
assert 'max_value should be a Decimal instance' in str(w[0].message)
|
||||||
|
assert 'min_value should be a Decimal instance' in str(w[1].message)
|
||||||
|
|
||||||
|
|
||||||
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues):
|
||||||
|
@ -1628,7 +1633,7 @@ class TestCustomTimezoneForDateTimeField(TestCase):
|
||||||
assert rendered_date == rendered_date_in_timezone
|
assert rendered_date == rendered_date_in_timezone
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.")
|
@pytest.mark.skipif(pytz is None, reason="Django 5.0 has removed pytz; this test should eventually be able to get removed.")
|
||||||
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||||
|
|
|
@ -12,7 +12,6 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import django
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
@ -453,14 +452,11 @@ class TestPosgresFieldsMapping(TestCase):
|
||||||
model = ArrayFieldModel
|
model = ArrayFieldModel
|
||||||
fields = ['array_field', 'array_field_with_blank']
|
fields = ['array_field', 'array_field_with_blank']
|
||||||
|
|
||||||
validators = ""
|
|
||||||
if django.VERSION < (4, 1):
|
|
||||||
validators = ", validators=[<django.core.validators.MaxLengthValidator object>]"
|
|
||||||
expected = dedent("""
|
expected = dedent("""
|
||||||
TestSerializer():
|
TestSerializer():
|
||||||
array_field = ListField(allow_empty=False, child=CharField(label='Array field'%s))
|
array_field = ListField(allow_empty=False, child=CharField(label='Array field'))
|
||||||
array_field_with_blank = ListField(child=CharField(label='Array field with blank'%s), required=False)
|
array_field_with_blank = ListField(child=CharField(label='Array field with blank'), required=False)
|
||||||
""" % (validators, validators))
|
""")
|
||||||
self.assertEqual(repr(TestSerializer()), expected)
|
self.assertEqual(repr(TestSerializer()), expected)
|
||||||
|
|
||||||
@pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField')
|
@pytest.mark.skipif(hasattr(models, 'JSONField'), reason='has models.JSONField')
|
||||||
|
|
|
@ -972,24 +972,17 @@ class TestCursorPagination(CursorPaginationTestsMixin):
|
||||||
def __init__(self, items):
|
def __init__(self, items):
|
||||||
self.items = items
|
self.items = items
|
||||||
|
|
||||||
def filter(self, q):
|
def filter(self, created__gt=None, created__lt=None):
|
||||||
q_args = dict(q.deconstruct()[1])
|
|
||||||
if not q_args:
|
|
||||||
# django 3.0.x artifact
|
|
||||||
q_args = dict(q.deconstruct()[2])
|
|
||||||
created__gt = q_args.get('created__gt')
|
|
||||||
created__lt = q_args.get('created__lt')
|
|
||||||
|
|
||||||
if created__gt is not None:
|
if created__gt is not None:
|
||||||
return MockQuerySet([
|
return MockQuerySet([
|
||||||
item for item in self.items
|
item for item in self.items
|
||||||
if item.created is None or item.created > int(created__gt)
|
if item.created > int(created__gt)
|
||||||
])
|
])
|
||||||
|
|
||||||
assert created__lt is not None
|
assert created__lt is not None
|
||||||
return MockQuerySet([
|
return MockQuerySet([
|
||||||
item for item in self.items
|
item for item in self.items
|
||||||
if item.created is None or item.created < int(created__lt)
|
if item.created < int(created__lt)
|
||||||
])
|
])
|
||||||
|
|
||||||
def order_by(self, *ordering):
|
def order_by(self, *ordering):
|
||||||
|
@ -1108,127 +1101,6 @@ class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase
|
||||||
return (previous, current, next, previous_url, next_url)
|
return (previous, current, next, previous_url, next_url)
|
||||||
|
|
||||||
|
|
||||||
class NullableCursorPaginationModel(models.Model):
|
|
||||||
created = models.IntegerField(null=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCursorPaginationWithNulls(TestCase):
|
|
||||||
"""
|
|
||||||
Unit tests for `pagination.CursorPagination` with ordering on a nullable field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
class ExamplePagination(pagination.CursorPagination):
|
|
||||||
page_size = 1
|
|
||||||
ordering = 'created'
|
|
||||||
|
|
||||||
self.pagination = ExamplePagination()
|
|
||||||
data = [
|
|
||||||
None, None, 3, 4
|
|
||||||
]
|
|
||||||
for idx in data:
|
|
||||||
NullableCursorPaginationModel.objects.create(created=idx)
|
|
||||||
|
|
||||||
self.queryset = NullableCursorPaginationModel.objects.all()
|
|
||||||
|
|
||||||
get_pages = TestCursorPagination.get_pages
|
|
||||||
|
|
||||||
def test_ascending(self):
|
|
||||||
"""Test paginating one row at a time, current should go 1, 2, 3, 4, 3, 2, 1."""
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages('/')
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [3] # [None] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L789
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [4]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [3]
|
|
||||||
assert current == [4]
|
|
||||||
assert next is None
|
|
||||||
assert next_url is None
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [4]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None] # [3] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
def test_descending(self):
|
|
||||||
"""Test paginating one row at a time, current should go 4, 3, 2, 1, 2, 3, 4."""
|
|
||||||
self.pagination.ordering = ('-created',)
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages('/')
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [4]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None] # [4] paging artifact
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None] # [3] paging artifact
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [None]
|
|
||||||
assert next is None
|
|
||||||
assert next_url is None
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [3]
|
|
||||||
assert current == [None]
|
|
||||||
assert next == [None]
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous == [None]
|
|
||||||
assert current == [3]
|
|
||||||
assert next == [3] # [4] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731
|
|
||||||
|
|
||||||
# skip back artifact
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
|
|
||||||
|
|
||||||
assert previous is None
|
|
||||||
assert current == [4]
|
|
||||||
assert next == [3]
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_displayed_page_numbers():
|
def test_get_displayed_page_numbers():
|
||||||
"""
|
"""
|
||||||
Test our contextual page display function.
|
Test our contextual page display function.
|
||||||
|
|
|
@ -716,3 +716,59 @@ class PermissionsCompositionTests(TestCase):
|
||||||
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
|
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
|
||||||
hasperm = composed_perm().has_object_permission(request, None, None)
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
||||||
assert hasperm is False
|
assert hasperm is False
|
||||||
|
|
||||||
|
def test_operand_holder_is_hashable(self):
|
||||||
|
assert hash((permissions.IsAuthenticated & permissions.IsAdminUser))
|
||||||
|
|
||||||
|
def test_operand_holder_hash_same_for_same_operands_and_operator(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) == hash(second_operand_holder)
|
||||||
|
|
||||||
|
def test_operand_holder_hash_differs_for_different_operands(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.AllowAny & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
third_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.AllowAny
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) != hash(second_operand_holder)
|
||||||
|
assert hash(first_operand_holder) != hash(third_operand_holder)
|
||||||
|
assert hash(second_operand_holder) != hash(third_operand_holder)
|
||||||
|
|
||||||
|
def test_operand_holder_hash_differs_for_different_operators(self):
|
||||||
|
first_operand_holder = (
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
second_operand_holder = (
|
||||||
|
permissions.IsAuthenticated | permissions.IsAdminUser
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hash(first_operand_holder) != hash(second_operand_holder)
|
||||||
|
|
||||||
|
def test_filtering_permissions(self):
|
||||||
|
unfiltered_permissions = [
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.AllowAny,
|
||||||
|
]
|
||||||
|
expected_permissions = [
|
||||||
|
permissions.IsAuthenticated & permissions.IsAdminUser,
|
||||||
|
permissions.AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered_permissions = [
|
||||||
|
perm for perm
|
||||||
|
in dict.fromkeys(unfiltered_permissions)
|
||||||
|
]
|
||||||
|
|
||||||
|
assert filtered_permissions == expected_permissions
|
||||||
|
|
|
@ -910,7 +910,7 @@ class TestDocumentationRenderer(TestCase):
|
||||||
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
||||||
}
|
}
|
||||||
html = template.render(context)
|
html = template.render(context)
|
||||||
assert 'testcases list' in html
|
assert 'testcases<span class="w"> </span>list' in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||||
|
|
|
@ -2,7 +2,6 @@ import itertools
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import django
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -319,10 +318,6 @@ class TestAPIRequestFactory(TestCase):
|
||||||
assert request.META['CONTENT_TYPE'] == 'application/json'
|
assert request.META['CONTENT_TYPE'] == 'application/json'
|
||||||
|
|
||||||
|
|
||||||
def check_urlpatterns(cls):
|
|
||||||
assert urlpatterns is not cls.urlpatterns
|
|
||||||
|
|
||||||
|
|
||||||
class TestUrlPatternTestCase(URLPatternsTestCase):
|
class TestUrlPatternTestCase(URLPatternsTestCase):
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', view),
|
path('', view),
|
||||||
|
@ -334,18 +329,11 @@ class TestUrlPatternTestCase(URLPatternsTestCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
assert urlpatterns is cls.urlpatterns
|
assert urlpatterns is cls.urlpatterns
|
||||||
|
|
||||||
if django.VERSION > (4, 0):
|
@classmethod
|
||||||
cls.addClassCleanup(
|
def doClassCleanups(cls):
|
||||||
check_urlpatterns,
|
assert urlpatterns is cls.urlpatterns
|
||||||
cls
|
super().doClassCleanups()
|
||||||
)
|
assert urlpatterns is not cls.urlpatterns
|
||||||
|
|
||||||
if django.VERSION < (4, 0):
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
assert urlpatterns is cls.urlpatterns
|
|
||||||
super().tearDownClass()
|
|
||||||
assert urlpatterns is not cls.urlpatterns
|
|
||||||
|
|
||||||
def test_urlpatterns(self):
|
def test_urlpatterns(self):
|
||||||
assert self.client.get('/').status_code == 200
|
assert self.client.get('/').status_code == 200
|
||||||
|
|
15
tox.ini
15
tox.ini
|
@ -1,11 +1,9 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py36,py37,py38,py39}-django30
|
{py38,py39}-{django42}
|
||||||
{py36,py37,py38,py39}-django31
|
{py310}-{django42,django50,djangomain}
|
||||||
{py36,py37,py38,py39,py310}-django32
|
{py311}-{django42,django50,djangomain}
|
||||||
{py38,py39,py310}-{django40,django41,django42,djangomain}
|
{py312}-{django42,django50,djangomain}
|
||||||
{py311}-{django41,django42,django50,djangomain}
|
|
||||||
{py312}-{django42,djanggo50,djangomain}
|
|
||||||
base
|
base
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
|
@ -17,11 +15,6 @@ setenv =
|
||||||
PYTHONDONTWRITEBYTECODE=1
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
PYTHONWARNINGS=once
|
PYTHONWARNINGS=once
|
||||||
deps =
|
deps =
|
||||||
django30: Django>=3.0,<3.1
|
|
||||||
django31: Django>=3.1,<3.2
|
|
||||||
django32: Django>=3.2,<4.0
|
|
||||||
django40: Django>=4.0,<4.1
|
|
||||||
django41: Django>=4.1,<4.2
|
|
||||||
django42: Django>=4.2,<5.0
|
django42: Django>=4.2,<5.0
|
||||||
django50: Django>=5.0,<5.1
|
django50: Django>=5.0,<5.1
|
||||||
djangomain: https://github.com/django/django/archive/main.tar.gz
|
djangomain: https://github.com/django/django/archive/main.tar.gz
|
||||||
|
|
Loading…
Reference in New Issue
Block a user