diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d7c23d6351..5a830ca53f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: encode custom: https://fund.django-rest-framework.org/topics/funding/ diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md new file mode 100644 index 0000000000..0da1549534 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -0,0 +1,10 @@ +--- +name: Issue +about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏 +--- + +## Checklist + +- [ ] Raised initially as discussion #... +- [ ] This 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.) +- [ ] I have reduced the issue to the simplest possible case. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..382fc521aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: +- name: Discussions + url: https://github.com/encode/django-rest-framework/discussions + about: > + The "Discussions" forum is where you want to start. 💖 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..42fee2a124 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +jobs: + tests: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-20.04 + + strategy: + matrix: + python-version: + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Upgrade packaging tools + run: python -m pip install --upgrade pip setuptools virtualenv wheel + + - name: Install dependencies + run: python -m pip install --upgrade codecov tox tox-py + + - name: Run tox targets for ${{ matrix.python-version }} + run: tox --py current + + - name: Run extra tox targets + if: ${{ matrix.python-version == '3.9' }} + run: | + python setup.py bdist_wheel + rm -r djangorestframework.egg-info # see #6139 + tox -e base,dist,docs + tox -e dist --installpkg ./dist/djangorestframework-*.whl + + - name: Upload coverage + run: | + codecov -e TOXENV,DJANGO diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..9c29ed0564 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,24 @@ +name: pre-commit + +on: + push: + branches: + - master + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - uses: pre-commit/action@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 41768084c5..641714d163 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.db *~ .* +*.py.bak + /site/ /htmlcov/ @@ -13,6 +15,6 @@ MANIFEST coverage.* +!.github !.gitignore -!.travis.yml -!.isort.cfg +!.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..5a6e554b98 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml +- repo: https://github.com/pycqa/isort + rev: 5.8.0 + hooks: + - id: isort +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-tidy-imports diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2c2724bf63..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,53 +0,0 @@ -language: python -cache: pip -dist: bionic -matrix: - fast_finish: true - include: - - - { python: "3.5", env: DJANGO=2.2 } - - - { python: "3.6", env: DJANGO=2.2 } - - { python: "3.6", env: DJANGO=3.0 } - - { python: "3.6", env: DJANGO=3.1 } - - { python: "3.6", env: DJANGO=master } - - - { python: "3.7", env: DJANGO=2.2 } - - { python: "3.7", env: DJANGO=3.0 } - - { python: "3.7", env: DJANGO=3.1 } - - { python: "3.7", env: DJANGO=master } - - - { python: "3.8", env: DJANGO=3.0 } - - { python: "3.8", env: DJANGO=3.1 } - - { python: "3.8", env: DJANGO=master } - - - { python: "3.9-dev", env: DJANGO=3.1 } - - { python: "3.9-dev", env: DJANGO=master } - - - { python: "3.8", env: TOXENV=base } - - { python: "3.8", env: TOXENV=lint } - - { python: "3.8", env: TOXENV=docs } - - - python: "3.8" - env: TOXENV=dist - script: - - python setup.py bdist_wheel - - rm -r djangorestframework.egg-info # see #6139 - - tox --installpkg ./dist/djangorestframework-*.whl - - tox # test sdist - - allow_failures: - - env: DJANGO=master - -install: - - pip install tox tox-travis - -script: - - tox - -after_success: - - pip install codecov - - codecov -e TOXENV,DJANGO - -notifications: - email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f1aad08f4..a7f17b1a35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,207 +1,3 @@ # Contributing to REST framework -> The world can only really be changed one piece at a time. The art is picking that piece. -> -> — [Tim Berners-Lee][cite] - -There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. - -## Community - -The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. - -If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. - -Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. - -When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. - -## Code of conduct - -Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. - -Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. (e.g. 'Hey folks,') - -The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. - -# Issues - -It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. - -Some tips on good 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. -* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. -* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. -* 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, bug fixes, 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 - -Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to: - -* Read through the ticket - does it make sense, is it missing any context that would help explain it better? -* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? -* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? -* If the ticket is a feature request, do you agree with it, and 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. - -# Development - -To start developing on Django REST framework, clone the repo: - - git clone https://github.com/encode/django-rest-framework - -Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles. - -## Testing - -To run the tests, clone the repository, and then: - - # Setup the virtual environment - python3 -m venv env - source env/bin/activate - pip install django - pip install -r requirements.txt - - # Run the tests - ./runtests.py - -### Test options - -Run using a more concise output style. - - ./runtests.py -q - -Run the tests using a more concise output style, no coverage, no flake8. - - ./runtests.py --fast - -Don't run the flake8 code linting. - - ./runtests.py --nolint - -Only run the flake8 code linting, don't run the tests. - - ./runtests.py --lintonly - -Run the tests for a given test case. - - ./runtests.py MyTestCase - -Run the tests for a given test method. - - ./runtests.py MyTestCase.test_this_method - -Shorter form to run the tests for a given test method. - - ./runtests.py test_this_method - -Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input. - -### Running against multiple environments - -You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: - - tox - -## Pull requests - -It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. - -It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests. - -It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. - -GitHub's documentation for working on pull requests is [available here][pull-requests]. - -Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django. - -Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect. - -## Managing compatibility issues - -Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. - -# Documentation - -The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs]. - -There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended. - -## Building the documentation - -To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command. - - mkdocs build - -This will build the documentation into the `site` directory. - -You can build the documentation and open a preview in a browser window by using the `serve` command. - - mkdocs serve - -## Language style - -Documentation should be in American English. The tone of the documentation is very important - try to stick to a simple, plain, objective and well-balanced style where possible. - -Some other tips: - -* Keep paragraphs reasonably short. -* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'. - -## Markdown style - -There are a couple of conventions you should follow when working on the documentation. - -##### 1. Headers - -Headers should use the hash style. For example: - - ### Some important topic - -The underline style should not be used. **Don't do this:** - - Some important topic - ==================== - -##### 2. Links - -Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. - - Here is a link to [some other thing][other-thing]. - - More text... - - [other-thing]: http://example.com/other/thing - -This style helps keep the documentation source consistent and readable. - -If you are hyperlinking to another REST framework document, you should use a relative link, and link to the `.md` suffix. For example: - - [authentication]: ../api-guide/authentication.md - -Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages. - -##### 3. Notes - -If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: - - --- - - **Note:** A useful documentation note. - - --- - - -[cite]: https://www.w3.org/People/Berners-Lee/FAQ.html -[code-of-conduct]: https://www.djangoproject.com/conduct/ -[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[so-filter]: https://stackexchange.com/filters/66475/rest-framework -[issues]: https://github.com/encode/django-rest-framework/issues?state=open -[pep-8]: https://www.python.org/dev/peps/pep-0008/ -[pull-requests]: https://help.github.com/articles/using-pull-requests -[tox]: https://tox.readthedocs.io/en/latest/ -[markdown]: https://daringfireball.net/projects/markdown/basics -[docs]: https://github.com/encode/django-rest-framework/tree/master/docs -[mou]: http://mouapp.com/ +See the [Contributing guide in the documentation](https://www.django-rest-framework.org/community/contributing/). diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 566bf95436..0000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -## Checklist - -- [ ] I have verified that that issue exists against the `master` branch of Django REST framework. -- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. -- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.) -- [ ] 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.) -- [ ] I have reduced the issue to the simplest possible case. -- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.) - -## Steps to reproduce - -## Expected behavior - -## Actual behavior diff --git a/MANIFEST.in b/MANIFEST.in index 262e3dc917..5159eeddc7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include README.md include LICENSE.md -recursive-include tests/* * +recursive-include tests/ * recursive-include rest_framework/static *.js *.css *.png *.ico *.eot *.svg *.ttf *.woff *.woff2 recursive-include rest_framework/templates *.html schema.js recursive-include rest_framework/locale *.mo diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 70673c6c16..e9230d5c99 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -*Note*: Before submitting this pull request, please review our [contributing guidelines](https://github.com/encode/django-rest-framework/blob/master/CONTRIBUTING.md#pull-requests). +*Note*: Before submitting this pull request, please review our [contributing guidelines](https://www.django-rest-framework.org/community/contributing/#pull-requests). ## Description diff --git a/README.md b/README.md index 8af1466f8a..18d1364c69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [Django REST framework][docs] -[![build-status-image]][travis] +[![build-status-image]][build-status] [![coverage-status-image]][codecov] [![pypi-version]][pypi] @@ -25,8 +25,10 @@ The initial aim is to provide a single full-time position on REST framework. [![][esg-img]][esg-url] [![][retool-img]][retool-url] [![][bitio-img]][bitio-url] +[![][posthog-img]][posthog-url] +[![][cryptapi-img]][cryptapi-url] -Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [ESG][esg-url], [Retool][retool-url], and [bit.io][bitio-url]. +Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [ESG][esg-url], [Retool][retool-url], [bit.io][bitio-url], [PostHog][posthog-url], and [CryptAPI][cryptapi-url]. --- @@ -52,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements -* Python (3.5, 3.6, 3.7, 3.8, 3.9) -* Django (2.2, 3.0, 3.1) +* Python (3.6, 3.7, 3.8, 3.9, 3.10) +* Django (2.2, 3.0, 3.1, 3.2, 4.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. @@ -113,7 +115,7 @@ router.register(r'users', UserViewSet) # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), ] ``` @@ -131,7 +133,7 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', ] } ``` @@ -168,7 +170,7 @@ Or to create a new user: Full documentation for the project is available at [https://www.django-rest-framework.org/][docs]. -For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode 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]. @@ -176,8 +178,8 @@ You may also want to [follow the author on Twitter][twitter]. Please see the [security policy][security-policy]. -[build-status-image]: https://secure.travis-ci.org/encode/django-rest-framework.svg?branch=master -[travis]: https://travis-ci.org/encode/django-rest-framework?branch=master +[build-status-image]: https://github.com/encode/django-rest-framework/actions/workflows/main.yml/badge.svg +[build-status]: https://github.com/encode/django-rest-framework/actions/workflows/main.yml [coverage-status-image]: https://img.shields.io/codecov/c/github/encode/django-rest-framework/master.svg [codecov]: https://codecov.io/github/encode/django-rest-framework?branch=master [pypi-version]: https://img.shields.io/pypi/v/djangorestframework.svg @@ -195,13 +197,17 @@ Please see the [security policy][security-policy]. [esg-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/esg-readme.png [retool-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/retool-readme.png [bitio-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/bitio-readme.png +[posthog-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/posthog-readme.png +[cryptapi-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cryptapi-readme.png [sentry-url]: https://getsentry.com/welcome/ -[stream-url]: https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf +[stream-url]: https://getstream.io/?utm_source=drf&utm_medium=sponsorship&utm_content=developer [rollbar-url]: https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial [esg-url]: https://software.esg-usa.com/ [retool-url]: https://retool.com/?utm_source=djangorest&utm_medium=sponsorship [bitio-url]: https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship +[posthog-url]: https://posthog.com?utm_source=drf&utm_medium=sponsorship&utm_campaign=open-source-sponsorship +[cryptapi-url]: https://cryptapi.io [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth [oauth2-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 5878040a48..57bbaeb679 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -11,9 +11,9 @@ source: Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The [permission] and [throttling] policies can then use those credentials to determine if the request should be permitted. -REST framework provides a number of authentication schemes out of the box, and also allows you to implement custom schemes. +REST framework provides several authentication schemes out of the box, and also allows you to implement custom schemes. -Authentication is always run at the very start of the view, before the permission and throttling checks occur, and before any other code is allowed to proceed. +Authentication always runs at the very start of the view, before the permission and throttling checks occur, and before any other code is allowed to proceed. The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class. @@ -23,7 +23,7 @@ The `request.auth` property is used for any additional authentication informatio **Note:** Don't forget that **authentication by itself won't allow or disallow an incoming request**, it simply identifies the credentials that the request was made with. -For information on how to setup the permission polices for your API please see the [permissions documentation][permission]. +For information on how to set up the permission policies for your API please see the [permissions documentation][permission]. --- @@ -60,8 +60,8 @@ using the `APIView` class-based views. def get(self, request, format=None): content = { - 'user': unicode(request.user), # `django.contrib.auth.User` instance. - 'auth': unicode(request.auth), # None + 'user': str(request.user), # `django.contrib.auth.User` instance. + 'auth': str(request.auth), # None } return Response(content) @@ -72,8 +72,8 @@ Or, if you're using the `@api_view` decorator with function based views. @permission_classes([IsAuthenticated]) def example_view(request, format=None): content = { - 'user': unicode(request.user), # `django.contrib.auth.User` instance. - 'auth': unicode(request.auth), # None + 'user': str(request.user), # `django.contrib.auth.User` instance. + 'auth': str(request.auth), # None } return Response(content) @@ -195,7 +195,7 @@ If you've already created some users, you can generate tokens for all existing u ##### By exposing an api endpoint -When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf: +When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behaviour. To use it, add the `obtain_auth_token` view to your URLconf: from rest_framework.authtoken import views urlpatterns += [ @@ -210,7 +210,7 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a Note that the default `obtain_auth_token` view explicitly uses JSON requests and responses, rather than using default renderer and parser classes in your settings. -By default there are no permissions or throttling applied to the `obtain_auth_token` view. If you do wish to apply throttling you'll need to override the view class, +By default, there are no permissions or throttling applied to the `obtain_auth_token` view. If you do wish to apply to throttle you'll need to override the view class, and include them using the `throttle_classes` attribute. If you need a customized version of the `obtain_auth_token` view, you can do so by subclassing the `ObtainAuthToken` view class, and using that in your url conf instead. @@ -244,7 +244,7 @@ And in your `urls.py`: ##### With Django admin -It is also possible to create Tokens manually through admin interface. In case you are using a large user base, we recommend that you monkey patch the `TokenAdmin` class to customize it to your needs, more specifically by declaring the `user` field as `raw_field`. +It is also possible to create Tokens manually through the admin interface. In case you are using a large user base, we recommend that you monkey patch the `TokenAdmin` class customize it to your needs, more specifically by declaring the `user` field as `raw_field`. `your_app/admin.py`: @@ -279,11 +279,11 @@ If successfully authenticated, `SessionAuthentication` provides the following cr Unauthenticated responses that are denied permission will result in an `HTTP 403 Forbidden` response. -If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details. +If you're using an AJAX-style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details. **Warning**: Always use Django's standard login view when creating login pages. This will ensure your login views are properly protected. -CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied. +CSRF validation in REST framework works slightly differently from standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied. ## RemoteUserAuthentication @@ -316,7 +316,7 @@ In some circumstances instead of returning `None`, you may want to raise an `Aut Typically the approach you should take is: * If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked. -* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes. +* If authentication is attempted but fails, raise an `AuthenticationFailed` exception. An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes. You *may* also override the `.authenticate_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response. @@ -353,11 +353,11 @@ The following example will authenticate any incoming request as the user given b # Third party packages -The following third party packages are also available. +The following third-party packages are also available. ## Django OAuth Toolkit -The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. +The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. #### Installation & configuration @@ -384,7 +384,7 @@ For more details see the [Django REST framework - Getting started][django-oauth- The [Django REST framework OAuth][django-rest-framework-oauth] package provides both OAuth1 and OAuth2 support for REST framework. -This package was previously included directly in REST framework but is now supported and maintained as a third party package. +This package was previously included directly in the REST framework but is now supported and maintained as a third-party package. #### Installation & configuration @@ -408,7 +408,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a ## Djoser -[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system. +[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and uses token-based authentication. This is ready to use REST implementation of the Django authentication system. ## django-rest-auth / dj-rest-auth @@ -426,11 +426,21 @@ There are currently two forks of this project. ## django-rest-knox -[Django-rest-knox][django-rest-knox] library provides models and views to handle token based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into). +[Django-rest-knox][django-rest-knox] library provides models and views to handle token-based authentication in a more secure and extensible way than the built-in TokenAuthentication scheme - with Single Page Applications and Mobile clients in mind. It provides per-client tokens, and views to generate them when provided some other authentication (usually basic authentication), to delete the token (providing a server enforced logout) and to delete all tokens (logs out all clients that a user is logged into). ## drfpasswordless -[drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's own TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number. +[drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number. + +## django-rest-authemail + +[django-rest-authemail][django-rest-authemail] provides a RESTful API interface for user signup and authentication. Email addresses are used for authentication, rather than usernames. API endpoints are available for signup, signup email verification, login, logout, password reset, password reset verification, email change, email change verification, password change, and user detail. A fully functional example project and detailed instructions are included. + +## Django-Rest-Durin + +[Django-Rest-Durin][django-rest-durin] is built with the idea to have one library that does token auth for multiple Web/CLI/Mobile API clients via one interface but allows different token configuration for each API Client that consumes the API. It provides support for multiple tokens per user via custom models, views, permissions that work with Django-Rest-Framework. The token expiration time can be different per API client and is customizable via the Django Admin Interface. + +More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html). [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 @@ -448,7 +458,7 @@ There are currently two forks of this project. [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [oauth-1.0a]: https://oauth.net/core/1.0a/ [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit -[evonove]: https://github.com/evonove/ +[jazzband]: https://github.com/jazzband/ [oauthlib]: https://github.com/idan/oauthlib [djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt [etoccalino]: https://github.com/etoccalino/ @@ -466,3 +476,5 @@ There are currently two forks of this project. [django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2 [django-rest-knox]: https://github.com/James1345/django-rest-knox [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail +[django-rest-durin]: https://github.com/eshaan7/django-rest-durin diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 96517b15ee..ab4f82cd2f 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -13,13 +13,13 @@ provided in Django. Django provides a [`method_decorator`][decorator] to use decorators with class based views. This can be used with -other cache decorators such as [`cache_page`][page] and -[`vary_on_cookie`][cookie]. +other cache decorators such as [`cache_page`][page], +[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers]. ```python from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page -from django.views.decorators.vary import vary_on_cookie +from django.views.decorators.vary import vary_on_cookie, vary_on_headers from rest_framework.response import Response from rest_framework.views import APIView @@ -27,8 +27,7 @@ from rest_framework import viewsets class UserViewSet(viewsets.ViewSet): - - # Cache requested url for each user for 2 hours + # With cookie: cache requested url for each user for 2 hours @method_decorator(cache_page(60*60*2)) @method_decorator(vary_on_cookie) def list(self, request, format=None): @@ -38,8 +37,18 @@ class UserViewSet(viewsets.ViewSet): return Response(content) -class PostView(APIView): +class ProfileView(APIView): + # With auth: cache requested url for each user for 2 hours + @method_decorator(cache_page(60*60*2)) + @method_decorator(vary_on_headers("Authorization",)) + def get(self, request, format=None): + content = { + 'user_feed': request.user.get_user_feed() + } + return Response(content) + +class PostView(APIView): # Cache page for the requested url @method_decorator(cache_page(60*60*2)) def get(self, request, format=None): @@ -55,4 +64,5 @@ class PostView(APIView): [page]: https://docs.djangoproject.com/en/dev/topics/cache/#the-per-view-cache [cookie]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_cookie +[headers]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_headers [decorator]: https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/#decorating-the-class diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index d7d73a2f2b..e62a7e4f9d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -38,7 +38,7 @@ Might receive an error response indicating that the `DELETE` method is not allow Validation errors are handled slightly differently, and will include the field names as the keys in the response. If the validation error was not specific to a particular field then it will use the "non_field_errors" key, or whatever string value has been set for the `NON_FIELD_ERRORS_KEY` setting. -Any example validation error might look like this: +An example validation error might look like this: HTTP/1.1 400 Bad Request Content-Type: application/json @@ -222,7 +222,7 @@ By default this exception results in a response with the HTTP status code "429 T The `ValidationError` exception is slightly different from the other `APIException` classes: * The `detail` argument is mandatory, not optional. -* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. +* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. By using a dictionary, you can specify field-level errors while performing object-level validation in the `validate()` method of a serializer. For example. `raise serializers.ValidationError({'name': 'Please enter a valid name.'})` * By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')` The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument: diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0492af9aa9..5b9688dcab 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -78,7 +78,14 @@ Defaults to `False` ### `source` -The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`. When serializing fields with dotted notation, it may be necessary to provide a `default` value if any object is not present or is empty during attribute traversal. +The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`. + +When serializing fields with dotted notation, it may be necessary to provide a `default` value if any object is not present or is empty during attribute traversal. Beware of possible n+1 problems when using source attribute if you are accessing a relational orm model. For example: + + class CommentSerializer(serializers.Serializer): + email = serializers.EmailField(source="user.email") + +would require user object to be fetched from database when it is not prefetched. If that is not wanted, be sure to be using `prefetch_related` and `select_related` methods appropriately. For more information about the methods refer to [django documentation][django-docs-select-related]. The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation. @@ -325,7 +332,7 @@ Corresponds to `django.db.models.fields.DateTimeField`. * `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. -* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive. +* `default_timezone` - A `tzinfo` subclass (`zoneinfo` or `pytz`) prepresenting the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive. #### `DateTimeField` format strings. @@ -583,6 +590,7 @@ The serializer method referred to by the `method_name` argument should accept a class Meta: model = User + fields = '__all__' def get_days_since_joined(self, obj): return (now() - obj.date_joined).days @@ -854,3 +862,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [django-hstore]: https://github.com/djangonauts/django-hstore [python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes [django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone +[django-docs-select-related]: https://docs.djangoproject.com/en/3.1/ref/models/querysets/#django.db.models.query.QuerySet.select_related diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index d305ede6ba..512acafbd9 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -75,7 +75,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ by filtering against a `username` query parameter in the URL. """ queryset = Purchase.objects.all() - username = self.request.query_params.get('username', None) + username = self.request.query_params.get('username') if username is not None: queryset = queryset.filter(purchaser__username=username) return queryset @@ -241,7 +241,7 @@ To dynamically change search fields based on request content, it's possible to s def get_search_fields(self, view, request): if request.query_params.get('title_only'): return ['title'] - return super(CustomSearchFilter, self).get_search_fields(view, request) + return super().get_search_fields(view, request) For more details, see the [Django documentation][search-django-admin]. @@ -335,7 +335,7 @@ Generic filters may also present an interface in the browsable API. To do so you The method should return a rendered HTML string. -## Pagination & schemas +## Filtering & schemas You can also make the filter controls available to the schema autogeneration that REST framework provides, by implementing a `get_schema_fields()` method. This method should have the following signature: diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index afc2cab563..fbafec93ad 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -96,6 +96,12 @@ For example: user = self.request.user return user.accounts.all() +--- + +**Note:** If the serializer_class used in the generic view spans orm relations, leading to an n+1 problem, you could optimize your queryset in this method using `select_related` and `prefetch_related`. To get more information about n+1 problem and use cases of the mentioned methods refer to related section in [django documentation][django-docs-select-related]. + +--- + #### `get_object(self)` Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset. @@ -389,3 +395,4 @@ The following third party packages provide additional generic view implementatio [UpdateModelMixin]: #updatemodelmixin [DestroyModelMixin]: #destroymodelmixin [django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels +[django-docs-select-related]: https://docs.djangoproject.com/en/3.1/ref/models/querysets/#django.db.models.query.QuerySet.select_related \ No newline at end of file diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 8d9eb22881..379c1975ad 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -218,10 +218,10 @@ To set these attributes you should override the `CursorPagination` class, and th # Custom pagination styles -To create a custom pagination serializer class you should subclass `pagination.BasePagination` and override the `paginate_queryset(self, queryset, request, view=None)` and `get_paginated_response(self, data)` methods: +To create a custom pagination serializer class, you should inherit the subclass `pagination.BasePagination`, override the `paginate_queryset(self, queryset, request, view=None)`, and `get_paginated_response(self, data)` methods: -* The `paginate_queryset` method is passed the initial queryset and should return an iterable object that contains only the data in the requested page. -* The `get_paginated_response` method is passed the serialized page data and should return a `Response` instance. +* The `paginate_queryset` method is passed to the initial queryset and should return an iterable object. That object contains only the data in the requested page. +* The `get_paginated_response` method is passed to the serialized page data and should return a `Response` instance. Note that the `paginate_queryset` method may set state on the pagination instance, that may later be used by the `get_paginated_response` method. @@ -312,7 +312,7 @@ The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagi ## link-header-pagination -The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as described in [Github's developer documentation](github-link-pagination). +The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as described in [GitHub REST API documentation][github-traversing-with-pagination]. [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [link-header]: ../img/link-header-pagination.png @@ -322,3 +322,4 @@ The [`django-rest-framework-link-header-pagination` package][drf-link-header-pag [drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination [disqus-cursor-api]: https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api [float_cursor_pagination_example]: https://gist.github.com/keturn/8bc88525a183fd41c73ffb729b8865be#file-fpcursorpagination-py +[github-traversing-with-pagination]: https://docs.github.com/en/rest/guides/traversing-with-pagination diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index e8f03de8bd..dde77c3e0e 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -73,7 +73,7 @@ Or, if you're using the `@api_view` decorator with function based views. ## JSONParser -Parses `JSON` request content. +Parses `JSON` request content. `request.data` will be populated with a dictionary of data. **.media_type**: `application/json` diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index ade1462572..5d6462b45d 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -24,9 +24,9 @@ A slightly less strict style of permission would be to allow full access to auth Permissions in REST framework are always defined as a list of permission classes. Before running the main body of the view each permission in the list is checked. -If any permission check fails an `exceptions.PermissionDenied` or `exceptions.NotAuthenticated` exception will be raised, and the main body of the view will not run. +If any permission check fails, an `exceptions.PermissionDenied` or `exceptions.NotAuthenticated` exception will be raised, and the main body of the view will not run. -When the permissions checks fail either a "403 Forbidden" or a "401 Unauthorized" response will be returned, according to the following rules: +When the permission checks fail, either a "403 Forbidden" or a "401 Unauthorized" response will be returned, according to the following rules: * The request was successfully authenticated, but permission was denied. *— An HTTP 403 Forbidden response will be returned.* * The request was not successfully authenticated, and the highest priority authentication class *does not* use `WWW-Authenticate` headers. *— An HTTP 403 Forbidden response will be returned.* @@ -70,6 +70,8 @@ For performance reasons the generic views will not automatically apply object le Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view. +Because the `get_object()` method is not called, object level permissions from the `has_object_permission()` method **are not applied** when creating objects. In order to restrict object creation you need to implement the permission check either in your Serializer class or override the `perform_create()` method of your ViewSet class. + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. @@ -116,7 +118,7 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) -__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file. +__Note:__ when you set new permission classes via the class attribute or decorators you're telling the view to ignore the default list set in the __settings.py__ file. Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written: @@ -169,7 +171,7 @@ This permission is suitable if you want to your API to allow read permissions to ## DjangoModelPermissions -This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. +This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. @@ -179,12 +181,6 @@ The default behaviour can also be overridden to support custom model permissions To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. -#### Using with views that do not include a `queryset` attribute. - -If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sentinel queryset, so that this class can determine the required permissions. For example: - - queryset = User.objects.none() # Required for DjangoModelPermissions - ## DjangoModelPermissionsOrAnonReadOnly Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. @@ -278,6 +274,30 @@ Note that the generic views will check the appropriate object level permissions, Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details. +# Overview of access restriction methods + +REST framework offers three different methods to customize access restrictions on a case-by-case basis. These apply in different scenarios and have different effects and limitations. + + * `queryset`/`get_queryset()`: Limits the general visibility of existing objects from the database. The queryset limits which objects will be listed and which objects can be modified or deleted. The `get_queryset()` method can apply different querysets based on the current action. + * `permission_classes`/`get_permissions()`: General permission checks based on the current action, request and targeted object. Object level permissions can only be applied to retrieve, modify and deletion actions. Permission checks for list and create will be applied to the entire object type. (In case of list: subject to restrictions in the queryset.) + * `serializer_class`/`get_serializer()`: Instance level restrictions that apply to all objects on input and output. The serializer may have access to the request context. The `get_serializer()` method can apply different serializers based on the current action. + +The following table lists the access restriction methods and the level of control they offer over which actions. + +| | `queryset` | `permission_classes` | `serializer_class` | +|------------------------------------|------------|----------------------|--------------------| +| Action: list | global | global | object-level* | +| Action: create | no | global | object-level | +| Action: retrieve | global | object-level | object-level | +| Action: update | global | object-level | object-level | +| Action: partial_update | global | object-level | object-level | +| Action: destroy | global | object-level | no | +| Can reference action in decision | no** | yes | no** | +| Can reference request in decision | no** | yes | yes | + + \* A Serializer class should not raise PermissionDenied in a list action, or the entire list would not be returned.
+ \** The `get_*()` methods have access to the current view and can return different Serializer or QuerySet instances based on the request or action. + --- # Third party packages diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index d3d8b30b8c..4547253b0a 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -17,6 +17,37 @@ Relational fields are used to represent model relationships. They can be applie --- +--- + +**Note:** REST Framework does not attempt to automatically optimize querysets passed to serializers in terms of `select_related` and `prefetch_related` since it would be too much magic. A serializer with a field spanning an orm relation through its source attribute could require an additional database hit to fetch related object from the database. It is the programmer's responsibility to optimize queries to avoid additional database hits which could occur while using such a serializer. + +For example, the following serializer would lead to a database hit each time evaluating the tracks field if it is not prefetched: + + class AlbumSerializer(serializers.ModelSerializer): + tracks = serializers.SlugRelatedField( + many=True, + read_only=True, + slug_field='title' + ) + + class Meta: + model = Album + fields = ['album_name', 'artist', 'tracks'] + + # For each album object, tracks should be fetched from database + qs = Album.objects.all() + print(AlbumSerializer(qs, many=True).data) + +If `AlbumSerializer` is used to serialize a fairly large queryset with `many=True` then it could be a serious performance problem. Optimizing the queryset passed to `AlbumSerializer` with: + + qs = Album.objects.prefetch_related('tracks') + # No additional database hits required + print(AlbumSerializer(qs, many=True).data) + +would solve the issue. + +--- + #### Inspecting relationships. When using the `ModelSerializer` class, serializer fields and relationships will be automatically generated for you. Inspecting these automatically generated fields can be a useful tool for determining how to customize the relationship style. @@ -603,6 +634,6 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re [generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations -[django-intermediary-manytomany]: https://docs.djangoproject.com/en/2.2/topics/db/models/#intermediary-manytomany +[django-intermediary-manytomany]: https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany [dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects [to_internal_value]: https://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index ca3a29b82c..f13b7ba946 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -103,6 +103,16 @@ Unlike other renderers, the data passed to the `Response` does not need to be se The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context. +--- + +**Note:** When used with a view that makes use of a serializer the `Response` sent for rendering may not be a dictionary and will need to be wrapped in a dict before returning to allow the TemplateHTMLRenderer to render it. For example: + +``` +response.data = {'results': response.data} +``` + +--- + The template name is determined by (in order of preference): 1. An explicit `template_name` argument passed to the response. @@ -518,7 +528,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. -[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/#the-rendering-process +[cite]: https://docs.djangoproject.com/en/stable/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md [html-and-forms]: ../topics/html-and-forms.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 1c336953ca..e877c868df 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -23,7 +23,7 @@ REST framework's Request objects provide flexible request parsing that allows yo * It includes all parsed content, including *file and non-file* inputs. * It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. -* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data. +* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming [JSON data] similarly to how you handle incoming [form data]. For more details see the [parsers documentation]. @@ -136,5 +136,7 @@ Note that due to implementation reasons the `Request` class does not inherit fro [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [parsers documentation]: parsers.md +[JSON data]: parsers.md#jsonparser +[form data]: parsers.md#formparser [authentication documentation]: authentication.md [browser enhancements documentation]: ../topics/browser-enhancements.md diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index b4832b3690..b9de6745fe 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -375,6 +375,20 @@ operationIds. In order to work around this, you can override `get_operation_id_base()` to provide a different base for name part of the ID. +#### `get_serializer()` + +If the view has implemented `get_serializer()`, returns the result. + +#### `get_request_serializer()` + +By default returns `get_serializer()` but can be overridden to +differentiate between request and response objects. + +#### `get_response_serializer()` + +By default returns `get_serializer()` but can be overridden to +differentiate between request and response objects. + ### `AutoSchema.__init__()` kwargs `AutoSchema` provides a number of `__init__()` kwargs that can be used for @@ -407,6 +421,7 @@ If your views have related customizations that are needed frequently, you can create a base `AutoSchema` subclass for your project that takes additional `__init__()` kwargs to save subclassing `AutoSchema` for each view. +[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api [openapi]: https://github.com/OAI/OpenAPI-Specification [openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions [openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index fd5dbb0e67..4d032bd9ec 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -116,7 +116,7 @@ Calling `.save()` will either create a new instance, or update an existing insta # .save() will update the existing `comment` instance. serializer = CommentSerializer(comment, data=data) -Both the `.create()` and `.update()` methods are optional. You can implement either neither, one, or both of them, depending on the use-case for your serializer class. +Both the `.create()` and `.update()` methods are optional. You can implement either none, one, or both of them, depending on the use-case for your serializer class. #### Passing additional attributes to `.save()` @@ -282,7 +282,7 @@ If a nested representation may optionally accept the `None` value you should pas content = serializers.CharField(max_length=200) created = serializers.DateTimeField() -Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized. +Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serializer. class CommentSerializer(serializers.Serializer): user = UserSerializer(required=False) @@ -605,13 +605,13 @@ For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`. -### `serializer_url_field` +### `.serializer_url_field` The serializer field class that should be used for any `url` field on the serializer. Defaults to `serializers.HyperlinkedIdentityField` -### `serializer_choice_field` +### `.serializer_choice_field` The serializer field class that should be used for any choice fields on the serializer. @@ -755,6 +755,14 @@ The following argument can also be passed to a `ListSerializer` field or a seria This is `True` by default, but can be set to `False` if you want to disallow empty lists as valid input. +### `max_length` + +This is `None` by default, but can be set to a positive integer if you want to validates that the list contains no more than this number of elements. + +### `min_length` + +This is `None` by default, but can be set to a positive integer if you want to validates that the list contains no fewer than this number of elements. + ### Customizing `ListSerializer` behavior There *are* a few use cases when you might want to customize the `ListSerializer` behavior. For example: @@ -1087,7 +1095,7 @@ For example, if you wanted to be able to set which fields should be used by a se fields = kwargs.pop('fields', None) # Instantiate the superclass normally - super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if fields is not None: # Drop any fields that are not specified in the `fields` argument. diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 73de68a76b..62eb8dd1a5 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -234,7 +234,7 @@ If you're using `SessionAuthentication` then you'll need to include a CSRF token for any `POST`, `PUT`, `PATCH` or `DELETE` requests. You can do so by following the same flow that a JavaScript based client would use. -First make a `GET` request in order to obtain a CRSF token, then present that +First, make a `GET` request in order to obtain a CSRF token, then present that token in the following request. For example... @@ -259,7 +259,7 @@ With careful usage both the `RequestsClient` and the `CoreAPIClient` provide the ability to write test cases that can run either in development, or be run directly against your staging server or production environment. -Using this style to create basic tests of a few core piece of functionality is +Using this style to create basic tests of a few core pieces of functionality is a powerful way to validate your live service. Doing so may require some careful attention to setup and teardown to ensure that the tests run in a way that they do not directly affect customer data. diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 215c735bf4..a3e42cacf9 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -59,7 +59,7 @@ using the `APIView` class-based views. } return Response(content) -Or, if you're using the `@api_view` decorator with function based views. +If you're using the `@api_view` decorator with function based views you can use the following decorator. @api_view(['GET']) @throttle_classes([UserRateThrottle]) @@ -69,6 +69,16 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +It's also possible to set throttle classes for routes that are created using the `@action` decorator. +Throttle classes set in this way will override any viewset level class settings. + + @action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle]) + def example_adhoc_method(request, pk=None): + content = { + 'status': 'request was permitted' + } + return Response(content) + ## How clients are identified The `X-Forwarded-For` HTTP header and `REMOTE_ADDR` WSGI variable are used to uniquely identify client IP addresses for throttling. If the `X-Forwarded-For` header is present then it will be used, otherwise the value of the `REMOTE_ADDR` variable from the WSGI environment will be used. diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 4451489d4d..76dcb0d541 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -238,7 +238,7 @@ In the case of update operations on *nested* serializers there's no way of applying this exclusion, because the instance is not available. Again, you'll probably want to explicitly remove the validator from the -serializer class, and write the code the for the validation constraint +serializer class, and write the code for the validation constraint explicitly, in a `.validate()` method, or in the view. ## Debugging complex cases diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 2224c1f3a5..878a291b22 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -145,6 +145,7 @@ REST framework also allows you to work with regular function based views. It pr The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: from rest_framework.decorators import api_view + from rest_framework.response import Response @api_view() def hello_world(request): diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 22cc3d8aa9..4179725078 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -125,7 +125,7 @@ You may inspect these attributes to adjust behaviour based on the current action if self.action == 'list': permission_classes = [IsAuthenticated] else: - permission_classes = [IsAdmin] + permission_classes = [IsAdminUser] return [permission() for permission in permission_classes] ## Marking extra actions for routing @@ -152,7 +152,7 @@ A more complete example of extra actions: user = self.get_object() serializer = PasswordSerializer(data=request.data) if serializer.is_valid(): - user.set_password(serializer.data['password']) + user.set_password(serializer.validated_data['password']) user.save() return Response({'status': 'password set'}) else: @@ -185,7 +185,7 @@ The decorator allows you to override any viewset-level configuration such as `pe def set_password(self, request, pk=None): ... -The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`. Use the `url_path` and `url_name` parameters to change the URL segement and the reverse URL name of the action. +The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`. Use the `url_path` and `url_name` parameters to change the URL segment and the reverse URL name of the action. To view all extra actions, call the `.get_extra_actions()` method. diff --git a/docs/community/3.13-announcement.md b/docs/community/3.13-announcement.md new file mode 100644 index 0000000000..e2c1fefa64 --- /dev/null +++ b/docs/community/3.13-announcement.md @@ -0,0 +1,55 @@ + + +# Django REST framework 3.13 + +## Django 4.0 support + +The latest release now fully supports Django 4.0. + +Our requirements are now: + +* Python 3.6+ +* Django 4.0, 3.2, 3.1, 2.2 (LTS) + +## Fields arguments are now keyword-only + +When instantiating fields on serializers, you should always use keyword arguments, +such as `serializers.CharField(max_length=200)`. This has always been the case, +and all the examples that we have in the documentation use keyword arguments, +rather than positional arguments. + +From REST framework 3.13 onwards, this is now *explicitly enforced*. + +The most feasible cases where users might be accidentally omitting the keyword arguments +are likely in the composite fields, `ListField` and `DictField`. For instance... + +```python +aliases = serializers.ListField(serializers.CharField()) +``` + +They must now use the more explicit keyword argument style... + +```python +aliases = serializers.ListField(child=serializers.CharField()) +``` + +This change has been made because using positional arguments here *does not* result in the expected behaviour. + +See Pull Request [#7632](https://github.com/encode/django-rest-framework/pull/7632) for more details. diff --git a/docs/community/3.9-announcement.md b/docs/community/3.9-announcement.md index fee6e69096..d673fdd183 100644 --- a/docs/community/3.9-announcement.md +++ b/docs/community/3.9-announcement.md @@ -110,7 +110,7 @@ You can now compose permission classes using the and/or operators, `&` and `|`. For example... ```python -permission_classes = [IsAuthenticated & (ReadOnly | IsAdmin)] +permission_classes = [IsAuthenticated & (ReadOnly | IsAdminUser)] ``` If you're using custom permission classes then make sure that you are subclassing diff --git a/docs/community/contributing.md b/docs/community/contributing.md index cb67100d2b..de1f8db0fb 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -54,11 +54,19 @@ To start developing on Django REST framework, first create a Fork from the Then clone your fork. The clone command will look like this, with your GitHub username instead of YOUR-USERNAME: - git clone https://github.com/YOUR-USERNAME/Spoon-Knife + git clone https://github.com/YOUR-USERNAME/django-rest-framework See GitHub's [_Fork a Repo_][how-to-fork] Guide for more help. Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles. +You can check your contributions against these conventions each time you commit using the [pre-commit](https://pre-commit.com/) hooks, which we also run on CI. +To set them up, first ensure you have the pre-commit tool installed, for example: + + python -m pip install pre-commit + +Then run: + + pre-commit install ## Testing @@ -79,18 +87,6 @@ Run using a more concise output style. ./runtests.py -q -Run the tests using a more concise output style, no coverage, no flake8. - - ./runtests.py --fast - -Don't run the flake8 code linting. - - ./runtests.py --nolint - -Only run the flake8 code linting, don't run the tests. - - ./runtests.py --lintonly - Run the tests for a given test case. ./runtests.py MyTestCase @@ -123,11 +119,11 @@ GitHub's documentation for working on pull requests is [available here][pull-req Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible on all supported versions of Python and Django. -Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect. +Once you've made a pull request take a look at the build status in the GitHub interface and make sure the tests are running as you'd expect. -![Travis status][travis-status] +![Build status][build-status] -*Above: Travis build notifications* +*Above: build notifications* ## Managing compatibility issues @@ -210,7 +206,7 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [so-filter]: https://stackexchange.com/filters/66475/rest-framework [issues]: https://github.com/encode/django-rest-framework/issues?state=open [pep-8]: https://www.python.org/dev/peps/pep-0008/ -[travis-status]: ../img/travis-status.png +[build-status]: ../img/build-status.png [pull-requests]: https://help.github.com/articles/using-pull-requests [tox]: https://tox.readthedocs.io/en/latest/ [markdown]: https://daringfireball.net/projects/markdown/basics diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index c981b9ac92..d3e9dd7cc2 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -34,8 +34,46 @@ You can determine your currently installed version using `pip show`: --- +## 3.13.x series + +### 3.13.0 + +Date: 13th December 2021 + +* Django 4.0 compatability. [#8178] +* Add `max_length` and `min_length` options to `ListSerializer`. [#8165] +* Add `get_request_serializer` and `get_response_serializer` hooks to `AutoSchema`. [#7424] +* Fix OpenAPI representation of null-able read only fields. [#8116] +* Respect `UNICODE_JSON` setting in API schema outputs. [#7991] +* Fix for `RemoteUserAuthentication`. [#7158] +* Make Field constructors keyword-only. [#7632] + +--- + ## 3.12.x series +### 3.12.4 + +Date: 26th March 2021 + +* Revert use of `deque` instead of `list` for tracking throttling `.history`. (Due to incompatibility with DjangoRedis cache backend. See #7870) [#7872] + +### 3.12.3 + +Date: 25th March 2021 + +* Properly handle ATOMIC_REQUESTS when multiple database configurations are used. [#7739] +* Bypass `COUNT` query when `LimitOffsetPagination` is configured but pagination params are not included on the request. [#6098] +* Respect `allow_null=True` on `DecimalField`. [#7718] +* Allow title cased `"Yes"`/`"No"` values with `BooleanField`. [#7739] +* Add `PageNumberPagination.get_page_number()` method for overriding behavior. [#7652] +* Fixed rendering of timedelta values in OpenAPI schemas, when present as default, min, or max fields. [#7641] +* Render JSONFields with indentation in browsable API forms. [#6243] +* Remove unnecessary database query in admin Token views. [#7852] +* Raise validation errors when bools are passed to `PrimaryKeyRelatedField` fields, instead of casting to ints. [#7597] +* Don't include model properties as automatically generated ordering fields with `OrderingFilter`. [#7609] +* Use `deque` instead of `list` for tracking throttling `.history`. [#7849] + ### 3.12.2 Date: 13th October 2020 @@ -177,6 +215,8 @@ Date: 28th September 2020 * Don't strict disallow redundant `SerializerMethodField` field name arguments. * Don't render extra actions in browable API if not authenticated. * Strip null characters from search parameters. +* Deprecate the `detail_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=True)` instead. [gh6687] +* Deprecate the `list_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=False)` instead. [gh6687] ## 3.9.x series @@ -2270,6 +2310,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6680]: https://github.com/encode/django-rest-framework/issues/6680 [gh6317]: https://github.com/encode/django-rest-framework/issues/6317 +[gh6687]: https://github.com/encode/django-rest-framework/issues/6687 [gh6892]: https://github.com/encode/django-rest-framework/issues/6892 diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index d4359890dd..e25421f503 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -14,142 +14,9 @@ We aim to make creating third party packages as easy as possible, whilst keeping If you have an idea for a new feature please consider how it may be packaged as a Third Party Package. We're always happy to discuss ideas on the [Mailing List][discussion-group]. -## How to create a Third Party Package +## Creating a Third Party Package -### Creating your package - -You can use [this cookiecutter template][cookiecutter] for creating reusable Django REST Framework packages quickly. Cookiecutter creates projects from project templates. While optional, this cookiecutter template includes best practices from Django REST framework and other packages, as well as a Travis CI configuration, Tox configuration, and a sane setup.py for easy PyPI registration/distribution. - -Note: Let us know if you have an alternate cookiecutter package so we can also link to it. - -#### Running the initial cookiecutter command - -To run the initial cookiecutter command, you'll first need to install the Python `cookiecutter` package. - - $ pip install cookiecutter - -Once `cookiecutter` is installed just run the following to create a new project. - - $ cookiecutter gh:jpadilla/cookiecutter-django-rest-framework - -You'll be prompted for some questions, answer them, then it'll create your Python package in the current working directory based on those values. - - full_name (default is "Your full name here")? Johnny Appleseed - email (default is "you@example.com")? jappleseed@example.com - github_username (default is "yourname")? jappleseed - pypi_project_name (default is "dj-package")? djangorestframework-custom-auth - repo_name (default is "dj-package")? django-rest-framework-custom-auth - app_name (default is "djpackage")? custom_auth - project_short_description (default is "Your project description goes here")? - year (default is "2014")? - version (default is "0.1.0")? - -#### Getting it onto GitHub - -To put your project up on GitHub, you'll need a repository for it to live in. You can create a new repository [here][new-repo]. If you need help, check out the [Create A Repo][create-a-repo] article on GitHub. - - -#### Adding to Travis CI - -We recommend using [Travis CI][travis-ci], a hosted continuous integration service which integrates well with GitHub and is free for public repositories. - -To get started with Travis CI, [sign in][travis-ci] with your GitHub account. Once you're signed in, go to your [profile page][travis-profile] and enable the service hook for the repository you want. - -If you use the cookiecutter template, your project will already contain a `.travis.yml` file which Travis CI will use to build your project and run tests. By default, builds are triggered every time you push to your repository or create Pull Request. - -#### Uploading to PyPI - -Once you've got at least a prototype working and tests running, you should publish it on PyPI to allow others to install it via `pip`. - -You must [register][pypi-register] an account before publishing to PyPI. - -To register your package on PyPI run the following command. - - $ python setup.py register - -If this is the first time publishing to PyPI, you'll be prompted to login. - -Note: Before publishing you'll need to make sure you have the latest pip that supports `wheel` as well as install the `wheel` package. - - $ pip install --upgrade pip - $ pip install wheel - -After this, every time you want to release a new version on PyPI just run the following command. - - $ python setup.py publish - You probably want to also tag the version now: - git tag -a {0} -m 'version 0.1.0' - git push --tags - -After releasing a new version to PyPI, it's always a good idea to tag the version and make available as a GitHub Release. - -We recommend to follow [Semantic Versioning][semver] for your package's versions. - -### Development - -#### Version requirements - -The cookiecutter template assumes a set of supported versions will be provided for Python and Django. Make sure you correctly update your requirements, docs, `tox.ini`, `.travis.yml`, and `setup.py` to match the set of versions you wish to support. - -#### Tests - -The cookiecutter template includes a `runtests.py` which uses the `pytest` package as a test runner. - -Before running, you'll need to install a couple test requirements. - - $ pip install -r requirements.txt - -Once requirements installed, you can run `runtests.py`. - - $ ./runtests.py - -Run using a more concise output style. - - $ ./runtests.py -q - -Run the tests using a more concise output style, no coverage, no flake8. - - $ ./runtests.py --fast - -Don't run the flake8 code linting. - - $ ./runtests.py --nolint - -Only run the flake8 code linting, don't run the tests. - - $ ./runtests.py --lintonly - -Run the tests for a given test case. - - $ ./runtests.py MyTestCase - -Run the tests for a given test method. - - $ ./runtests.py MyTestCase.test_this_method - -Shorter form to run the tests for a given test method. - - $ ./runtests.py test_this_method - -To run your tests against multiple versions of Python as different versions of requirements such as Django we recommend using `tox`. [Tox][tox-docs] is a generic virtualenv management and test command line tool. - -First, install `tox` globally. - - $ pip install tox - -To run `tox`, just simply run: - - $ tox - -To run a particular `tox` environment: - - $ tox -e envlist - -`envlist` is a comma-separated value to that specifies the environments to run tests against. To view a list of all possible test environments, run: - - $ tox -l - -#### Version compatibility +### Version compatibility Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into a `compat.py` module, and should provide a single common interface that the rest of the codebase can use. @@ -187,9 +54,10 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [hawkrest][hawkrest] - Provides Hawk HTTP Authorization. * [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism. * [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. -* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. +* [dj-rest-auth][dj-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. * [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF. * [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers. +* [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses. ### Permissions @@ -214,17 +82,19 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-action-serializer][drf-action-serializer] - Serializer providing per-action fields config for use with ViewSets to prevent having to write multiple serializers. * [djangorestframework-dataclasses][djangorestframework-dataclasses] - Serializer providing automatic field generation for Python dataclasses, like the built-in ModelSerializer does for models. * [django-restql][django-restql] - Turn your REST API into a GraphQL like API(It allows clients to control which fields will be sent in a response, uses GraphQL like syntax, supports read and write on both flat and nested fields). +* [graphwrap][graphwrap] - Transform your REST API into a fully compliant GraphQL API with just two lines of code. Leverages [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/) to dynamically build, at runtime, a GraphQL ObjectType for each view in your API. ### Serializer fields * [drf-compound-fields][drf-compound-fields] - Provides "compound" serializer fields, such as lists of simple values. -* [django-extra-fields][django-extra-fields] - Provides extra serializer fields. +* [drf-extra-fields][drf-extra-fields] - Provides extra serializer fields. * [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs]. ### Views * [django-rest-multiple-models][django-rest-multiple-models] - Provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request. * [drf-typed-views][drf-typed-views] - Use Python type annotations to validate/deserialize request parameters. Inspired by API Star, Hug and FastAPI. +* [rest-framework-actions][rest-framework-actions] - Provides control over each action in ViewSets. Serializers per action, method. ### Routers @@ -236,6 +106,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-msgpack][djangorestframework-msgpack] - Provides MessagePack renderer and parser support. * [djangorestframework-jsonapi][djangorestframework-jsonapi] - Provides a parser, renderer, serializers, and other tools to help build an API that is compliant with the jsonapi.org spec. * [djangorestframework-camel-case][djangorestframework-camel-case] - Provides camel case JSON renderers and parsers. +* [nested-multipart-parser][nested-multipart-parser] - Provides nested parser for http multipart request ### Renderers @@ -276,13 +147,12 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-features][djangorestframework-features] - Advanced schema generation and more based on named features. * [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons. * [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models.. +* [fast-drf] - A model based library for making API development faster and easier. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework [new-repo]: https://github.com/new [create-a-repo]: https://help.github.com/articles/create-a-repo/ -[travis-ci]: https://travis-ci.org -[travis-profile]: https://travis-ci.org/profile [pypi-register]: https://pypi.org/account/register/ [semver]: https://semver.org/ [tox-docs]: https://tox.readthedocs.io/en/latest/ @@ -308,12 +178,13 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-gis]: https://github.com/djangonauts/django-rest-framework-gis [djangorestframework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [drf-compound-fields]: https://github.com/estebistec/drf-compound-fields -[django-extra-fields]: https://github.com/Hipo/drf-extra-fields +[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields [django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db.rest]: https://wq.io/docs/about-rest [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case +[nested-multipart-parser]: https://github.com/remigermain/nested-multipart-parser [djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv [drf_ujson2]: https://github.com/Amertz08/drf_ujson2 [rest-pandas]: https://github.com/wq/django-rest-pandas @@ -324,7 +195,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework [drf-extensions]: https://github.com/chibisov/drf-extensions [ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter -[django-rest-auth]: https://github.com/Tivix/django-rest-auth/ +[dj-rest-auth]: https://github.com/iMerica/dj-rest-auth [django-versatileimagefield]: https://github.com/WGBH/django-versatileimagefield [django-versatileimagefield-drf-docs]:https://django-versatileimagefield.readthedocs.io/en/latest/drf_integration.html [drf-tracking]: https://github.com/aschn/drf-tracking @@ -362,3 +233,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-elasticsearch-dsl-drf]: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf [django-api-client]: https://github.com/rhenter/django-api-client [drf-psq]: https://github.com/drf-psq/drf-psq +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail +[graphwrap]: https://github.com/PaulGilmartin/graph_wrap +[rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions +[fast-drf]: https://github.com/iashraful/fast-drf diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index cfd3ba852e..23faf79128 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -76,6 +76,7 @@ There are a wide range of resources available for learning and using Django REST * [Chatbot Using Django REST Framework + api.ai + Slack — Part 1/3][chatbot-using-drf-part1] * [New Django Admin with DRF and EmberJS... What are the News?][new-django-admin-with-drf-and-emberjs] * [Blog posts about Django REST Framework][medium-django-rest-framework] +* [Implementing Rest APIs With Embedded Privacy][doordash-implementing-rest-apis] ### Documentations * [Classy Django REST Framework][cdrf.co] @@ -95,7 +96,7 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [ember-and-django-part 1-video]: http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1 [django-rest-framework-part-1-video]: http://www.neckbeardrepublic.com/screencasts/django-rest-framework-part-1 [web-api-performance-profiling-django-rest-framework]: https://www.dabapps.com/blog/api-performance-profiling-django-rest-framework/ -[api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/ +[api-development-with-django-and-django-rest-framework]: https://bnotions.com/news-and-insights/api-development-with-django-and-django-rest-framework/ [cdrf.co]:http://www.cdrf.co [medium-django-rest-framework]: https://medium.com/django-rest-framework [django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework @@ -128,3 +129,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [anna-email]: mailto:anna@django-rest-framework.org [pycon-us-2017]: https://www.youtube.com/watch?v=Rk6MHZdust4 [django-rest-react-valentinog]: https://www.valentinog.com/blog/tutorial-api-django-rest-react/ +[doordash-implementing-rest-apis]: https://doordash.engineering/2013/10/07/implementing-rest-apis-with-embedded-privacy/ diff --git a/docs/coreapi/7-schemas-and-client-libraries.md b/docs/coreapi/7-schemas-and-client-libraries.md index 203d81ea5d..d95019dab6 100644 --- a/docs/coreapi/7-schemas-and-client-libraries.md +++ b/docs/coreapi/7-schemas-and-client-libraries.md @@ -1,5 +1,16 @@ # Tutorial 7: Schemas & client libraries +---- + +**DEPRECATION NOTICE:** Use of CoreAPI-based schemas were deprecated with the introduction of native OpenAPI-based schema generation as of Django REST Framework v3.10. See the [Version 3.10 Release Announcement](../community/3.10-announcement.md) for more details. + +If you are looking for information regarding schemas, you might want to look at these updated resources: + +1. [Schema](../api-guide/schemas.md) +2. [Documenting your API](../topics/documenting-your-api.md) + +---- + A schema is a machine-readable document that describes the available API endpoints, their URLS, and what operations they support. diff --git a/docs/coreapi/from-documenting-your-api.md b/docs/coreapi/from-documenting-your-api.md index 604dfa6686..65ad71c7a7 100644 --- a/docs/coreapi/from-documenting-your-api.md +++ b/docs/coreapi/from-documenting-your-api.md @@ -1,6 +1,17 @@ ## Built-in API documentation +---- + +**DEPRECATION NOTICE:** Use of CoreAPI-based schemas were deprecated with the introduction of native OpenAPI-based schema generation as of Django REST Framework v3.10. See the [Version 3.10 Release Announcement](../community/3.10-announcement.md) for more details. + +If you are looking for information regarding schemas, you might want to look at these updated resources: + +1. [Schema](../api-guide/schemas.md) +2. [Documenting your API](../topics/documenting-your-api.md) + +---- + The built-in API documentation includes: * Documentation of API endpoints. diff --git a/docs/coreapi/index.md b/docs/coreapi/index.md index 9195eb33e4..dbcb115840 100644 --- a/docs/coreapi/index.md +++ b/docs/coreapi/index.md @@ -1,8 +1,8 @@ # Legacy CoreAPI Schemas Docs -Use of CoreAPI-based schemas were deprecated with the introduction of native OpenAPI-based schema generation in Django REST Framework v3.10. +Use of CoreAPI-based schemas were deprecated with the introduction of native OpenAPI-based schema generation as of Django REST Framework v3.10. -See the [Version 3.10 Release Announcement](/community/3.10-announcement.md) for more details. +See the [Version 3.10 Release Announcement](../community/3.10-announcement.md) for more details. ---- diff --git a/docs/coreapi/schemas.md b/docs/coreapi/schemas.md index 653105a7a1..9f1482d2d8 100644 --- a/docs/coreapi/schemas.md +++ b/docs/coreapi/schemas.md @@ -2,6 +2,14 @@ source: schemas.py # Schemas +---- + +**DEPRECATION NOTICE:** Use of CoreAPI-based schemas were deprecated with the introduction of native OpenAPI-based schema generation as of Django REST Framework v3.10. See the [Version 3.10 Release Announcement](../community/3.10-announcement.md) for more details. + +You are probably looking for [this page](../api-guide/schemas.md) if you want latest information regarding schemas. + +---- + > A machine-readable [schema] describes what resources are available via the API, what their URLs are, how they are represented and what operations they support. > > — Heroku, [JSON Schema for the Heroku Platform API][cite] diff --git a/docs/img/build-status.png b/docs/img/build-status.png new file mode 100644 index 0000000000..bb043cb9e9 Binary files /dev/null and b/docs/img/build-status.png differ diff --git a/docs/img/premium/cryptapi-readme.png b/docs/img/premium/cryptapi-readme.png new file mode 100644 index 0000000000..163f6a9ea2 Binary files /dev/null and b/docs/img/premium/cryptapi-readme.png differ diff --git a/docs/img/premium/posthog-readme.png b/docs/img/premium/posthog-readme.png new file mode 100644 index 0000000000..9ca8b0ecf0 Binary files /dev/null and b/docs/img/premium/posthog-readme.png differ diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png deleted file mode 100644 index fec98cf9b2..0000000000 Binary files a/docs/img/travis-status.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index 0273da9f14..2954f793ac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,8 +20,8 @@

- - + + @@ -67,15 +67,17 @@ continued development by **[signing up for a paid plan][funding]**.

-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), and [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=sponsorship&utm_content=developer), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship), [bit.io](https://bit.io/jobs?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), [PostHog](https://posthog.com?utm_source=DRF&utm_medium=sponsor&utm_campaign=DRF_sponsorship), and [CryptAPI](https://cryptapi.io).* --- @@ -83,8 +85,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Python (3.5, 3.6, 3.7, 3.8, 3.9) -* Django (2.2, 3.0, 3.1) +* Python (3.6, 3.7, 3.8, 3.9, 3.10) +* Django (2.2, 3.0, 3.1, 3.2, 4.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. @@ -186,15 +188,10 @@ Framework. ## Support -For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. +For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.libera.chat`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/). -For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter. - -Follow @_tomchristie - - ## Security If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**. @@ -262,7 +259,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [funding]: community/funding.md [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[botbot]: https://botbot.me/freenode/restframework/ [stack-overflow]: https://stackoverflow.com/ [django-rest-framework-tag]: https://stackoverflow.com/questions/tagged/django-rest-framework [security-mail]: mailto:rest-framework-security@googlegroups.com diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index 646f3f5638..a65e3fdf8d 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -31,11 +31,11 @@ In order to make AJAX requests, you need to include CSRF token in the HTTP heade The best way to deal with CORS in REST framework is to add the required response headers in middleware. This ensures that CORS is supported transparently, without having to change any behavior in your views. -[Otto Yiu][ottoyiu] maintains the [django-cors-headers] package, which is known to work correctly with REST framework APIs. +[Adam Johnson][adamchainz] maintains the [django-cors-headers] package, which is known to work correctly with REST framework APIs. [cite]: https://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/ [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) [csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax [cors]: https://www.w3.org/TR/cors/ -[ottoyiu]: https://github.com/ottoyiu/ -[django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/ +[adamchainz]: https://github.com/adamchainz +[django-cors-headers]: https://github.com/adamchainz/django-cors-headers diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 9b61eaf427..b9f5e3ecd8 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -453,7 +453,7 @@ For example, using the "Django REST framework JWT" package function loginUser(username, password) { let action = ["api-token-auth", "obtain-token"]; - let params = {username: "example", email: "example@example.com"}; + let params = {username: username, password: password}; client.action(schema, action, params).then(function(result) { // On success, instantiate an authenticated client. let auth = window.coreapi.auth.TokenAuthentication({ diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index cd7e5098fe..5eabeee7bb 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -202,7 +202,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `op meta = self.metadata_class() data = meta.determine_metadata(request, self) data.pop('description') - return data + return Response(data=data, status=status.HTTP_200_OK) See [the Metadata docs][metadata-docs] for more details. diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md index 7cfc6e247c..c20cf9e339 100644 --- a/docs/topics/internationalization.md +++ b/docs/topics/internationalization.md @@ -103,10 +103,10 @@ You can find more information on how the language preference is determined in th For API clients the most appropriate of these will typically be to use the `Accept-Language` header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an `Accept-Language` header for API clients rather than using language URL prefixes. [cite]: https://youtu.be/Wa0VfS2q94Y -[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation +[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling [transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ [django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po -[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference -[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS -[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name +[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference +[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS +[django-locale-name]: https://docs.djangoproject.com/en/stable/topics/i18n/#term-locale-name diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 85d8676b1d..908b7474a0 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -374,5 +374,5 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2]. [sandbox]: https://restframework.herokuapp.com/ [venv]: https://docs.python.org/3/library/venv.html [tut-2]: 2-requests-and-responses.md -[httpie]: https://github.com/jakubroztocil/httpie#installation +[httpie]: https://github.com/httpie/httpie#installation [curl]: https://curl.haxx.se/ diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 79ce355c93..cb0321ea21 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -38,7 +38,7 @@ And now we can add a `.save()` method to our model class: formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options) self.highlighted = highlight(self.code, lexer, formatter) - super(Snippet, self).save(*args, **kwargs) + super().save(*args, **kwargs) When that's all done we'll need to update our database tables. Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 4cd4e9bbd5..f999fdf507 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -31,7 +31,6 @@ The other thing we need to consider when creating the code highlight view is tha Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your `snippets/views.py` add: from rest_framework import renderers - from rest_framework.response import Response class SnippetHighlight(generics.GenericAPIView): queryset = Snippet.objects.all() @@ -143,7 +142,7 @@ We can change the default list style to use pagination, by modifying our `tutori Note that settings in REST framework are all namespaced into a single dictionary setting, named `REST_FRAMEWORK`, which helps keep them well separated from your other project settings. -We could also customize the pagination style if we needed too, but in this case we'll just stick with the default. +We could also customize the pagination style if we needed to, but in this case we'll just stick with the default. ## Browsing the API diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index ee839790f1..f4dcc5606c 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -225,4 +225,4 @@ If you want to get a more in depth understanding of how REST framework fits toge [image]: ../img/quickstart.png [tutorial]: 1-serialization.md [guide]: ../api-guide/requests.md -[httpie]: https://github.com/jakubroztocil/httpie#installation +[httpie]: https://httpie.io/docs#installation diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 992bc60a42..7006f2a668 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -37,7 +37,7 @@ body.index-page #main-content iframe.github-star-button { margin-right: -15px; } -/* Travis CI and PyPI badge */ +/* CI and PyPI badge */ body.index-page #main-content img.status-badge { float: right; margin-right: 8px; diff --git a/mkdocs.yml b/mkdocs.yml index 573898bca0..439245a8d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.13 Announcement': 'community/3.13-announcement.md' - '3.12 Announcement': 'community/3.12-announcement.md' - '3.11 Announcement': 'community/3.11-announcement.md' - '3.10 Announcement': 'community/3.10-announcement.md' diff --git a/requirements.txt b/requirements.txt index b4e5ff5797..395f3b7a86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ -r requirements/requirements-optionals.txt -r requirements/requirements-testing.txt -r requirements/requirements-documentation.txt --r requirements/requirements-codestyle.txt -r requirements/requirements-packaging.txt diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt deleted file mode 100644 index 4f54d6e778..0000000000 --- a/requirements/requirements-codestyle.txt +++ /dev/null @@ -1,7 +0,0 @@ -# PEP8 code linting, which we run on all commits. -flake8==3.8.3 -flake8-tidy-imports==4.1.0 -pycodestyle==2.6.0 - -# Sort and lint imports -isort==5.4.2 diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index e969ff471b..ad49287304 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,2 +1,2 @@ # MkDocs to build our documentation. -mkdocs==1.1 +mkdocs>=1.1.2,<1.2 diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 2b7a18a13f..75b9ab4d60 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,9 +1,10 @@ # Optional packages which may be used with REST framework. -psycopg2-binary>=2.8.5, <2.9 -markdown==3.1.1 -pygments==2.4.2 -django-guardian==2.2.0 -django-filter>=2.2.0, <2.3 coreapi==2.3.1 coreschema==0.0.4 -pyyaml>=5.1 +django-filter>=2.4.0,<3.0 +django-guardian>=2.4.0,<2.5 +markdown==3.3;python_version>="3.6" +markdown==3.2.2;python_version=="3.5" +psycopg2-binary>=2.8.5,<2.9 +pygments>=2.7.1,<2.8 +pyyaml>=5.3.1,<5.4 diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index 091622fbeb..fae03baab5 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -1,8 +1,8 @@ # Wheel for PyPI installs. -wheel==0.34.2 +wheel>=0.35.1,<0.36 # Twine for secured PyPI uploads. -twine==3.1.1 +twine>=3.2.0,<3.3 # Transifex client for managing translation resources. -transifex-client==0.13.9 +transifex-client>=0.13.12,<0.14 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index ad246e8570..313fdedc9b 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,4 +1,4 @@ # Pytest for running the tests. -pytest>=5.4.1,<5.5 -pytest-django>=3.9.0,<3.10 -pytest-cov>=2.7.1 +pytest>=6.1,<7.0 +pytest-cov>=2.10.1,<3.0 +pytest-django>=4.1.0,<5.0 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 7ff188a5ad..88d86c03e5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -10,7 +10,7 @@ import django __title__ = 'Django REST framework' -__version__ = '3.12.2' +__version__ = '3.13.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 9111007c09..382abf1580 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -227,6 +227,6 @@ class RemoteUserAuthentication(BaseAuthentication): header = "REMOTE_USER" def authenticate(self, request): - user = authenticate(remote_user=request.META.get(self.header)) + user = authenticate(request=request, remote_user=request.META.get(self.header)) if user and user.is_active: return (user, None) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 540049295d..5a143d936c 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -46,7 +46,7 @@ class TokenProxy(Token): """ @property def pk(self): - return self.user.pk + return self.user_id class Meta: proxy = 'rest_framework.authtoken' in settings.INSTALLED_APPS diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 30b9d84d4e..7ba43d37c8 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -7,6 +7,7 @@ methods on viewsets that should be included by routers. """ import types +from functools import update_wrapper from django.forms.utils import pretty_name @@ -22,18 +23,8 @@ def api_view(http_method_names=None): def decorator(func): - WrappedAPIView = type( - 'WrappedAPIView', - (APIView,), - {'__doc__': func.__doc__} - ) - - # Note, the above allows us to set the docstring. - # It is the equivalent of: - # - # class WrappedAPIView(APIView): - # pass - # WrappedAPIView.__doc__ = func.doc <--- Not possible to do this + class WrappedAPIView(APIView): + pass # api_view applied without (method_names) assert not(isinstance(http_method_names, types.FunctionType)), \ @@ -52,9 +43,6 @@ def handler(self, *args, **kwargs): for method in http_method_names: setattr(WrappedAPIView, method.lower(), handler) - WrappedAPIView.__name__ = func.__name__ - WrappedAPIView.__module__ = func.__module__ - WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes', APIView.renderer_classes) @@ -73,7 +61,7 @@ def handler(self, *args, **kwargs): WrappedAPIView.schema = getattr(func, 'schema', APIView.schema) - return WrappedAPIView.as_view() + return update_wrapper(WrappedAPIView.as_view(), func) return decorator diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 943dcc88c3..fee8f024f2 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -20,7 +20,7 @@ def _get_error_details(data, default_code=None): Descend into a nested data structure, forcing any lazy translation strings or strings into `ErrorDetail`. """ - if isinstance(data, list): + if isinstance(data, (list, tuple)): ret = [ _get_error_details(item, default_code) for item in data ] @@ -150,7 +150,9 @@ def __init__(self, detail=None, code=None): # For validation failures, we may collect many errors together, # so the details should always be coerced to a list if not already. - if not isinstance(detail, dict) and not isinstance(detail, list): + if isinstance(detail, tuple): + detail = list(detail) + elif not isinstance(detail, dict) and not isinstance(detail, list): detail = [detail] self.detail = _get_error_details(detail, code) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fdfba13f26..d7e7816cee 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -320,7 +320,7 @@ class Field: default_empty_html = empty initial = None - def __init__(self, read_only=False, write_only=False, + def __init__(self, *, read_only=False, write_only=False, required=None, default=empty, initial=empty, source=None, label=None, help_text=None, style=None, error_messages=None, validators=None, allow_null=False): @@ -704,7 +704,7 @@ class BooleanField(Field): initial = False TRUE_VALUES = { 't', 'T', - 'y', 'Y', 'yes', 'YES', + 'y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE', 'on', 'On', 'ON', '1', 1, @@ -712,7 +712,7 @@ class BooleanField(Field): } FALSE_VALUES = { 'f', 'F', - 'n', 'N', 'no', 'NO', + 'n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE', 'off', 'Off', 'OFF', '0', 0, 0.0, @@ -1046,6 +1046,11 @@ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value= 'Invalid rounding option %s. Valid values for rounding are: %s' % (rounding, valid_roundings)) self.rounding = rounding + def validate_empty_values(self, data): + if smart_str(data).strip() == '' and self.allow_null: + return (True, None) + return super().validate_empty_values(data) + def to_internal_value(self, data): """ Validate that the input is a decimal number and return a Decimal @@ -1112,6 +1117,12 @@ def validate_precision(self, value): def to_representation(self, value): coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING) + if value is None: + if coerce_to_string: + return '' + else: + return None + if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value).strip()) @@ -1152,14 +1163,14 @@ class DateTimeField(Field): } datetime_parser = datetime.datetime.strptime - def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): + def __init__(self, format=empty, input_formats=None, default_timezone=None, **kwargs): if format is not empty: self.format = format if input_formats is not None: self.input_formats = input_formats if default_timezone is not None: self.timezone = default_timezone - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def enforce_timezone(self, value): """ @@ -1238,12 +1249,12 @@ class DateField(Field): } datetime_parser = datetime.datetime.strptime - def __init__(self, format=empty, input_formats=None, *args, **kwargs): + def __init__(self, format=empty, input_formats=None, **kwargs): if format is not empty: self.format = format if input_formats is not None: self.input_formats = input_formats - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) @@ -1304,12 +1315,12 @@ class TimeField(Field): } datetime_parser = datetime.datetime.strptime - def __init__(self, format=empty, input_formats=None, *args, **kwargs): + def __init__(self, format=empty, input_formats=None, **kwargs): if format is not empty: self.format = format if input_formats is not None: self.input_formats = input_formats - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.TIME_INPUT_FORMATS) @@ -1459,9 +1470,9 @@ class MultipleChoiceField(ChoiceField): } default_empty_html = [] - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.allow_empty = kwargs.pop('allow_empty', True) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def get_value(self, dictionary): if self.field_name not in dictionary: @@ -1480,6 +1491,8 @@ def to_internal_value(self, data): self.fail('empty') return { + # Arguments for super() are needed because of scoping inside + # comprehensions. super(MultipleChoiceField, self).to_internal_value(item) for item in data } @@ -1518,12 +1531,12 @@ class FileField(Field): 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.max_length = kwargs.pop('max_length', None) self.allow_empty_file = kwargs.pop('allow_empty_file', False) if 'use_url' in kwargs: self.use_url = kwargs.pop('use_url') - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): try: @@ -1567,9 +1580,9 @@ class ImageField(FileField): ), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def to_internal_value(self, data): # Image validation is a bit grungy, so we'll just outright @@ -1584,8 +1597,8 @@ def to_internal_value(self, data): # Composite field types... class _UnvalidatedField(Field): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.allow_blank = True self.allow_null = True @@ -1606,7 +1619,7 @@ class ListField(Field): 'max_length': _('Ensure this field has no more than {max_length} elements.') } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) self.max_length = kwargs.pop('max_length', None) @@ -1618,7 +1631,7 @@ def __init__(self, *args, **kwargs): "Remove `source=` from the field declaration." ) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.child.bind(field_name='', parent=self) if self.max_length is not None: message = lazy_format(self.error_messages['max_length'], max_length=self.max_length) @@ -1683,7 +1696,7 @@ class DictField(Field): 'empty': _('This dictionary may not be empty.'), } - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) @@ -1693,7 +1706,7 @@ def __init__(self, *args, **kwargs): "Remove `source=` from the field declaration." ) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.child.bind(field_name='', parent=self) def get_value(self, dictionary): @@ -1742,8 +1755,8 @@ def run_child_validation(self, data): class HStoreField(DictField): child = CharField(allow_blank=True, allow_null=True) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) assert isinstance(self.child, CharField), ( "The `child` argument must be an instance of `CharField`, " "as the hstore extension stores values as strings." @@ -1755,11 +1768,14 @@ class JSONField(Field): 'invalid': _('Value must be valid JSON.') } - def __init__(self, *args, **kwargs): + # Workaround for isinstance calls when importing the field isn't possible + _is_jsonfield = True + + def __init__(self, **kwargs): self.binary = kwargs.pop('binary', False) self.encoder = kwargs.pop('encoder', None) self.decoder = kwargs.pop('decoder', None) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) def get_value(self, dictionary): if html.is_html_input(dictionary) and self.field_name in dictionary: diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 3665775195..1ffd9edc02 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -226,10 +226,20 @@ def get_default_valid_fields(self, queryset, view, context={}): ) raise ImproperlyConfigured(msg % self.__class__.__name__) + model_class = queryset.model + model_property_names = [ + # 'pk' is a property added in Django's Model class, however it is valid for ordering. + attr for attr in dir(model_class) if isinstance(getattr(model_class, attr), property) and attr != 'pk' + ] + return [ (field.source.replace('.', '__') or field_name, field.label) for field_name, field in serializer_class(context=context).fields.items() - if not getattr(field, 'write_only', False) and not field.source == '*' + if ( + not getattr(field, 'write_only', False) and + not field.source == '*' and + field.source not in model_property_names + ) ] def get_valid_fields(self, queryset, view, context={}): diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 60a57c8e4a..dc120d8e86 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -198,9 +198,7 @@ def paginate_queryset(self, queryset, request, view=None): return None paginator = self.django_paginator_class(queryset, page_size) - page_number = request.query_params.get(self.page_query_param, 1) - if page_number in self.last_page_strings: - page_number = paginator.num_pages + page_number = self.get_page_number(request, paginator) try: self.page = paginator.page(page_number) @@ -217,6 +215,12 @@ def paginate_queryset(self, queryset, request, view=None): self.request = request return list(self.page) + def get_page_number(self, request, paginator): + page_number = request.query_params.get(self.page_query_param, 1) + if page_number in self.last_page_strings: + page_number = paginator.num_pages + return page_number + def get_paginated_response(self, data): return Response(OrderedDict([ ('count', self.page.paginator.count), @@ -376,11 +380,11 @@ class LimitOffsetPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' def paginate_queryset(self, queryset, request, view=None): - self.count = self.get_count(queryset) self.limit = self.get_limit(request) if self.limit is None: return None + self.count = self.get_count(queryset) self.offset = self.get_offset(request) self.request = request if self.count > self.limit and self.template is not None: @@ -484,8 +488,7 @@ def get_html_context(self): _divide_with_ceil(self.offset, self.limit) ) - if final < 1: - final = 1 + final = max(final, 1) else: current = 1 final = 1 diff --git a/rest_framework/relations.py b/rest_framework/relations.py index eaf27e1d96..c987007842 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -104,11 +104,11 @@ def __init__(self, **kwargs): self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) ) if not method_overridden('get_queryset', RelatedField, self): - assert self.queryset is not None or kwargs.get('read_only', None), ( + assert self.queryset is not None or kwargs.get('read_only'), ( 'Relational field must provide a `queryset` argument, ' 'override `get_queryset`, or set read_only=`True`.' ) - assert not (self.queryset is not None and kwargs.get('read_only', None)), ( + assert not (self.queryset is not None and kwargs.get('read_only')), ( 'Relational fields should not provide a `queryset` argument, ' 'when setting read_only=`True`.' ) @@ -259,6 +259,8 @@ def to_internal_value(self, data): data = self.pk_field.to_internal_value(data) queryset = self.get_queryset() try: + if isinstance(data, bool): + raise TypeError return queryset.get(pk=data) except ObjectDoesNotExist: self.fail('does_not_exist', pk_value=data) @@ -337,7 +339,7 @@ def get_url(self, obj, view_name, request, format): return self.reverse(view_name, kwargs=kwargs, request=request, format=format) def to_internal_value(self, data): - request = self.context.get('request', None) + request = self.context.get('request') try: http_prefix = data.startswith(('http:', 'https:')) except AttributeError: @@ -380,7 +382,7 @@ def to_representation(self, value): ) request = self.context['request'] - format = self.context.get('format', None) + format = self.context.get('format') # By default use whatever format is given for the current context # unless the target is a different type to the source. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3c4be8aeb0..b0ddca2b59 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1035,13 +1035,16 @@ class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer): media_type = 'application/vnd.oai.openapi+json' charset = None format = 'openapi-json' + ensure_ascii = not api_settings.UNICODE_JSON def __init__(self): assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.' def render(self, data, media_type=None, renderer_context=None): structure = self.get_structure(data) - return json.dumps(structure, indent=4).encode('utf-8') + return json.dumps( + structure, indent=4, + ensure_ascii=self.ensure_ascii).encode('utf-8') class OpenAPIRenderer(BaseRenderer): @@ -1063,7 +1066,11 @@ def ignore_aliases(self, data): class JSONOpenAPIRenderer(BaseRenderer): media_type = 'application/vnd.oai.openapi+json' charset = None + encoder_class = encoders.JSONEncoder format = 'openapi-json' + ensure_ascii = not api_settings.UNICODE_JSON def render(self, data, media_type=None, renderer_context=None): - return json.dumps(data, indent=2).encode('utf-8') + return json.dumps( + data, cls=self.encoder_class, indent=2, + ensure_ascii=self.ensure_ascii).encode('utf-8') diff --git a/rest_framework/request.py b/rest_framework/request.py index 2a007cd2bb..17ceadb08e 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -316,7 +316,7 @@ def _supports_form_parsing(self): 'application/x-www-form-urlencoded', 'multipart/form-data' ) - return any([parser.media_type in form_media for parser in self.parsers]) + return any(parser.media_type in form_media for parser in self.parsers) def _parse(self): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index e2afa573fe..e0ae24b95c 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -290,7 +290,7 @@ def get(self, request, *args, **kwargs): args=args, kwargs=kwargs, request=request, - format=kwargs.get('format', None) + format=kwargs.get('format') ) except NoReverseMatch: # Don't bail out if eg. no list routes exist, only detail routes. diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 75ed5671af..179f0fa3c8 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -58,7 +58,7 @@ class LinkNode(OrderedDict): def __init__(self): self.links = [] self.methods_counter = Counter() - super(LinkNode, self).__init__() + super().__init__() def get_available_key(self, preferred_key): if preferred_key not in self: @@ -120,7 +120,7 @@ def __init__(self, title=None, url=None, description=None, patterns=None, urlcon assert coreapi, '`coreapi` must be installed for schema support.' assert coreschema, '`coreschema` must be installed for schema support.' - super(SchemaGenerator, self).__init__(title, url, description, patterns, urlconf) + super().__init__(title, url, description, patterns, urlconf) self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES def get_links(self, request=None): @@ -346,7 +346,7 @@ def __init__(self, manual_fields=None): * `manual_fields`: list of `coreapi.Field` instances that will be added to auto-generated fields, overwriting on `Field.name` """ - super(AutoSchema, self).__init__() + super().__init__() if manual_fields is None: manual_fields = [] self._manual_fields = manual_fields @@ -587,7 +587,7 @@ def __init__(self, fields, description='', encoding=None): * `fields`: list of `coreapi.Field` instances. * `description`: String description for view. Optional. """ - super(ManualSchema, self).__init__() + super().__init__() assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances" self._fields = fields self._description = description diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 4ecb7a65f1..5e9d59f8bf 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -192,15 +192,22 @@ def get_components(self, path, method): if method.lower() == 'delete': return {} - serializer = self.get_serializer(path, method) + request_serializer = self.get_request_serializer(path, method) + response_serializer = self.get_response_serializer(path, method) - if not isinstance(serializer, serializers.Serializer): - return {} + components = {} + + if isinstance(request_serializer, serializers.Serializer): + component_name = self.get_component_name(request_serializer) + content = self.map_serializer(request_serializer) + components.setdefault(component_name, content) - component_name = self.get_component_name(serializer) + if isinstance(response_serializer, serializers.Serializer): + component_name = self.get_component_name(response_serializer) + content = self.map_serializer(response_serializer) + components.setdefault(component_name, content) - content = self.map_serializer(serializer) - return {component_name: content} + return components def _to_camel_case(self, snake_str): components = snake_str.split('_') @@ -615,6 +622,20 @@ def get_serializer(self, path, method): .format(view.__class__.__name__, method, path)) return None + def get_request_serializer(self, path, method): + """ + Override this method if your view uses a different serializer for + handling request body. + """ + return self.get_serializer(path, method) + + def get_response_serializer(self, path, method): + """ + Override this method if your view uses a different serializer for + populating response data. + """ + return self.get_serializer(path, method) + def _get_reference(self, serializer): return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))} @@ -624,7 +645,7 @@ def get_request_body(self, path, method): self.request_media_types = self.map_parsers(path, method) - serializer = self.get_serializer(path, method) + serializer = self.get_request_serializer(path, method) if not isinstance(serializer, serializers.Serializer): item_schema = {} @@ -648,7 +669,7 @@ def get_responses(self, path, method): self.response_media_types = self.map_renderers(path, method) - serializer = self.get_serializer(path, method) + serializer = self.get_response_serializer(path, method) if not isinstance(serializer, serializers.Serializer): item_schema = {} diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 49eec82591..3896805177 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -71,7 +71,8 @@ LIST_SERIALIZER_KWARGS = ( 'read_only', 'write_only', 'required', 'default', 'initial', 'source', 'label', 'help_text', 'style', 'error_messages', 'allow_empty', - 'instance', 'data', 'partial', 'context', 'allow_null' + 'instance', 'data', 'partial', 'context', 'allow_null', + 'max_length', 'min_length' ) ALL_FIELDS = '__all__' @@ -143,12 +144,18 @@ def many_init(cls, *args, **kwargs): return CustomListSerializer(*args, **kwargs) """ allow_empty = kwargs.pop('allow_empty', None) + max_length = kwargs.pop('max_length', None) + min_length = kwargs.pop('min_length', None) child_serializer = cls(*args, **kwargs) list_kwargs = { 'child': child_serializer, } if allow_empty is not None: list_kwargs['allow_empty'] = allow_empty + if max_length is not None: + list_kwargs['max_length'] = max_length + if min_length is not None: + list_kwargs['min_length'] = min_length list_kwargs.update({ key: value for key, value in kwargs.items() if key in LIST_SERIALIZER_KWARGS @@ -568,12 +575,16 @@ class ListSerializer(BaseSerializer): default_error_messages = { 'not_a_list': _('Expected a list of items but got type "{input_type}".'), - 'empty': _('This list may not be empty.') + 'empty': _('This list may not be empty.'), + 'max_length': _('Ensure this field has no more than {max_length} elements.'), + 'min_length': _('Ensure this field has at least {min_length} elements.') } def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) + self.max_length = kwargs.pop('max_length', None) + self.min_length = kwargs.pop('min_length', None) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' super().__init__(*args, **kwargs) @@ -635,6 +646,18 @@ def to_internal_value(self, data): api_settings.NON_FIELD_ERRORS_KEY: [message] }, code='empty') + if self.max_length is not None and len(data) > self.max_length: + message = self.error_messages['max_length'].format(max_length=self.max_length) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='max_length') + + if self.min_length is not None and len(data) < self.min_length: + message = self.error_messages['min_length'].format(min_length=self.min_length) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='min_length') + ret = [] errors = [] @@ -1326,9 +1349,8 @@ def include_extra_kwargs(self, kwargs, extra_kwargs): """ if extra_kwargs.get('read_only', False): for attr in [ - 'required', 'default', 'allow_blank', 'allow_null', - 'min_length', 'max_length', 'min_value', 'max_value', - 'validators', 'queryset' + 'required', 'default', 'allow_blank', 'min_length', + 'max_length', 'min_value', 'max_value', 'validators', 'queryset' ]: kwargs.pop(attr, None) diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 86fef17737..51ca3ba19e 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -40,7 +40,7 @@ td.nested > table { margin: 0; } -form select, form input, form textarea { +form select, form input:not([type=checkbox]), form textarea { width: 90%; } diff --git a/rest_framework/static/rest_framework/docs/css/base.css b/rest_framework/static/rest_framework/docs/css/base.css index 0be2bafa91..06b240c522 100644 --- a/rest_framework/static/rest_framework/docs/css/base.css +++ b/rest_framework/static/rest_framework/docs/css/base.css @@ -7,15 +7,15 @@ h1 { } pre.highlight code * { - white-space: nowrap; // this sets all children inside to nowrap + white-space: nowrap; /* this sets all children inside to nowrap */ } pre.highlight { - overflow-x: auto; // this sets the scrolling in x + overflow-x: auto; /* this sets the scrolling in x */ } pre.highlight code { - white-space: pre; // forces to respect
 formatting
+  white-space: pre;       /* forces  to respect 
 formatting */
 }
 
 .main-container {
diff --git a/rest_framework/templates/rest_framework/filters/search.html b/rest_framework/templates/rest_framework/filters/search.html
index edb28d45d8..065c3889ac 100644
--- a/rest_framework/templates/rest_framework/filters/search.html
+++ b/rest_framework/templates/rest_framework/filters/search.html
@@ -5,7 +5,7 @@ 

{% trans "Search" %}

- +
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 7bfa8f5995..db0e9c95c3 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -206,7 +206,7 @@ def format_value(value): if value is None or isinstance(value, bool): return mark_safe('%s' % {True: 'true', False: 'false', None: 'null'}[value]) elif isinstance(value, list): - if any([isinstance(item, (list, dict)) for item in value]): + if any(isinstance(item, (list, dict)) for item in value): template = loader.get_template('rest_framework/admin/list_value.html') else: template = loader.get_template('rest_framework/admin/simple_list_value.html') @@ -285,7 +285,7 @@ def schema_links(section, sec_key=None): def add_nested_class(value): if isinstance(value, dict): return 'class=nested' - if isinstance(value, list) and any([isinstance(item, (list, dict)) for item in value]): + if isinstance(value, list) and any(isinstance(item, (list, dict)) for item in value): return 'class=nested' return '' diff --git a/rest_framework/test.py b/rest_framework/test.py index f2581cacca..0212348ee0 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -3,6 +3,7 @@ import io from importlib import import_module +import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.handlers.wsgi import WSGIHandler @@ -79,7 +80,7 @@ def send(self, request, *args, **kwargs): """ raw_kwargs = {} - def start_response(wsgi_status, wsgi_headers): + def start_response(wsgi_status, wsgi_headers, exc_info=None): status, _, reason = wsgi_status.partition(' ') raw_kwargs['status'] = int(status) raw_kwargs['reason'] = reason @@ -124,7 +125,7 @@ class CoreAPIClient(coreapi.Client): def __init__(self, *args, **kwargs): self._session = RequestsClient() kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)] - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @property def session(self): @@ -357,6 +358,13 @@ class APILiveServerTestCase(testcases.LiveServerTestCase): client_class = APIClient +def cleanup_url_patterns(cls): + if hasattr(cls, '_module_urlpatterns'): + cls._module.urlpatterns = cls._module_urlpatterns + else: + del cls._module.urlpatterns + + class URLPatternsTestCase(testcases.SimpleTestCase): """ Isolate URL patterns on a per-TestCase basis. For example, @@ -385,14 +393,20 @@ def setUpClass(cls): cls._module.urlpatterns = cls.urlpatterns cls._override.enable() + + if django.VERSION > (4, 0): + cls.addClassCleanup(cls._override.disable) + cls.addClassCleanup(cleanup_url_patterns, cls) + super().setUpClass() - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls._override.disable() + 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 + if hasattr(cls, '_module_urlpatterns'): + cls._module.urlpatterns = cls._module_urlpatterns + else: + del cls._module.urlpatterns diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 0ba2ba66b1..e262b886bc 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -52,7 +52,7 @@ class SimpleRateThrottle(BaseThrottle): A simple cache implementation, that only requires `.get_cache_key()` to be overridden. - The rate (requests / seconds) is set by a `rate` attribute on the View + The rate (requests / seconds) is set by a `rate` attribute on the Throttle class. The attribute is a string of the form 'number_of_requests/period'. Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index b18fbe0df9..4cd2ada314 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from django.utils.encoding import force_str @@ -87,7 +87,12 @@ def as_form_field(self): # value will be a JSONString, rather than a JSON primitive. if not getattr(value, 'is_json_string', False): try: - value = json.dumps(self.value, sort_keys=True, indent=4) + value = json.dumps( + self.value, + sort_keys=True, + indent=4, + separators=(',', ': '), + ) except (TypeError, ValueError): pass return self.__class__(self._field, value, self.errors, self._prefix) @@ -101,7 +106,7 @@ class NestedBoundField(BoundField): """ def __init__(self, field, value, errors, prefix=''): - if value is None or value == '': + if value is None or value == '' or not isinstance(value, Mapping): value = {} super().__init__(field, value, errors, prefix) @@ -115,6 +120,8 @@ def __getitem__(self, key): error = self.errors.get(key) if isinstance(self.errors, dict) else None if hasattr(field, 'fields'): return NestedBoundField(field, value, error, prefix=self.name + '.') + elif getattr(field, '_is_jsonfield', False): + return JSONBoundField(field, value, error, prefix=self.name + '.') return BoundField(field, value, error, prefix=self.name + '.') def as_form_field(self): diff --git a/rest_framework/views.py b/rest_framework/views.py index d1b5e4ed90..5b06220691 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -3,7 +3,7 @@ """ from django.conf import settings from django.core.exceptions import PermissionDenied -from django.db import connection, models, transaction +from django.db import connections, models from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers @@ -63,9 +63,9 @@ def get_view_description(view, html=False): def set_rollback(): - atomic_requests = connection.settings_dict.get('ATOMIC_REQUESTS', False) - if atomic_requests and connection.in_atomic_block: - transaction.set_rollback(True) + for db in connections.all(): + if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block: + db.set_rollback(True) def exception_handler(exc, context): diff --git a/runtests.py b/runtests.py index 82028ea32c..98f34c0673 100755 --- a/runtests.py +++ b/runtests.py @@ -1,42 +1,8 @@ #! /usr/bin/env python3 -import subprocess import sys import pytest -PYTEST_ARGS = { - 'default': [], - 'fast': ['-q'], -} - -FLAKE8_ARGS = ['rest_framework', 'tests'] - -ISORT_ARGS = ['--check-only', '--diff', 'rest_framework', 'tests'] - - -def exit_on_failure(ret, message=None): - if ret: - sys.exit(ret) - - -def flake8_main(args): - print('Running flake8 code linting') - ret = subprocess.call(['flake8'] + args) - print('flake8 failed' if ret else 'flake8 passed') - return ret - - -def isort_main(args): - print('Running isort code checking') - ret = subprocess.call(['isort'] + args) - - if ret: - print('isort failed: Some modules have incorrectly ordered imports. Fix by running `isort --recursive .`') - else: - print('isort passed') - - return ret - def split_class_and_function(string): class_string, function_string = string.split('.', 1) @@ -54,31 +20,6 @@ def is_class(string): if __name__ == "__main__": - try: - sys.argv.remove('--nolint') - except ValueError: - run_flake8 = True - run_isort = True - else: - run_flake8 = False - run_isort = False - - try: - sys.argv.remove('--lintonly') - except ValueError: - run_tests = True - else: - run_tests = False - - try: - sys.argv.remove('--fast') - except ValueError: - style = 'default' - else: - style = 'fast' - run_flake8 = False - run_isort = False - if len(sys.argv) > 1: pytest_args = sys.argv[1:] first_arg = pytest_args[0] @@ -105,13 +46,6 @@ def is_class(string): # `runtests.py test_function [flags]` pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] else: - pytest_args = PYTEST_ARGS[style] - - if run_tests: - exit_on_failure(pytest.main(pytest_args)) - - if run_flake8: - exit_on_failure(flake8_main(FLAKE8_ARGS)) + pytest_args = [] - if run_isort: - exit_on_failure(isort_main(ISORT_ARGS)) + sys.exit(pytest.main(pytest_args)) diff --git a/setup.cfg b/setup.cfg index abb7cca908..46ffb13c52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,7 @@ license_file = LICENSE.md [tool:pytest] -addopts=--tb=short --strict -ra -testspath = tests +addopts=--tb=short --strict-markers -ra [flake8] ignore = E501,W504 diff --git a/setup.py b/setup.py index e2a1c0222c..3c3761c866 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from setuptools import find_packages, setup CURRENT_PYTHON = sys.version_info[:2] -REQUIRED_PYTHON = (3, 5) +REQUIRED_PYTHON = (3, 6) # This check and everything above must remain compatible with Python 2.7. if CURRENT_PYTHON < REQUIRED_PYTHON: @@ -82,8 +82,8 @@ def get_version(package): author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=2.2"], - python_requires=">=3.5", + install_requires=["django>=2.2", "pytz"], + python_requires=">=3.6", zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -92,22 +92,25 @@ def get_version(package): 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Internet :: WWW/HTTP', ], project_urls={ 'Funding': 'https://fund.django-rest-framework.org/topics/funding/', 'Source': 'https://github.com/encode/django-rest-framework', + 'Changelog': 'https://www.django-rest-framework.org/community/release-notes/', }, ) diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index a73e0d79c7..d771aaf8b4 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,5 +1,6 @@ import base64 +import django import pytest from django.conf import settings from django.contrib.auth.models import User @@ -218,7 +219,16 @@ def test_post_form_session_auth_passing_csrf(self): Ensure POSTing form over session authentication with CSRF token succeeds. Regression test for #6088 """ - from django.middleware.csrf import _get_new_csrf_token + # Remove this shim when dropping support for Django 2.2. + if django.VERSION < (3, 0): + 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) diff --git a/tests/conftest.py b/tests/conftest.py index ac29e4a429..79cabd5e1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,18 +18,23 @@ def pytest_addoption(parser): def pytest_configure(config): from django.conf import settings + # USE_L10N is deprecated, and will be removed in Django 5.0. + use_l10n = {"USE_L10N": True} if django.VERSION < (4, 0) else {} settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' + }, + 'secondary': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' } }, SITE_ID=1, SECRET_KEY='not very secret in tests', USE_I18N=True, - USE_L10N=True, STATIC_URL='/static/', ROOT_URLCONF='tests.urls', TEMPLATES=[ @@ -64,6 +69,7 @@ def pytest_configure(config): PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', ), + **use_l10n, ) # guardian is optional diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 542c377b15..daa035a3f3 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -2,6 +2,7 @@ import warnings import pytest +from django.db import models from django.test import RequestFactory, TestCase, override_settings from django.urls import path from django.utils.translation import gettext_lazy as _ @@ -11,7 +12,8 @@ from rest_framework.compat import uritemplate from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import ( - BaseRenderer, BrowsableAPIRenderer, JSONRenderer, OpenAPIRenderer + BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer, + OpenAPIRenderer ) from rest_framework.request import Request from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator @@ -109,6 +111,24 @@ class Serializer(serializers.Serializer): assert data['properties']['default_false']['default'] is False, "default must be false" assert 'default' not in data['properties']['without_default'], "default must not be defined" + def test_nullable_fields(self): + class Model(models.Model): + rw_field = models.CharField(null=True) + ro_field = models.CharField(null=True) + + class Serializer(serializers.ModelSerializer): + class Meta: + model = Model + fields = ["rw_field", "ro_field"] + read_only_fields = ["ro_field"] + + inspector = AutoSchema() + + data = inspector.map_serializer(Serializer()) + assert data['properties']['rw_field']['nullable'], "rw_field nullable must be true" + assert data['properties']['ro_field']['nullable'], "ro_field nullable must be true" + assert data['properties']['ro_field']['readOnly'], "ro_field read_only must be true" + @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.') class TestOperationIntrospection(TestCase): @@ -711,6 +731,91 @@ def get_operation_id_base(self, path, method, action): operationId = inspector.get_operation_id(path, method) assert operationId == 'listItem' + def test_different_request_response_objects(self): + class RequestSerializer(serializers.Serializer): + text = serializers.CharField() + + class ResponseSerializer(serializers.Serializer): + text = serializers.BooleanField() + + class CustomSchema(AutoSchema): + def get_request_serializer(self, path, method): + return RequestSerializer() + + def get_response_serializer(self, path, method): + return ResponseSerializer() + + path = '/' + method = 'POST' + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = CustomSchema() + inspector.view = view + + components = inspector.get_components(path, method) + assert components == { + 'Request': { + 'properties': { + 'text': { + 'type': 'string' + } + }, + 'required': ['text'], + 'type': 'object' + }, + 'Response': { + 'properties': { + 'text': { + 'type': 'boolean' + } + }, + 'required': ['text'], + 'type': 'object' + } + } + + operation = inspector.get_operation(path, method) + assert operation == { + 'operationId': 'createExample', + 'description': '', + 'parameters': [], + 'requestBody': { + 'content': { + 'application/json': { + 'schema': { + '$ref': '#/components/schemas/Request' + } + }, + 'application/x-www-form-urlencoded': { + 'schema': { + '$ref': '#/components/schemas/Request' + } + }, + 'multipart/form-data': { + 'schema': { + '$ref': '#/components/schemas/Request' + } + } + } + }, + 'responses': { + '201': { + 'content': { + 'application/json': { + 'schema': { + '$ref': '#/components/schemas/Response' + } + } + }, + 'description': '' + } + }, + 'tags': [''] + } + def test_repeat_operation_ids(self): router = routers.SimpleRouter() router.register('account', views.ExampleGenericViewSet, basename="account") @@ -992,6 +1097,19 @@ def test_schema_construction(self): assert 'openapi' in schema assert 'paths' in schema + def test_schema_rendering_to_json(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = JSONOpenAPIRenderer().render(schema) + + assert b'"openapi": "' in ret + assert b'"default": "0.0"' in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 18b3beae4e..f1ed0bd4e3 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -1,4 +1,5 @@ import uuid +from datetime import timedelta from django.core.validators import ( DecimalValidator, MaxLengthValidator, MaxValueValidator, @@ -59,6 +60,7 @@ def get(self, *args, **kwargs): class ExampleSerializer(serializers.Serializer): date = serializers.DateField() datetime = serializers.DateTimeField() + duration = serializers.DurationField(default=timedelta()) hstore = serializers.HStoreField() uuid_field = serializers.UUIDField(default=uuid.uuid4) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 15b41e02f4..beda5cba19 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -130,6 +130,41 @@ def test_api_exception_rollback_transaction(self): assert BasicModel.objects.count() == 0 +@unittest.skipUnless( + connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints." +) +class MultiDBTransactionAPIExceptionTests(TestCase): + databases = '__all__' + + def setUp(self): + self.view = APIExceptionView.as_view() + connections.databases['default']['ATOMIC_REQUESTS'] = True + connections.databases['secondary']['ATOMIC_REQUESTS'] = True + + def tearDown(self): + connections.databases['default']['ATOMIC_REQUESTS'] = False + connections.databases['secondary']['ATOMIC_REQUESTS'] = False + + def test_api_exception_rollback_transaction(self): + """ + Transaction is rollbacked by our transaction atomic block. + """ + request = factory.post('/') + num_queries = 4 if connection.features.can_release_savepoints else 3 + with self.assertNumQueries(num_queries): + # 1 - begin savepoint + # 2 - insert + # 3 - rollback savepoint + # 4 - release savepoint + with transaction.atomic(), transaction.atomic(using='secondary'): + response = self.view(request) + assert transaction.get_rollback() + assert transaction.get_rollback(using='secondary') + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert BasicModel.objects.count() == 0 + + @unittest.skipUnless( connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py index dc5ab542ff..eee7d9b852 100644 --- a/tests/test_bound_fields.py +++ b/tests/test_bound_fields.py @@ -91,6 +91,10 @@ class ExampleSerializer(serializers.Serializer): assert rendered_packed == expected_packed +class CustomJSONField(serializers.JSONField): + pass + + class TestNestedBoundField: def test_nested_empty_bound_field(self): class Nested(serializers.Serializer): @@ -117,14 +121,31 @@ def test_as_form_fields(self): class Nested(serializers.Serializer): bool_field = serializers.BooleanField() null_field = serializers.IntegerField(allow_null=True) + json_field = serializers.JSONField() + custom_json_field = CustomJSONField() class ExampleSerializer(serializers.Serializer): nested = Nested() - serializer = ExampleSerializer(data={'nested': {'bool_field': False, 'null_field': None}}) + serializer = ExampleSerializer( + data={'nested': { + 'bool_field': False, 'null_field': None, + 'json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}, + 'custom_json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}, + }}) assert serializer.is_valid() assert serializer['nested']['bool_field'].as_form_field().value == '' assert serializer['nested']['null_field'].as_form_field().value == '' + assert serializer['nested']['json_field'].as_form_field().value == '''{ + "bool_item": true, + "number": 1, + "text_item": "text" +}''' + assert serializer['nested']['custom_json_field'].as_form_field().value == '''{ + "bool_item": true, + "number": 1, + "text_item": "text" +}''' def test_rendering_nested_fields_with_none_value(self): from rest_framework.renderers import HTMLFormRenderer @@ -163,6 +184,33 @@ class ExampleSerializer(serializers.Serializer): rendered_packed = ''.join(rendered.split()) assert rendered_packed == expected_packed + def test_rendering_nested_fields_with_not_mappable_value(self): + from rest_framework.renderers import HTMLFormRenderer + + class Nested(serializers.Serializer): + text_field = serializers.CharField() + + class ExampleSerializer(serializers.Serializer): + nested = Nested() + + serializer = ExampleSerializer(data={'nested': 1}) + assert not serializer.is_valid() + renderer = HTMLFormRenderer() + for field in serializer: + rendered = renderer.render_field(field, {}) + expected_packed = ( + '
' + 'Nested' + '' + '' + '' + '' + '
' + ) + + rendered_packed = ''.join(rendered.split()) + assert rendered_packed == expected_packed + class TestJSONBoundField: def test_as_form_fields(self): diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 99ba13e60c..116d6f1be4 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -162,6 +162,16 @@ def view(request): assert isinstance(view.cls.schema, CustomSchema) + def test_wrapper_assignments(self): + @api_view(["GET"]) + def test_view(request): + """example docstring""" + pass + + assert test_view.__name__ == "test_view" + assert test_view.__doc__ == "example docstring" + assert test_view.__qualname__ == "DecoratorTestCase.test_wrapper_assignments..test_view" + class ActionDecoratorTestCase(TestCase): diff --git a/tests/test_description.py b/tests/test_description.py index ae00fe4a97..363ad6513e 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -1,192 +1,178 @@ -from django.test import TestCase - -from rest_framework.compat import apply_markdown -from rest_framework.utils.formatting import dedent -from rest_framework.views import APIView - -# We check that docstrings get nicely un-indented. -DESCRIPTION = """an example docstring -==================== - -* list -* list - -another header --------------- - - code block - -indented - -# hash style header # - -``` json -[{ - "alpha": 1, - "beta: "this is a string" -}] -```""" - - -# If markdown is installed we also test it's working -# (and that our wrapped forces '=' to h2 and '-' to h3) -MARKED_DOWN_HILITE = """ -
[{
"alpha": 1,
\ - "beta: "this\ - is a \ -string"
}]
- -


""" - -MARKED_DOWN_NOT_HILITE = """ -

json -[{ - "alpha": 1, - "beta: "this is a string" -}]

""" - -# We support markdown < 2.1 and markdown >= 2.1 -MARKED_DOWN_lt_21 = """

an example docstring

-
    -
  • list
  • -
  • list
  • -
-

another header

-
code block
-
-

indented

-

hash style header

%s""" - -MARKED_DOWN_gte_21 = """

an example docstring

-
    -
  • list
  • -
  • list
  • -
-

another header

-
code block
-
-

indented

-

hash style header

%s""" - - -class TestViewNamesAndDescriptions(TestCase): - def test_view_name_uses_class_name(self): - """ - Ensure view names are based on the class name. - """ - class MockView(APIView): - pass - assert MockView().get_view_name() == 'Mock' - - def test_view_name_uses_name_attribute(self): - class MockView(APIView): - name = 'Foo' - assert MockView().get_view_name() == 'Foo' - - def test_view_name_uses_suffix_attribute(self): - class MockView(APIView): - suffix = 'List' - assert MockView().get_view_name() == 'Mock List' - - def test_view_name_preferences_name_over_suffix(self): - class MockView(APIView): - name = 'Foo' - suffix = 'List' - assert MockView().get_view_name() == 'Foo' - - def test_view_description_uses_docstring(self): - """Ensure view descriptions are based on the docstring.""" - class MockView(APIView): - """an example docstring - ==================== - - * list - * list - - another header - -------------- - - code block - - indented - - # hash style header # - - ``` json - [{ - "alpha": 1, - "beta: "this is a string" - }] - ```""" - - assert MockView().get_view_description() == DESCRIPTION - - def test_view_description_uses_description_attribute(self): - class MockView(APIView): - description = 'Foo' - assert MockView().get_view_description() == 'Foo' - - def test_view_description_allows_empty_description(self): - class MockView(APIView): - """Description.""" - description = '' - assert MockView().get_view_description() == '' - - def test_view_description_can_be_empty(self): - """ - Ensure that if a view has no docstring, - then it's description is the empty string. - """ - class MockView(APIView): - pass - assert MockView().get_view_description() == '' - - def test_view_description_can_be_promise(self): - """ - Ensure a view may have a docstring that is actually a lazily evaluated - class that can be converted to a string. - - See: https://github.com/encode/django-rest-framework/issues/1708 - """ - # use a mock object instead of gettext_lazy to ensure that we can't end - # up with a test case string in our l10n catalog - - class MockLazyStr: - def __init__(self, string): - self.s = string - - def __str__(self): - return self.s - - class MockView(APIView): - __doc__ = MockLazyStr("a gettext string") - - assert MockView().get_view_description() == 'a gettext string' - - def test_markdown(self): - """ - Ensure markdown to HTML works as expected. - """ - if apply_markdown: - md_applied = apply_markdown(DESCRIPTION) - gte_21_match = ( - md_applied == ( - MARKED_DOWN_gte_21 % MARKED_DOWN_HILITE) or - md_applied == ( - MARKED_DOWN_gte_21 % MARKED_DOWN_NOT_HILITE)) - lt_21_match = ( - md_applied == ( - MARKED_DOWN_lt_21 % MARKED_DOWN_HILITE) or - md_applied == ( - MARKED_DOWN_lt_21 % MARKED_DOWN_NOT_HILITE)) - assert gte_21_match or lt_21_match - - -def test_dedent_tabs(): - result = 'first string\n\nsecond string' - assert dedent(" first string\n\n second string") == result - assert dedent("first string\n\n second string") == result - assert dedent("\tfirst string\n\n\tsecond string") == result - assert dedent("first string\n\n\tsecond string") == result +import sys + +import pytest +from django.test import TestCase + +from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import dedent +from rest_framework.views import APIView + +# We check that docstrings get nicely un-indented. +DESCRIPTION = """an example docstring +==================== + +* list +* list + +another header +-------------- + + code block + +indented + +# hash style header # + +```json +[{ + "alpha": 1, + "beta": "this is a string" +}] +```""" + + +# If markdown is installed we also test it's working +# (and that our wrapped forces '=' to h2 and '-' to h3) +MARKDOWN_BASE = """

an example docstring

+
    +
  • list
  • +
  • list
  • +
+

another header

+
code block
+
+

indented

+

hash style header

%s""" + +MARKDOWN_gte_33 = """ +
[{
\ + "alpha":\ + 1,
\ + "beta":\ + "this is a string"
\ +}]
+


""" + +MARKDOWN_lt_33 = """ +
[{
\ + "alpha":\ + 1,
\ + "beta":\ + "this is a string"
\ +}]
+ +


""" + + +class TestViewNamesAndDescriptions(TestCase): + def test_view_name_uses_class_name(self): + """ + Ensure view names are based on the class name. + """ + class MockView(APIView): + pass + assert MockView().get_view_name() == 'Mock' + + def test_view_name_uses_name_attribute(self): + class MockView(APIView): + name = 'Foo' + assert MockView().get_view_name() == 'Foo' + + def test_view_name_uses_suffix_attribute(self): + class MockView(APIView): + suffix = 'List' + assert MockView().get_view_name() == 'Mock List' + + def test_view_name_preferences_name_over_suffix(self): + class MockView(APIView): + name = 'Foo' + suffix = 'List' + assert MockView().get_view_name() == 'Foo' + + def test_view_description_uses_docstring(self): + """Ensure view descriptions are based on the docstring.""" + class MockView(APIView): + """an example docstring + ==================== + + * list + * list + + another header + -------------- + + code block + + indented + + # hash style header # + + ```json + [{ + "alpha": 1, + "beta": "this is a string" + }] + ```""" + + assert MockView().get_view_description() == DESCRIPTION + + def test_view_description_uses_description_attribute(self): + class MockView(APIView): + description = 'Foo' + assert MockView().get_view_description() == 'Foo' + + def test_view_description_allows_empty_description(self): + class MockView(APIView): + """Description.""" + description = '' + assert MockView().get_view_description() == '' + + def test_view_description_can_be_empty(self): + """ + Ensure that if a view has no docstring, + then it's description is the empty string. + """ + class MockView(APIView): + pass + assert MockView().get_view_description() == '' + + def test_view_description_can_be_promise(self): + """ + Ensure a view may have a docstring that is actually a lazily evaluated + class that can be converted to a string. + + See: https://github.com/encode/django-rest-framework/issues/1708 + """ + # use a mock object instead of gettext_lazy to ensure that we can't end + # up with a test case string in our l10n catalog + + class MockLazyStr: + def __init__(self, string): + self.s = string + + def __str__(self): + return self.s + + class MockView(APIView): + __doc__ = MockLazyStr("a gettext string") + + assert MockView().get_view_description() == 'a gettext string' + + @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed") + def test_markdown(self): + """ + Ensure markdown to HTML works as expected. + """ + # Markdown 3.3 is only supported on Python 3.6 and higher + if sys.version_info >= (3, 6): + assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33 + else: + assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33 + + +def test_dedent_tabs(): + result = 'first string\n\nsecond string' + assert dedent(" first string\n\n second string") == result + assert dedent("first string\n\n second string") == result + assert dedent("\tfirst string\n\n\tsecond string") == result + assert dedent("first string\n\n\tsecond string") == result diff --git a/tests/test_fields.py b/tests/test_fields.py index fdd570d8a6..7a5304a82a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1090,6 +1090,9 @@ class TestDecimalField(FieldValues): '2E+1': Decimal('20'), } invalid_inputs = ( + (None, ["This field may not be null."]), + ('', ["A valid number is required."]), + (' ', ["A valid number is required."]), ('abc', ["A valid number is required."]), (Decimal('Nan'), ["A valid number is required."]), (Decimal('Snan'), ["A valid number is required."]), @@ -1115,6 +1118,32 @@ class TestDecimalField(FieldValues): field = serializers.DecimalField(max_digits=3, decimal_places=1) +class TestAllowNullDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: '', + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True) + + +class TestAllowNullNoStringCoercionDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: None, + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, coerce_to_string=False) + + class TestMinMaxDecimalField(FieldValues): """ Valid and invalid values for `DecimalField` with min and max limits. @@ -1134,6 +1163,30 @@ class TestMinMaxDecimalField(FieldValues): ) +class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues): + """ + Check that empty string ('', ' ') is acceptable value for the DecimalField + if allow_null=True and there are max/min validators + """ + valid_inputs = { + None: None, + '': None, + ' ': None, + ' ': None, + 5: Decimal('5'), + '0': Decimal('0'), + '10': Decimal('10'), + } + invalid_inputs = { + -1: ['Ensure this value is greater than or equal to 0.'], + 11: ['Ensure this value is less than or equal to 10.'], + } + outputs = { + None: '', + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, min_value=0, max_value=10) + + class TestNoMaxDigitsDecimalField(FieldValues): field = serializers.DecimalField( max_value=100, min_value=0, @@ -1167,12 +1220,12 @@ class TestNoStringCoercionDecimalField(FieldValues): class TestLocalizedDecimalField(TestCase): - @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') + @override_settings(LANGUAGE_CODE='pl') def test_to_internal_value(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) assert field.to_internal_value('1,1') == Decimal('1.1') - @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') + @override_settings(LANGUAGE_CODE='pl') def test_to_representation(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) assert field.to_representation(Decimal('1.1')) == '1,1' @@ -1411,15 +1464,24 @@ def setup_class(cls): cls.field = serializers.DateTimeField() cls.kolkata = pytz.timezone('Asia/Kolkata') + def assertUTC(self, tzinfo): + """ + Check UTC for datetime.timezone, ZoneInfo, and pytz tzinfo instances. + """ + assert ( + tzinfo is utc or + (getattr(tzinfo, "key", None) or getattr(tzinfo, "zone", None)) == "UTC" + ) + def test_default_timezone(self): - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) def test_current_timezone(self): - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) activate(self.kolkata) assert self.field.default_timezone() == self.kolkata deactivate() - assert self.field.default_timezone() == utc + self.assertUTC(self.field.default_timezone()) @pytest.mark.skipif(pytz is None, reason='pytz not installed') @@ -1456,7 +1518,7 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): } outputs = {} - class MockTimezone: + class MockTimezone(pytz.BaseTzInfo): @staticmethod def localize(value, is_dst): raise pytz.InvalidTimeError() @@ -1957,6 +2019,11 @@ def test_collection_types_are_invalid_input(self): field.to_internal_value(input_value) assert exc_info.value.detail == ['Expected a list of items but got type "dict".'] + def test_constructor_misuse_raises(self): + # Test that `ListField` can only be instantiated with keyword arguments + with pytest.raises(TypeError): + serializers.ListField(serializers.CharField()) + class TestNestedListField(FieldValues): """ diff --git a/tests/test_filters.py b/tests/test_filters.py index 567e5f83fc..37ae4c7cf3 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -424,6 +424,10 @@ class OrderingFilterModel(models.Model): title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) + @property + def description(self): + return self.title + ": " + self.text + class OrderingFilterRelatedModel(models.Model): related_object = models.ForeignKey(OrderingFilterModel, related_name="relateds", on_delete=models.CASCADE) @@ -436,6 +440,17 @@ class Meta: fields = '__all__' +class OrderingFilterSerializerWithModelProperty(serializers.ModelSerializer): + class Meta: + model = OrderingFilterModel + fields = ( + "id", + "title", + "text", + "description" + ) + + class OrderingDottedRelatedSerializer(serializers.ModelSerializer): related_text = serializers.CharField(source='related_object.text') related_title = serializers.CharField(source='related_object.title') @@ -551,6 +566,42 @@ class OrderingListView(generics.ListAPIView): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] + def test_ordering_without_ordering_fields(self): + class OrderingListView(generics.ListAPIView): + queryset = OrderingFilterModel.objects.all() + serializer_class = OrderingFilterSerializerWithModelProperty + filter_backends = (filters.OrderingFilter,) + ordering = ('title',) + + view = OrderingListView.as_view() + + # Model field ordering works fine. + request = factory.get('/', {'ordering': 'text'}) + response = view(request) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + ] + + # `incorrectfield` ordering works fine. + request = factory.get('/', {'ordering': 'foobar'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + ] + + # `description` is a Model property, which should be ignored. + request = factory.get('/', {'ordering': 'description'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + ] + def test_default_ordering(self): class OrderingListView(generics.ListAPIView): queryset = OrderingFilterModel.objects.all() diff --git a/tests/test_relations.py b/tests/test_relations.py index 92aeecf6c4..bb719a65a9 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -107,6 +107,12 @@ def test_pk_related_lookup_invalid_type(self): msg = excinfo.value.detail[0] assert msg == 'Incorrect type. Expected pk value, received BadType.' + def test_pk_related_lookup_bool(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.field.to_internal_value(True) + msg = excinfo.value.detail[0] + assert msg == 'Incorrect type. Expected pk value, received bool.' + def test_pk_representation(self): representation = self.field.to_representation(self.instance) assert representation == self.instance.pk diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index f35c4fcc9e..551f626662 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -616,3 +616,70 @@ def test_nested_serializer_with_list_multipart(self): assert serializer.is_valid() assert serializer.validated_data == [] + + +class TestMaxMinLengthListSerializer: + """ + Tests the behaviour of ListSerializers when max_length and min_length are used + """ + + def setup(self): + class IntegerSerializer(serializers.Serializer): + some_int = serializers.IntegerField() + + class MaxLengthSerializer(serializers.Serializer): + many_int = IntegerSerializer(many=True, max_length=5) + + class MinLengthSerializer(serializers.Serializer): + many_int = IntegerSerializer(many=True, min_length=3) + + class MaxMinLengthSerializer(serializers.Serializer): + many_int = IntegerSerializer(many=True, min_length=3, max_length=5) + + self.MaxLengthSerializer = MaxLengthSerializer + self.MinLengthSerializer = MinLengthSerializer + self.MaxMinLengthSerializer = MaxMinLengthSerializer + + def test_min_max_length_two_items(self): + input_data = {'many_int': [{'some_int': i} for i in range(2)]} + + max_serializer = self.MaxLengthSerializer(data=input_data) + min_serializer = self.MinLengthSerializer(data=input_data) + max_min_serializer = self.MaxMinLengthSerializer(data=input_data) + + assert max_serializer.is_valid() + assert max_serializer.validated_data == input_data + + assert not min_serializer.is_valid() + + assert not max_min_serializer.is_valid() + + def test_min_max_length_four_items(self): + input_data = {'many_int': [{'some_int': i} for i in range(4)]} + + max_serializer = self.MaxLengthSerializer(data=input_data) + min_serializer = self.MinLengthSerializer(data=input_data) + max_min_serializer = self.MaxMinLengthSerializer(data=input_data) + + assert max_serializer.is_valid() + assert max_serializer.validated_data == input_data + + assert min_serializer.is_valid() + assert min_serializer.validated_data == input_data + + assert max_min_serializer.is_valid() + assert min_serializer.validated_data == input_data + + def test_min_max_length_six_items(self): + input_data = {'many_int': [{'some_int': i} for i in range(6)]} + + max_serializer = self.MaxLengthSerializer(data=input_data) + min_serializer = self.MinLengthSerializer(data=input_data) + max_min_serializer = self.MaxMinLengthSerializer(data=input_data) + + assert not max_serializer.is_valid() + + assert min_serializer.is_valid() + assert min_serializer.validated_data == input_data + + assert not max_min_serializer.is_valid() diff --git a/tests/test_status.py b/tests/test_status.py index 07d893bee9..b10f7df994 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,8 +1,7 @@ from django.test import TestCase from rest_framework.status import ( - is_client_error, is_informational, is_redirect, is_server_error, - is_success + is_client_error, is_informational, is_redirect, is_server_error, is_success ) diff --git a/tests/test_testing.py b/tests/test_testing.py index cc60e4f003..5066ee142e 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,5 +1,6 @@ from io import BytesIO +import django from django.contrib.auth.models import User from django.shortcuts import redirect from django.test import TestCase, override_settings @@ -282,6 +283,10 @@ def test_empty_request_content_type(self): assert request.META['CONTENT_TYPE'] == 'application/json' +def check_urlpatterns(cls): + assert urlpatterns is not cls.urlpatterns + + class TestUrlPatternTestCase(URLPatternsTestCase): urlpatterns = [ path('', view), @@ -293,11 +298,18 @@ def setUpClass(cls): super().setUpClass() assert urlpatterns is cls.urlpatterns - @classmethod - def tearDownClass(cls): - assert urlpatterns is cls.urlpatterns - super().tearDownClass() - assert urlpatterns is not cls.urlpatterns + if django.VERSION > (4, 0): + cls.addClassCleanup( + check_urlpatterns, + cls + ) + + 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): assert self.client.get('/').status_code == 200 diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 562fe37e6b..341c4342a5 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -2,6 +2,7 @@ from rest_framework import serializers, status from rest_framework.decorators import api_view +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory @@ -99,3 +100,12 @@ def test_function_based_view_exception_handler(self): response = view(request) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data == self.expected_response_data + + +class TestValidationErrorConvertsTuplesToLists(TestCase): + def test_validation_error_details(self): + error = ValidationError(detail=('message1', 'message2')) + assert isinstance(error.detail, list) + assert len(error.detail) == 2 + assert str(error.detail[0]) == 'message1' + assert str(error.detail[1]) == 'message2' diff --git a/tests/test_validators.py b/tests/test_validators.py index 4962cf5816..bccbe1514b 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -7,8 +7,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.validators import ( - BaseUniqueForValidator, UniqueTogetherValidator, UniqueValidator, - qs_exists + BaseUniqueForValidator, UniqueTogetherValidator, UniqueValidator, qs_exists ) diff --git a/tox.ini b/tox.ini index df6387d5e1..a41176d72f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,29 +1,31 @@ [tox] envlist = - {py35,py36,py37}-django22, - {py36,py37,py38}-django30, + {py36,py37,py38,py39}-django22, {py36,py37,py38,py39}-django31, - {py36,py37,py38,py39}-djangomaster, - base,dist,lint,docs, + {py36,py37,py38,py39,py310}-django32, + {py38,py39,py310}-{django40,djangomain}, + base,dist,docs, [travis:env] DJANGO = 2.2: django22 - 3.0: django30 3.1: django31 - master: djangomaster + 3.2: django32 + 4.0: django40 + main: djangomain [testenv] -commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning runtests.py --fast --coverage {posargs} +commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning runtests.py --coverage {posargs} envdir = {toxworkdir}/venvs/{envname} setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = django22: Django>=2.2,<3.0 - django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 - djangomaster: https://github.com/django/django/archive/master.tar.gz + django32: Django>=3.2,<4.0 + django40: Django>=4.0,<5.0 + djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt @@ -34,21 +36,24 @@ deps = -rrequirements/requirements-testing.txt [testenv:dist] -commands = ./runtests.py --fast --no-pkgroot --staticfiles {posargs} +commands = ./runtests.py --no-pkgroot --staticfiles {posargs} deps = django -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt -[testenv:lint] -commands = ./runtests.py --lintonly -deps = - -rrequirements/requirements-codestyle.txt - -rrequirements/requirements-testing.txt - [testenv:docs] skip_install = true commands = mkdocs build deps = -rrequirements/requirements-testing.txt -rrequirements/requirements-documentation.txt + +[testenv:py38-djangomain] +ignore_outcome = true + +[testenv:py39-djangomain] +ignore_outcome = true + +[testenv:py310-djangomain] +ignore_outcome = true